@erclx/canon 4.77.0 → 4.79.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 +9 -9
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-operator/SKILL.md +1 -1
- package/claude/skills/create-standard/SKILL.md +1 -1
- package/claude/skills/design-extract/REQUIREMENT.md +4 -4
- package/claude/skills/design-extract/SKILL.md +11 -11
- package/claude/skills/docs-fold/SKILL.md +14 -14
- package/claude/skills/docs-fold/references/anchor-sweep.md +1 -1
- package/claude/skills/docs-fold/references/wireframe-sweep.md +4 -4
- package/claude/skills/draft-context/REQUIREMENT.md +6 -6
- package/claude/skills/draft-context/SKILL.md +8 -6
- package/claude/skills/draft-diagram/SKILL.md +5 -5
- package/claude/skills/draft-identity/SKILL.md +3 -3
- package/claude/skills/draft-readme/REQUIREMENT.md +1 -1
- package/claude/skills/draft-screencast/SKILL.md +1 -1
- package/claude/skills/draft-wireframes/REQUIREMENT.md +7 -7
- package/claude/skills/draft-wireframes/SKILL.md +7 -5
- package/claude/skills/memory-capture/REQUIREMENT.md +1 -1
- package/claude/skills/memory-capture/SKILL.md +6 -6
- package/claude/skills/memory-review/SKILL.md +1 -1
- package/claude/skills/migration-claude-md/REQUIREMENT.md +3 -3
- package/claude/skills/migration-claude-md/SKILL.md +7 -5
- package/claude/skills/migration-context/REQUIREMENT.md +3 -3
- package/claude/skills/migration-context/SKILL.md +11 -9
- package/claude/skills/plan-feature/SKILL.md +4 -4
- package/claude/skills/plan-groundwork/SKILL.md +2 -2
- package/claude/skills/plan-intake/SKILL.md +2 -2
- package/claude/skills/project-commands/SKILL.md +2 -2
- package/claude/skills/review-branch/SKILL.md +3 -3
- package/claude/skills/review-pr/SKILL.md +2 -2
- package/claude/skills/role-orchestrator/SKILL.md +1 -1
- package/claude/skills/role-planner/SKILL.md +1 -1
- package/claude/skills/session-relay/REQUIREMENT.md +1 -1
- package/claude/skills/session-resume/SKILL.md +1 -1
- package/claude/skills/setup-gov/SKILL.md +1 -1
- package/claude/skills/setup-init/SKILL.md +1 -1
- package/claude/skills/teach-workspace/references/promotion.md +5 -7
- package/claude/skills/ux-audit/SKILL.md +5 -5
- package/claude/skills/ux-measure/SKILL.md +1 -1
- package/docs/agents/capture.md +6 -5
- package/docs/agents/commands.md +8 -3
- package/docs/agents/context-audit-checks.md +3 -3
- package/docs/agents/context-audit.md +2 -2
- package/docs/agents/counts.md +3 -3
- package/docs/agents/design-board.md +3 -3
- package/docs/agents/docs.md +3 -3
- package/docs/agents/driver.md +1 -1
- package/docs/agents/indexes.md +1 -1
- package/docs/agents/install-and-sync.md +1 -1
- package/docs/agents/review-classification.md +1 -1
- package/docs/agents/routing.md +2 -2
- package/docs/agents/rule-citations.md +1 -1
- package/docs/agents/sandbox.md +1 -1
- package/docs/agents/scripting.md +1 -1
- package/docs/agents/skills-audit.md +1 -1
- package/docs/agents/skills-reach.md +4 -4
- package/docs/target-projects.md +10 -10
- package/docs/workflow/ai-workflow.md +16 -14
- package/docs/workflow/operating-model.md +2 -2
- package/docs/workflow/visual-design-workflow.md +20 -21
- package/governance/rules/claude/510-context.md +3 -2
- package/governance/rules/claude/520-wireframes.md +3 -2
- package/governance/rules/claude/530-requirements.md +2 -1
- package/governance/rules/claude/540-architecture.md +2 -1
- package/governance/rules/claude/550-design.md +2 -1
- package/governance/rules/core/025-indexes.md +1 -1
- package/governance/rules/core/045-memory.md +1 -1
- package/package.json +1 -1
- package/scripts/core/check-ignore-parity.sh +1 -1
- package/scripts/core/check-seed-independence.sh +5 -2
- package/scripts/core/install-check.sh +1 -1
- package/scripts/core/list-seed-roots.sh +3 -3
- package/scripts/core/regen-hero.sh +1 -1
- package/scripts/docs/list.sh +3 -3
- package/scripts/lib/sandbox-git.sh +1 -1
- package/scripts/lib/tooling.sh +6 -4
- package/snippets/claude/figma-steps.md +1 -1
- package/src/audits/catalog.ts +2 -2
- package/src/autoship/paths.ts +1 -1
- package/src/capture/render.ts +26 -9
- package/src/capture/sources.ts +29 -3
- package/src/claude/cases/authoring.ts +2 -2
- package/src/claude/cases/setup.ts +1 -1
- package/src/claude/routing.ts +1 -1
- package/src/claude/seeds-list.ts +1 -1
- package/src/claude/seeds.ts +62 -28
- package/src/claude/skills-reach.ts +3 -3
- package/src/commands/capture.ts +17 -6
- package/src/commands/claude.ts +1 -1
- package/src/commands/context.ts +8 -8
- package/src/commands/design.ts +1 -1
- package/src/commands/gov.ts +1 -1
- package/src/commands/migrate.ts +182 -1
- package/src/context/audit.ts +1 -1
- package/src/context/citations.ts +7 -5
- package/src/context/folders.ts +2 -2
- package/src/counts/scan.ts +4 -4
- package/src/design/board.ts +5 -5
- package/src/design/components.ts +2 -2
- package/src/design/css.ts +1 -1
- package/src/design/document.ts +1 -1
- package/src/design/regen.ts +1 -1
- package/src/design/render.ts +1 -1
- package/src/design/tokens.ts +2 -2
- package/src/docs/read.ts +9 -3
- package/src/gate/measures.ts +10 -7
- package/src/gate/stages.ts +4 -4
- package/src/markdown/scan.ts +1 -1
- package/src/migrate/surface-roots.ts +458 -0
- package/src/record-root.ts +6 -4
- package/src/sandbox/census.ts +1 -1
- package/src/shipped/references.ts +3 -3
- package/src/surface-root.ts +39 -7
- package/src/sync/layout.ts +10 -6
- package/src/sync/reverse.ts +1 -1
- package/src/tooling/inject.ts +5 -3
- package/src/tooling/scan.ts +4 -1
- package/standards/architecture.md +4 -4
- package/standards/context.md +7 -7
- package/standards/design.md +3 -3
- package/standards/diagrams.md +6 -6
- package/standards/index.md +5 -5
- package/standards/markdown.md +2 -2
- package/standards/publish.md +3 -3
- package/standards/readme.md +1 -1
- package/standards/requirements.md +4 -4
- package/standards/skill.md +2 -2
- package/standards/tasks.md +1 -1
- package/standards/wiki.md +3 -5
- package/standards/wireframes.md +8 -8
- package/tooling/astro/reference.md +3 -3
- package/tooling/base/reference.md +3 -3
- package/tooling/claude/reference.md +7 -3
- package/tooling/claude/seeds/CLAUDE.md +9 -9
- package/tooling/nextjs/reference.md +2 -2
- package/tooling/python/reference.md +1 -1
- package/tooling/vite-react/reference.md +2 -2
- package/tooling/web/reference.md +2 -2
- /package/tooling/base/seeds/{.claude → canon}/context/ci.md +0 -0
- /package/tooling/base/seeds/{.claude → canon}/context/development.md +0 -0
- /package/tooling/base/seeds/{.claude → canon}/context/index.md +0 -0
- /package/tooling/claude/seeds/{.claude → canon}/ARCHITECTURE.md +0 -0
- /package/tooling/claude/seeds/{.claude → canon}/DESIGN.md +0 -0
- /package/tooling/claude/seeds/{.claude → canon}/REQUIREMENTS.md +0 -0
- /package/tooling/claude/seeds/{.claude → canon}/wireframes/index.md +0 -0
|
@@ -6,4 +6,4 @@ description: Check a folder's index before searching its contents
|
|
|
6
6
|
|
|
7
7
|
## Before searching
|
|
8
8
|
|
|
9
|
-
- Check a folder's `index.md` before grepping its source or reading its files, starting with
|
|
9
|
+
- Check a folder's `index.md` before grepping its source or reading its files, starting with `canon/context/` for a domain and `canon/wireframes/` for a UI surface. It orients faster than a blind search.
|
|
@@ -7,6 +7,6 @@ description: Keep memory writes scoped to .canon/memory/ and out of context-owne
|
|
|
7
7
|
## Writing memory
|
|
8
8
|
|
|
9
9
|
- Write all memory files to `.canon/memory/`, not `~/.claude/projects/`
|
|
10
|
-
- A fact about a domain goes to that domain's
|
|
10
|
+
- A fact about a domain goes to that domain's `canon/context/` entry, not to memory. `canon:memory-capture` routes it there and `canon:docs-fold` folds it in. Memory keeps only what no context entry owns. Report it rather than proceeding silently when either skill does not resolve. Both ship with the plugin and this rule ships with the CLI, so a project that installed governance alone does not have them.
|
|
11
11
|
- Never delete a memory entry. Retire one by moving it to `.canon/tmp/memory-archive/`. A bulk retire runs through the shell, where no file edit fires a path-scoped rule, and the folder is gitignored with nothing to recover from.
|
|
12
12
|
- Follow the memory standard for the filename and type prefix, the frontmatter, the body shape each type carries, and the lifecycle. Read it with `canon standards memory`. Check every entry in the pen against that standard and fix what breaks it, since nothing keeps the folder conforming on its own.
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ SECTION="# Claude"
|
|
|
19
19
|
# The diagrams divergence is the one that cost something. A target used to track
|
|
20
20
|
# its diagrams and keep the context audit's default coverage of them, and it now
|
|
21
21
|
# ignores them with the rest of its records, so that coverage is gone unless the
|
|
22
|
-
# project names the folder itself.
|
|
22
|
+
# project names the folder itself. `canon/context/tooling.md` carries the
|
|
23
23
|
# narrative.
|
|
24
24
|
#
|
|
25
25
|
# An exception mechanism is not kept against a future divergence, because an
|
|
@@ -23,7 +23,7 @@ fi
|
|
|
23
23
|
# stages disagree about what the same condition means.
|
|
24
24
|
seed_roots=$(collect_seed_roots)
|
|
25
25
|
if [ -z "$seed_roots" ]; then
|
|
26
|
-
echo "No seed root carries .claude/, nothing to check." >&2
|
|
26
|
+
echo "No seed root carries .claude/ or canon/, nothing to check." >&2
|
|
27
27
|
exit 0
|
|
28
28
|
fi
|
|
29
29
|
|
|
@@ -36,9 +36,12 @@ measured=0
|
|
|
36
36
|
while IFS= read -r seed_root; do
|
|
37
37
|
while IFS= read -r file; do
|
|
38
38
|
measured=$((measured + 1))
|
|
39
|
+
# A token followed by `/` is the tracked surface root, a folder the seed
|
|
40
|
+
# itself installs, rather than the binary. Every other spelling still
|
|
41
|
+
# reports, which keeps a verb, a plugin skill prefix, and a bare mention in.
|
|
39
42
|
while IFS= read -r hit; do
|
|
40
43
|
cited="$cited ${file#"$PROJECT_ROOT/"}:$hit"$'\n'
|
|
41
|
-
done < <(grep -
|
|
44
|
+
done < <(grep -nE "${TOOLKIT_TOKEN}([^/]|$)" "$file" || true)
|
|
42
45
|
done < <(find "$PROJECT_ROOT/$seed_root" -type f -name '*.md')
|
|
43
46
|
done <<<"$seed_roots"
|
|
44
47
|
|
|
@@ -95,7 +95,7 @@ log_step "Assert scaffold"
|
|
|
95
95
|
# `@`-reference convention rule is the one snippets-domain file that still
|
|
96
96
|
# installs, since `base` carries `governance/rules/snippets/` as a
|
|
97
97
|
# folder-whole entry the same way it does `core` and `claude`.
|
|
98
|
-
for path in "CLAUDE.md" ".claude/wiki/index.md" ".claude" "
|
|
98
|
+
for path in "CLAUDE.md" ".claude/wiki/index.md" ".claude" "canon/context/index.md" "canon/wireframes/index.md" ".canon/diagrams/index.md" \
|
|
99
99
|
".prettierrc" ".editorconfig" ".lintstagedrc" ".husky/pre-commit" ".github/workflows/verify.yml" "scripts/verify.sh" \
|
|
100
100
|
".claude/rules/canon/core/000-constitution.md" ".claude/rules/canon/snippets/600-at-references.md"; do
|
|
101
101
|
if [ ! -e "$TARGET_DIR/$path" ]; then
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Prints every seed root carrying a `.claude/`, one per line and
|
|
3
|
-
# the project root.
|
|
2
|
+
# Prints every seed root carrying a `.claude/` or a `canon/`, one per line and
|
|
3
|
+
# relative to the project root.
|
|
4
4
|
#
|
|
5
5
|
# The discovery rule has one definition, `collect_seed_roots` in
|
|
6
6
|
# `scripts/lib/tooling.sh`, which `check-seed-independence.sh` already reads.
|
|
7
7
|
# This file is the route a caller outside bash takes to that same answer, so a
|
|
8
|
-
# stack seeding
|
|
8
|
+
# stack seeding either root later reaches both readers and the two stages
|
|
9
9
|
# measuring seed content cannot disagree about which roots exist.
|
|
10
10
|
set -e
|
|
11
11
|
set -o pipefail
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# source, writing the .html beside each one.
|
|
4
4
|
#
|
|
5
5
|
# The name says hero because the hero was the only template when it was written
|
|
6
|
-
# and every citation of it across docs/,
|
|
6
|
+
# and every citation of it across docs/, canon/context/, and src/ spells that
|
|
7
7
|
# name. Renaming the file is a sweep across roughly twenty surfaces for a
|
|
8
8
|
# cosmetic gain, so the file keeps its name and the loop below covers whatever
|
|
9
9
|
# templates the folder holds.
|
package/scripts/docs/list.sh
CHANGED
|
@@ -9,7 +9,7 @@ source "$PROJECT_ROOT/scripts/lib/ui.sh"
|
|
|
9
9
|
source "$PROJECT_ROOT/scripts/lib/frontmatter.sh"
|
|
10
10
|
|
|
11
11
|
DOCS_DIR="$PROJECT_ROOT/docs"
|
|
12
|
-
CONTEXT_DIR="$PROJECT_ROOT
|
|
12
|
+
CONTEXT_DIR="$PROJECT_ROOT/canon/context"
|
|
13
13
|
|
|
14
14
|
INTERNAL_TOPICS=" ci development sandbox "
|
|
15
15
|
|
|
@@ -97,7 +97,7 @@ collect_context() {
|
|
|
97
97
|
[ "$name" = "index" ] && continue
|
|
98
98
|
is_internal_topic "$name" && continue
|
|
99
99
|
description=$(read_frontmatter_field "$file" "description")
|
|
100
|
-
printf '%s\t%s\t%s\n' "$name" "$description" "
|
|
100
|
+
printf '%s\t%s\t%s\n' "$name" "$description" "canon/context/$name.md"
|
|
101
101
|
done < <(find "$CONTEXT_DIR" -maxdepth 1 -type f -name "*.md")
|
|
102
102
|
|
|
103
103
|
# A split domain is named by its folder and described by its generated
|
|
@@ -106,7 +106,7 @@ collect_context() {
|
|
|
106
106
|
name=$(basename "$(dirname "$file")")
|
|
107
107
|
is_internal_topic "$name" && continue
|
|
108
108
|
description=$(read_frontmatter_field "$file" "subtitle")
|
|
109
|
-
printf '%s\t%s\t%s\n' "$name" "$description" "
|
|
109
|
+
printf '%s\t%s\t%s\n' "$name" "$description" "canon/context/$name/index.md"
|
|
110
110
|
done < <(find "$CONTEXT_DIR" -mindepth 2 -maxdepth 2 -type f -name "index.md")
|
|
111
111
|
} | sort
|
|
112
112
|
}
|
|
@@ -95,7 +95,7 @@ ensure_sandbox_anchor_repo() {
|
|
|
95
95
|
# nine to a pass against something that is not the anchor. Refusing is the safe
|
|
96
96
|
# default and creating is the opt-in, the shape `canon tooling sync --write`
|
|
97
97
|
# already sets. `canon records push` refuses outright for the reason
|
|
98
|
-
#
|
|
98
|
+
# `canon/context/development/scratch.md` records, so the two still differ.
|
|
99
99
|
# The sibling SANDBOX_ flags are presence tests, so any non-empty value turns
|
|
100
100
|
# them on. This one allowlists instead, because a presence test would have
|
|
101
101
|
# SANDBOX_ANCHOR_CREATE=false provisioning a repository. Both spellings are
|
package/scripts/lib/tooling.sh
CHANGED
|
@@ -23,13 +23,15 @@ list_tooling_stacks() {
|
|
|
23
23
|
done < <(find "$tooling_dir" -mindepth 1 -maxdepth 1 -type d | sort)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
# Seed roots that carry a `.claude/`, emitted relative to
|
|
27
|
-
# stage measuring seed content discovers through this
|
|
28
|
-
# so a stack seeding
|
|
26
|
+
# Seed roots that carry a `.claude/` or a `canon/`, emitted relative to
|
|
27
|
+
# `PROJECT_ROOT`. Every stage measuring seed content discovers through this
|
|
28
|
+
# rather than naming a stack, so a stack seeding either root later arrives
|
|
29
|
+
# covered with no edit to any caller. A stack seeding only tracked surfaces
|
|
30
|
+
# carries `canon/` alone, which a `.claude/` test would drop from both stages.
|
|
29
31
|
collect_seed_roots() {
|
|
30
32
|
local dir
|
|
31
33
|
for dir in "$PROJECT_ROOT"/tooling/*/seeds; do
|
|
32
|
-
[ -d "$dir/.claude" ] || continue
|
|
34
|
+
[ -d "$dir/.claude" ] || [ -d "$dir/canon" ] || continue
|
|
33
35
|
printf '%s\n' "${dir#"$PROJECT_ROOT"/}"
|
|
34
36
|
done
|
|
35
37
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Read the relevant spec from
|
|
1
|
+
Read the relevant spec from `canon/DESIGN.md` and any other design files needed. Write step-by-step Figma instructions to produce the component exactly.
|
|
2
2
|
|
|
3
3
|
For each step, specify:
|
|
4
4
|
|
package/src/audits/catalog.ts
CHANGED
|
@@ -490,8 +490,8 @@ export const AUDITS: readonly AuditSpec[] = [
|
|
|
490
490
|
corpus: 'tracked',
|
|
491
491
|
// The one reason this verb refuses for that is an absence rather than a
|
|
492
492
|
// break, on the same test the reach check and the skill audit take: no
|
|
493
|
-
// target adopts
|
|
494
|
-
//
|
|
493
|
+
// target adopts `canon/context/`, `.canon/diagrams/`, and
|
|
494
|
+
// `canon/wireframes/` all at once, so without the allowance every such
|
|
495
495
|
// project reports the verb unmeasured on every run and never changes.
|
|
496
496
|
absentReasons: ['no-folders'] satisfies ContextAuditRefusal[],
|
|
497
497
|
counts: contextCounts,
|
package/src/autoship/paths.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* application three times, so the set moved here and the body now calls a verb
|
|
8
8
|
* that reads it. Being machine-parsed makes it permanently exempt from any
|
|
9
9
|
* later design that folds a standard back into the surface citing it, per the
|
|
10
|
-
* machine-parsed clause in
|
|
10
|
+
* machine-parsed clause in `canon/ARCHITECTURE.md`.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
package/src/capture/render.ts
CHANGED
|
@@ -37,12 +37,12 @@ export interface CaptureOptions {
|
|
|
37
37
|
export type CaptureResult =
|
|
38
38
|
| {
|
|
39
39
|
status: 'rendered'
|
|
40
|
-
|
|
40
|
+
source: string
|
|
41
41
|
pngPath: string
|
|
42
42
|
width: number
|
|
43
43
|
height: number
|
|
44
44
|
}
|
|
45
|
-
| { status: 'failed';
|
|
45
|
+
| { status: 'failed'; source: string; reason: string }
|
|
46
46
|
|
|
47
47
|
export async function captureSources(
|
|
48
48
|
sourcePath: string,
|
|
@@ -74,7 +74,9 @@ async function captureOne(
|
|
|
74
74
|
deviceScaleFactor: DEVICE_SCALE_FACTOR,
|
|
75
75
|
})
|
|
76
76
|
try {
|
|
77
|
-
await page.goto(
|
|
77
|
+
await page.goto(
|
|
78
|
+
source.kind === 'url' ? source.url : pathToFileURL(source.htmlPath).href,
|
|
79
|
+
)
|
|
78
80
|
const element = page.locator(selector).first()
|
|
79
81
|
if ((await element.count()) === 0) {
|
|
80
82
|
return failed(source, `no element matched ${selector}`)
|
|
@@ -97,7 +99,7 @@ async function captureOne(
|
|
|
97
99
|
writeStamp(source, png)
|
|
98
100
|
return {
|
|
99
101
|
status: 'rendered',
|
|
100
|
-
|
|
102
|
+
source: sourceIdentifier(source),
|
|
101
103
|
pngPath: source.pngPath,
|
|
102
104
|
width: png.readUInt32BE(16),
|
|
103
105
|
height: png.readUInt32BE(20),
|
|
@@ -118,8 +120,11 @@ async function captureOne(
|
|
|
118
120
|
* caller's catch and reports the source as failed, which is correct: a PNG
|
|
119
121
|
* whose stamp never landed is the state the verify stage exists to reject.
|
|
120
122
|
*
|
|
121
|
-
*
|
|
122
|
-
* machine that ran the capture into a tracked file and differ per
|
|
123
|
+
* A file source is stored as a bare filename. An absolute path would record
|
|
124
|
+
* the machine that ran the capture into a tracked file and differ per
|
|
125
|
+
* checkout. A URL source is stored as the URL itself, and its digest hashes
|
|
126
|
+
* the URL string's UTF-8 bytes rather than any local file, since there is
|
|
127
|
+
* none to hash.
|
|
123
128
|
*
|
|
124
129
|
* The image digest is taken over the buffer the screenshot returned rather than
|
|
125
130
|
* by reading the file back, so the stamp describes the bytes this run wrote.
|
|
@@ -128,15 +133,27 @@ function writeStamp(source: CaptureSource, png: Uint8Array): void {
|
|
|
128
133
|
writeFileSync(
|
|
129
134
|
stampPath(source.pngPath),
|
|
130
135
|
formatStamp({
|
|
131
|
-
source: basename(source.htmlPath),
|
|
132
|
-
sourceSha256:
|
|
136
|
+
source: source.kind === 'url' ? source.url : basename(source.htmlPath),
|
|
137
|
+
sourceSha256:
|
|
138
|
+
source.kind === 'url'
|
|
139
|
+
? hashSource(Buffer.from(source.url, 'utf8'))
|
|
140
|
+
: hashSource(readFileSync(source.htmlPath)),
|
|
133
141
|
imageSha256: hashSource(png),
|
|
134
142
|
}),
|
|
135
143
|
)
|
|
136
144
|
}
|
|
137
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The identifier a `CaptureResult` reports, kept as the full path for a file
|
|
148
|
+
* source so `displayPath` in `src/commands/capture.ts` can still show it
|
|
149
|
+
* relative to the working directory.
|
|
150
|
+
*/
|
|
151
|
+
function sourceIdentifier(source: CaptureSource): string {
|
|
152
|
+
return source.kind === 'url' ? source.url : source.htmlPath
|
|
153
|
+
}
|
|
154
|
+
|
|
138
155
|
function failed(source: CaptureSource, reason: string): CaptureResult {
|
|
139
|
-
return { status: 'failed',
|
|
156
|
+
return { status: 'failed', source: sourceIdentifier(source), reason }
|
|
140
157
|
}
|
|
141
158
|
|
|
142
159
|
/**
|
package/src/capture/sources.ts
CHANGED
|
@@ -1,20 +1,45 @@
|
|
|
1
1
|
import { readdirSync, statSync } from 'node:fs'
|
|
2
2
|
import { basename, dirname, extname, join } from 'node:path'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const URL_SOURCE = /^https?:\/\//
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The one test for whether a source string names a URL rather than a
|
|
8
|
+
* filesystem path, shared with `src/commands/capture.ts` so the two never
|
|
9
|
+
* drift apart on what counts as a URL source.
|
|
10
|
+
*/
|
|
11
|
+
export function isUrlSource(source: string): boolean {
|
|
12
|
+
return URL_SOURCE.test(source)
|
|
7
13
|
}
|
|
8
14
|
|
|
15
|
+
export type CaptureSource =
|
|
16
|
+
| {
|
|
17
|
+
readonly kind: 'file'
|
|
18
|
+
readonly htmlPath: string
|
|
19
|
+
readonly pngPath: string
|
|
20
|
+
}
|
|
21
|
+
| { readonly kind: 'url'; readonly url: string; readonly pngPath: string }
|
|
22
|
+
|
|
9
23
|
/**
|
|
10
24
|
* Pairs each HTML source with the PNG it renders to. A directory expands to
|
|
11
25
|
* every `.html` directly inside it, so adding a capture is a file drop rather
|
|
12
26
|
* than a flag. Omitting `outDir` writes the PNG beside its source.
|
|
27
|
+
*
|
|
28
|
+
* A `http(s)://` source has no directory to walk and no basename on disk to
|
|
29
|
+
* name the PNG from, so `outDir` there is read as the destination file
|
|
30
|
+
* itself rather than a directory, and the caller is what requires it.
|
|
13
31
|
*/
|
|
14
32
|
export function resolveCaptureSources(
|
|
15
33
|
sourcePath: string,
|
|
16
34
|
outDir?: string,
|
|
17
35
|
): CaptureSource[] {
|
|
36
|
+
if (isUrlSource(sourcePath)) {
|
|
37
|
+
if (!outDir) {
|
|
38
|
+
throw new Error('a URL source needs --out naming the destination PNG')
|
|
39
|
+
}
|
|
40
|
+
return [{ kind: 'url', url: sourcePath, pngPath: outDir }]
|
|
41
|
+
}
|
|
42
|
+
|
|
18
43
|
const htmlPaths = statSync(sourcePath).isDirectory()
|
|
19
44
|
? readdirSync(sourcePath)
|
|
20
45
|
.filter((name) => extname(name) === '.html')
|
|
@@ -23,6 +48,7 @@ export function resolveCaptureSources(
|
|
|
23
48
|
: [sourcePath]
|
|
24
49
|
|
|
25
50
|
return htmlPaths.map((htmlPath) => ({
|
|
51
|
+
kind: 'file',
|
|
26
52
|
htmlPath,
|
|
27
53
|
pngPath: join(
|
|
28
54
|
outDir ?? dirname(htmlPath),
|
|
@@ -48,12 +48,12 @@ export const AUTHORING_CASES: readonly SkillCase[] = [
|
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
prompt:
|
|
51
|
-
'Write a context entry for the payments domain, there is no
|
|
51
|
+
'Write a context entry for the payments domain, there is no canon/context page for it yet.',
|
|
52
52
|
expect: 'draft-context',
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
prompt:
|
|
56
|
-
'Draft a wireframe for the settings panel, nothing under
|
|
56
|
+
'Draft a wireframe for the settings panel, nothing under canon/wireframes covers that surface yet.',
|
|
57
57
|
expect: 'draft-wireframes',
|
|
58
58
|
},
|
|
59
59
|
{
|
|
@@ -72,7 +72,7 @@ export const SETUP_CASES: readonly SkillCase[] = [
|
|
|
72
72
|
},
|
|
73
73
|
// The performing skill wins a phrase naming its operation over the front
|
|
74
74
|
// door's own routing framing, even though canon-operator's description
|
|
75
|
-
// quotes a phrase near this one. See
|
|
75
|
+
// quotes a phrase near this one. See canon/context/cli/audits.md.
|
|
76
76
|
{
|
|
77
77
|
prompt:
|
|
78
78
|
"Whatever the right toolkit command is, get this project's rules installed.",
|
package/src/claude/routing.ts
CHANGED
|
@@ -82,7 +82,7 @@ export function namedPaths(line: string): string[] {
|
|
|
82
82
|
* The openable part of a token, which for a shape is the folder above the
|
|
83
83
|
* placeholder.
|
|
84
84
|
*
|
|
85
|
-
*
|
|
85
|
+
* `canon/context/<domain>.md` names `canon/context/` and nothing narrower,
|
|
86
86
|
* so dropping the whole token would report the section that carries it as
|
|
87
87
|
* naming no path at all. A token whose placeholder sits in the first segment
|
|
88
88
|
* has no openable prefix and is dropped.
|
package/src/claude/seeds-list.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface SeedListingWithContent {
|
|
|
19
19
|
/**
|
|
20
20
|
* Reads the same plan `canon claude init` applies. The bash re-globbed the seeds
|
|
21
21
|
* directory with its own hard-coded subdirectory list, which had drifted: it
|
|
22
|
-
* never listed
|
|
22
|
+
* never listed `canon/context/`, so a seed init installs went unreported.
|
|
23
23
|
*
|
|
24
24
|
* The target is irrelevant to a listing, so it resolves against the source
|
|
25
25
|
* root and only the source-side fields are read.
|
package/src/claude/seeds.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
-
import { dirname, join } from 'node:path'
|
|
3
|
+
import { dirname, join, relative } from 'node:path'
|
|
4
4
|
import { copyPreservingMode } from '@/copy'
|
|
5
5
|
import { creationRel, isRecordEntry } from '@/record-root'
|
|
6
6
|
import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
|
|
7
|
+
import { SURFACE_ENTRIES, surfaceDir } from '@/surface-root'
|
|
7
8
|
|
|
8
9
|
const SEEDS_DIR = join('tooling', 'claude', 'seeds')
|
|
9
10
|
const CLAUDE_DIR = '.claude'
|
|
11
|
+
const SURFACE_DIR = 'canon'
|
|
10
12
|
const CLAUDE_MD = 'CLAUDE.md'
|
|
11
13
|
const HOOKS = 'hooks'
|
|
12
14
|
/**
|
|
13
|
-
* Seed subdirectories under `.claude
|
|
14
|
-
*
|
|
15
|
-
* `@/sync/layout` pairs a target against
|
|
15
|
+
* Seed subdirectories, authored under `.claude/` or, for a tracked surface,
|
|
16
|
+
* under `canon/`. Exported because each one replaced a single file of the same
|
|
17
|
+
* stem in an older layout, which is what `@/sync/layout` pairs a target against
|
|
18
|
+
* to find a superseded artifact.
|
|
16
19
|
*/
|
|
17
20
|
export const SUBDIRS: readonly string[] = [
|
|
18
21
|
HOOKS,
|
|
@@ -48,10 +51,26 @@ function seedsRoot(root: string): string {
|
|
|
48
51
|
return join(root, SEEDS_DIR)
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
function isSurfaceEntry(name: string): boolean {
|
|
55
|
+
return SURFACE_ENTRIES.includes(name)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Where a surface seed installs, relative to the target.
|
|
60
|
+
*
|
|
61
|
+
* Resolved the way a read resolves rather than fixed at `canon/`. A target that
|
|
62
|
+
* still holds the surface under `.claude/` receives the seed beside the copy it
|
|
63
|
+
* has, since a fresh `canon/` folder there would win read precedence and hide
|
|
64
|
+
* every entry that target already wrote.
|
|
65
|
+
*/
|
|
66
|
+
function surfaceRel(target: string, entry: string): string {
|
|
67
|
+
return relative(target, surfaceDir(target, entry))
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
/**
|
|
52
71
|
* Lists a single directory level, sorted the way `find -maxdepth 1 -type f |
|
|
53
|
-
* sort` was. Bun.Glob skips dotfiles without `dot`, and
|
|
54
|
-
* under
|
|
72
|
+
* sort` was. Bun.Glob skips dotfiles without `dot`, and the `.claude` source
|
|
73
|
+
* sits under a dotted name, so omitting it would match nothing there.
|
|
55
74
|
*/
|
|
56
75
|
function listLevel(dir: string): string[] {
|
|
57
76
|
if (!existsSync(dir)) return []
|
|
@@ -61,19 +80,19 @@ function listLevel(dir: string): string[] {
|
|
|
61
80
|
}
|
|
62
81
|
|
|
63
82
|
/**
|
|
64
|
-
* Builds the seed list in the order the
|
|
65
|
-
* level,
|
|
66
|
-
*
|
|
83
|
+
* Builds the seed list in the order the timeline prints it: the `.claude` root
|
|
84
|
+
* level, the `canon` root level, then each subdirectory from whichever source
|
|
85
|
+
* authors it, then the project-level `CLAUDE.md`.
|
|
67
86
|
*/
|
|
68
87
|
export function planSeeds(root: string, target: string): SeedEntry[] {
|
|
69
|
-
const
|
|
70
|
-
const
|
|
88
|
+
const claudeSource = join(seedsRoot(root), CLAUDE_DIR)
|
|
89
|
+
const surfaceSource = join(seedsRoot(root), SURFACE_DIR)
|
|
71
90
|
const seeds: Seed[] = []
|
|
72
91
|
|
|
73
|
-
for (const name of listLevel(
|
|
92
|
+
for (const name of listLevel(claudeSource)) {
|
|
74
93
|
seeds.push({
|
|
75
|
-
src: join(
|
|
76
|
-
dest: join(
|
|
94
|
+
src: join(claudeSource, name),
|
|
95
|
+
dest: join(target, CLAUDE_DIR, name),
|
|
77
96
|
scanLabel: name,
|
|
78
97
|
applyLabel: join(CLAUDE_DIR, name),
|
|
79
98
|
scope: 'claude',
|
|
@@ -81,23 +100,38 @@ export function planSeeds(root: string, target: string): SeedEntry[] {
|
|
|
81
100
|
})
|
|
82
101
|
}
|
|
83
102
|
|
|
103
|
+
for (const name of listLevel(surfaceSource)) {
|
|
104
|
+
const rel = surfaceRel(target, name)
|
|
105
|
+
seeds.push({
|
|
106
|
+
src: join(surfaceSource, name),
|
|
107
|
+
dest: join(target, rel),
|
|
108
|
+
scanLabel: name,
|
|
109
|
+
applyLabel: rel,
|
|
110
|
+
scope: 'claude',
|
|
111
|
+
executable: false,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
84
115
|
for (const subdir of SUBDIRS) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
// `.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
const surface = isSurfaceEntry(subdir)
|
|
117
|
+
const source = join(surface ? surfaceSource : claudeSource, subdir)
|
|
118
|
+
|
|
119
|
+
// Three of the `.claude/` subdirectories are record folders that install
|
|
120
|
+
// under the record root instead. A target that has not migrated resolves
|
|
121
|
+
// back to `.claude/`, so the same seed lands beside the records already
|
|
122
|
+
// there rather than opening a second root, and never outside the single
|
|
123
|
+
// `.canon/` ignore entry a target receives.
|
|
124
|
+
const installRel = surface
|
|
125
|
+
? surfaceRel(target, subdir)
|
|
126
|
+
: isRecordEntry(subdir)
|
|
127
|
+
? creationRel(target, subdir)
|
|
128
|
+
: join(CLAUDE_DIR, subdir)
|
|
129
|
+
|
|
130
|
+
for (const name of listLevel(source)) {
|
|
97
131
|
seeds.push({
|
|
98
|
-
src: join(source,
|
|
132
|
+
src: join(source, name),
|
|
99
133
|
dest: join(target, installRel, name),
|
|
100
|
-
scanLabel:
|
|
134
|
+
scanLabel: `${subdir}/${name}`,
|
|
101
135
|
applyLabel: join(installRel, name),
|
|
102
136
|
scope: 'claude',
|
|
103
137
|
executable: subdir === HOOKS,
|
|
@@ -17,7 +17,7 @@ import { SURFACE_ROOTS } from '@/surface-root'
|
|
|
17
17
|
* `docs/agents/` is the exception, being the CLI contract pages that exist
|
|
18
18
|
* here alone.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
20
|
+
* The context folder carries both surface-root spellings, since this list
|
|
21
21
|
* decides whether a shipped body names this repository's own tree and a body
|
|
22
22
|
* naming either root is doing that regardless of which root a given checkout
|
|
23
23
|
* carries.
|
|
@@ -51,7 +51,7 @@ const TOKEN = /`([^`\s]+)`/g
|
|
|
51
51
|
* A path a reader could open, which is the only kind worth measuring.
|
|
52
52
|
*
|
|
53
53
|
* Requires an extension and a separator, and admits no `<`, `$`, or `*`. A
|
|
54
|
-
* body writes
|
|
54
|
+
* body writes `canon/context/<domain>.md` to name a shape rather than a
|
|
55
55
|
* file, and `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` to resolve
|
|
56
56
|
* against the plugin root, which is self-contained by construction.
|
|
57
57
|
*/
|
|
@@ -88,7 +88,7 @@ export function isQualified(line: string): boolean {
|
|
|
88
88
|
* The roots that belong to the toolkit rather than to the reader, read
|
|
89
89
|
* against the corpus being measured.
|
|
90
90
|
*
|
|
91
|
-
* A target's
|
|
91
|
+
* A target's `canon/context/` is the reader's own tree. A seed put the
|
|
92
92
|
* entries there and the project owns them afterward, so a body under
|
|
93
93
|
* `.claude/skills/` citing one names a file its reader holds. Measuring it
|
|
94
94
|
* would report a correct citation on every run, which is exactly why `src/`
|
package/src/commands/capture.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
isBrowserMissing,
|
|
7
7
|
isEngineMissing,
|
|
8
8
|
} from '@/browser/engine'
|
|
9
|
+
import { isUrlSource } from '@/capture/sources'
|
|
9
10
|
import {
|
|
10
11
|
frameError,
|
|
11
12
|
intro,
|
|
@@ -42,8 +43,15 @@ export function register(program: Command): void {
|
|
|
42
43
|
program
|
|
43
44
|
.command('capture')
|
|
44
45
|
.description('Render HTML capture sources to PNG')
|
|
45
|
-
.argument(
|
|
46
|
-
|
|
46
|
+
.argument(
|
|
47
|
+
'[source]',
|
|
48
|
+
'HTML file, a directory of them, or an http(s):// URL',
|
|
49
|
+
DEFAULT_SOURCE,
|
|
50
|
+
)
|
|
51
|
+
.option(
|
|
52
|
+
'-o, --out <path>',
|
|
53
|
+
'Output directory for a file source, or the destination PNG for a URL source; defaults beside the source',
|
|
54
|
+
)
|
|
47
55
|
.option('-s, --selector <selector>', 'Element to capture')
|
|
48
56
|
.action(
|
|
49
57
|
async (
|
|
@@ -66,8 +74,9 @@ export function register(program: Command): void {
|
|
|
66
74
|
}
|
|
67
75
|
const selector = opts.selector
|
|
68
76
|
|
|
69
|
-
const
|
|
70
|
-
|
|
77
|
+
const isUrl = isUrlSource(source)
|
|
78
|
+
const sourcePath = isUrl ? source : resolve(process.cwd(), source)
|
|
79
|
+
if (!isUrl && !existsSync(sourcePath)) {
|
|
71
80
|
frameError(`${source} not found`)
|
|
72
81
|
process.exitCode = 1
|
|
73
82
|
return
|
|
@@ -100,7 +109,7 @@ export function register(program: Command): void {
|
|
|
100
109
|
`${displayPath(result.pngPath)} ${result.width}x${result.height}`,
|
|
101
110
|
)
|
|
102
111
|
} else {
|
|
103
|
-
logError(`${displayPath(result.
|
|
112
|
+
logError(`${displayPath(result.source)}: ${result.reason}`)
|
|
104
113
|
}
|
|
105
114
|
}
|
|
106
115
|
outro()
|
|
@@ -141,9 +150,11 @@ function reportInFrame(error: unknown): void {
|
|
|
141
150
|
/**
|
|
142
151
|
* Keeps a path clickable in the operator's terminal. A source outside the
|
|
143
152
|
* project reports absolute, since a relative path to it is a run of `..`
|
|
144
|
-
* segments no editor resolves.
|
|
153
|
+
* segments no editor resolves. A URL is not a filesystem path at all, so it
|
|
154
|
+
* reports as given.
|
|
145
155
|
*/
|
|
146
156
|
function displayPath(path: string): string {
|
|
157
|
+
if (isUrlSource(path)) return path
|
|
147
158
|
const fromCwd = relative(process.cwd(), path)
|
|
148
159
|
return fromCwd.startsWith('..') ? path : fromCwd
|
|
149
160
|
}
|
package/src/commands/claude.ts
CHANGED
|
@@ -435,7 +435,7 @@ export function register(program: Command): void {
|
|
|
435
435
|
' carrying that corpus alone. A cited path counts when it sits under',
|
|
436
436
|
' an authoring root no install channel delivers and the project',
|
|
437
437
|
' holds it. A path under src/, scripts/, or bare docs/ names the',
|
|
438
|
-
" reader's own tree and is not measured, and
|
|
438
|
+
" reader's own tree and is not measured, and canon/context/ joins",
|
|
439
439
|
" them when the corpus read is a project's own.",
|
|
440
440
|
'',
|
|
441
441
|
'Exit codes:',
|