@erclx/canon 5.74.0 → 5.76.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/CHANGELOG.md +29 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/create-rule/SKILL.md +5 -1
- package/claude/skills/create-rule/references/ledger.md +73 -0
- package/claude/skills/create-rule/references/rule.md +1 -1
- package/claude/skills/create-skill/references/skill-practice.md +1 -1
- package/claude/skills/design-identity/REQUIREMENT.md +1 -1
- package/claude/skills/design-identity/SKILL.md +13 -4
- package/claude/skills/design-identity/references/card-route.md +11 -4
- package/claude/skills/git-issue/REQUIREMENT.md +4 -2
- package/claude/skills/git-issue/SKILL.md +32 -3
- package/claude/skills/git-issue/references/issue.md +7 -0
- package/claude/skills/issue-triage/REQUIREMENT.md +35 -0
- package/claude/skills/issue-triage/SKILL.md +130 -0
- package/claude/skills/pr-evidence/references/reel.md +9 -9
- package/claude/skills/proposal-critique/references/adopted.md +44 -0
- package/claude/skills/role-orchestrator/SKILL.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +15 -15
- package/claude/skills/role-orchestrator/scripts/poll.ts +211 -96
- package/claude/skills/role-orchestrator/scripts/repo.ts +53 -1
- package/claude/skills/ui-film/SKILL.md +8 -7
- package/docs/agents/audits.md +24 -24
- package/docs/agents/migrate.md +10 -10
- package/docs/migrations/by-verb.md +10 -4
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/skill-map.md +1 -0
- package/governance/detect.toml +4 -0
- package/governance/ledger/claude/rule.md +16 -0
- package/governance/rules/claude/rule.md +13 -0
- package/package.json +1 -1
- package/scripts/ci/refresh-frames-pr.sh +11 -12
- package/src/audits/catalog.ts +7 -2
- package/src/capture/render.ts +30 -5
- package/src/capture/stamp.ts +3 -5
- package/src/claude/cases/git.ts +5 -0
- package/src/claude/cases/workflow.ts +4 -0
- package/src/claude/skills-audit.ts +65 -0
- package/src/cli/help.ts +1 -1
- package/src/commands/capture.ts +38 -1
- package/src/commands/claude.ts +10 -2
- package/src/commands/gov.ts +12 -7
- package/src/commands/migrate.ts +6 -2
- package/src/commands/take.ts +36 -7
- package/src/counts/catalogs.ts +3 -5
- package/src/gate/measures.ts +1 -144
- package/src/gate/stages.ts +0 -23
- package/src/gov/audit.ts +59 -13
- package/src/markdown/scope.ts +4 -0
- package/src/migrate/session-defaults.ts +146 -8
- package/src/take/encode.ts +9 -1
- package/src/take/film.ts +17 -2
- package/src/take/fixtures/busy.html +107 -0
- package/src/take/perform.ts +99 -25
- package/src/take/screencast.ts +4 -3
- package/src/take/stepper.ts +33 -20
- package/tooling/claude/reference.md +4 -2
- package/tooling/claude/seeds/.claude/hooks/plan-guard.sh +239 -0
- package/tooling/claude/seeds/.claude/settings.json +12 -0
- package/scripts/core/regen-hero.ts +0 -113
- package/src/capture/frame-values.ts +0 -326
package/docs/agents/audits.md
CHANGED
|
@@ -63,30 +63,30 @@ An audit that finds no corpus reports `absent`, which moves neither the verdict
|
|
|
63
63
|
|
|
64
64
|
`canon audits list` reports the first 18 rows. The rows marked `outside` run by hand or from a skill and sit beside the aggregate.
|
|
65
65
|
|
|
66
|
-
| Id | Verb | Measures
|
|
67
|
-
| -------------------- | ----------------------------------- |
|
|
68
|
-
| `context` | `canon context audit` | Context folder structure, citations, and the requirements record's word cap
|
|
69
|
-
| `markdown` | `canon markdown audit` | Bans, dead links, density, and reference shape
|
|
70
|
-
| `skills` | `canon claude skills audit` | Both skill corpora against the skill standards
|
|
71
|
-
| `skills-reach` | `canon claude skills reach` | Skill bodies citing a path no target receives
|
|
72
|
-
| `skills-rank` | `canon claude skills rank` | Whether skill descriptions separate against a routing case corpus
|
|
73
|
-
| `tasks` | `canon tasks validate` | Board rows nothing resolves
|
|
74
|
-
| `records-plans` | `canon records validate plans` | Plan records against their shape
|
|
75
|
-
| `records-groundwork` | `canon records validate groundwork` | Groundwork folders against their shape
|
|
76
|
-
| `records-intake` | `canon records validate intake` | Intake folders against their shape
|
|
77
|
-
| `comments` | `canon comments scan` | Comment density by language and kind, and `TODO`, `FIXME`, `HACK`, `XXX`
|
|
78
|
-
| `test-order` | `canon gov test-order` | Implementations that reached branch history before their test
|
|
79
|
-
| `secrets` | `canon secrets scan` | Credential-shaped values in the shipped tree
|
|
80
|
-
| `labels` | `canon labels audit` | Changed paths no row of the label map reaches
|
|
81
|
-
| `restated` | `canon gov restated` | Instructions `CLAUDE.md` states that a second surface states too
|
|
82
|
-
| `rules` | `canon gov audit` | Rules past a shape limit
|
|
83
|
-
| `counts` | `canon gov counts` | Sentences stating a closed catalog's size that disagree with the tree
|
|
84
|
-
| `census` | `canon census` | File count, extension breakdown, and line total
|
|
85
|
-
| `deps` | `canon deps audit` | Advisories against the resolved dependencies
|
|
86
|
-
| `outside` | `canon gov citations` | Rule citations and internal frontmatter globs reaching nothing
|
|
87
|
-
| `outside` | `canon gov superseded <old> <new>` | Where the tree still asserts a value a changed convention no longer produces
|
|
88
|
-
| `outside` | `canon claude skills drift <ref>` | Shipped skill bodies rewritten since a ref
|
|
89
|
-
| `outside` | `canon claude routing` | Per `CLAUDE.md` section, how many bullets name a path and whether a rule covers it
|
|
66
|
+
| Id | Verb | Measures | Finding |
|
|
67
|
+
| -------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------- | -------------- |
|
|
68
|
+
| `context` | `canon context audit` | Context folder structure, citations, and the requirements record's word cap | Gates |
|
|
69
|
+
| `markdown` | `canon markdown audit` | Bans, dead links, density, and reference shape | Gates |
|
|
70
|
+
| `skills` | `canon claude skills audit` | Both skill corpora against the skill standards, and any ledger under `canon/ledger/skills/` | Gates |
|
|
71
|
+
| `skills-reach` | `canon claude skills reach` | Skill bodies citing a path no target receives | Reports |
|
|
72
|
+
| `skills-rank` | `canon claude skills rank` | Whether skill descriptions separate against a routing case corpus | Reports |
|
|
73
|
+
| `tasks` | `canon tasks validate` | Board rows nothing resolves | Reports |
|
|
74
|
+
| `records-plans` | `canon records validate plans` | Plan records against their shape | Reports |
|
|
75
|
+
| `records-groundwork` | `canon records validate groundwork` | Groundwork folders against their shape | Reports |
|
|
76
|
+
| `records-intake` | `canon records validate intake` | Intake folders against their shape | Reports |
|
|
77
|
+
| `comments` | `canon comments scan` | Comment density by language and kind, and `TODO`, `FIXME`, `HACK`, `XXX` | Reports |
|
|
78
|
+
| `test-order` | `canon gov test-order` | Implementations that reached branch history before their test | Reports |
|
|
79
|
+
| `secrets` | `canon secrets scan` | Credential-shaped values in the shipped tree | Gates |
|
|
80
|
+
| `labels` | `canon labels audit` | Changed paths no row of the label map reaches | Reports |
|
|
81
|
+
| `restated` | `canon gov restated` | Instructions `CLAUDE.md` states that a second surface states too | Reports |
|
|
82
|
+
| `rules` | `canon gov audit` | Rules past a shape limit or out of step with a ledger, and here rules short of one | Reports |
|
|
83
|
+
| `counts` | `canon gov counts` | Sentences stating a closed catalog's size that disagree with the tree | Reports |
|
|
84
|
+
| `census` | `canon census` | File count, extension breakdown, and line total | Reports |
|
|
85
|
+
| `deps` | `canon deps audit` | Advisories against the resolved dependencies | Reports |
|
|
86
|
+
| `outside` | `canon gov citations` | Rule citations and internal frontmatter globs reaching nothing | Gates in check |
|
|
87
|
+
| `outside` | `canon gov superseded <old> <new>` | Where the tree still asserts a value a changed convention no longer produces | Reports |
|
|
88
|
+
| `outside` | `canon claude skills drift <ref>` | Shipped skill bodies rewritten since a ref | Reports |
|
|
89
|
+
| `outside` | `canon claude routing` | Per `CLAUDE.md` section, how many bullets name a path and whether a rule covers it | Reports |
|
|
90
90
|
|
|
91
91
|
A deliberate secret takes a `canon-allow-secret: <reason>` marker. A deliberate citation takes `canon-allow-citation: <reason>` and a deliberate superseded value takes `canon-allow-superseded: <reason>`, each on its own line or the one above. Run any verb with `--help` for its flags and refusal reasons.
|
|
92
92
|
|
package/docs/agents/migrate.md
CHANGED
|
@@ -21,7 +21,7 @@ kind: reference
|
|
|
21
21
|
| `record-layout` | Review receipts, memory archives, feedback, renders, picks, evidence, and the design board into their current folders |
|
|
22
22
|
| `session-defaults` | The seeded session defaults into an existing `.claude/settings.json`, listed below the table |
|
|
23
23
|
|
|
24
|
-
`record-tree` follows citations in `diagrams`, `memory`, `plans`, `proposals`, `review`, `tasks`, and `teach`. `session-defaults` denies
|
|
24
|
+
`record-tree` follows citations in `diagrams`, `memory`, `plans`, `proposals`, `review`, `tasks`, and `teach`. `session-defaults` denies subagents and both merge commands, allows the two lease push forms workers run, turns auto memory off, and wires an installed `plan-guard.sh`. The bullets below add detail per verb.
|
|
25
25
|
|
|
26
26
|
- `skill-names` takes no `--scope`, and skips the changelog, eval transcripts, replay fixtures, and the pointer each renamed `draft-` skill leaves at its old name
|
|
27
27
|
- `surface-roots` moves `context/`, `wireframes/`, `ARCHITECTURE.md`, `REQUIREMENTS.md`, `DESIGN.md`, and the stamp folder, and leaves `rules/`, `skills/`, `hooks/`, and `settings.json` in place
|
|
@@ -56,15 +56,15 @@ Three markers keep an old spelling on a line. Each also covers the nearest non-b
|
|
|
56
56
|
|
|
57
57
|
## Refusals
|
|
58
58
|
|
|
59
|
-
| Verb | Refuses when
|
|
60
|
-
| ------------------ |
|
|
61
|
-
| `records` | The project does not ignore `.canon/`. Run `canon tooling sync --write` first.
|
|
62
|
-
| `records` | A record folder already exists at the destination
|
|
63
|
-
| `surface-roots` | Any destination already exists under `canon/`, refusing the whole plan
|
|
64
|
-
| `scratch-evidence` | A destination is occupied
|
|
65
|
-
| `record-layout` | A destination already exists, each move refused on its own
|
|
66
|
-
| `rule-layout` | A destination holds different bytes. Neither file is touched and other moves apply.
|
|
67
|
-
| `session-defaults` | The settings file is not valid JSON, or `permissions` or
|
|
59
|
+
| Verb | Refuses when |
|
|
60
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
|
61
|
+
| `records` | The project does not ignore `.canon/`. Run `canon tooling sync --write` first. |
|
|
62
|
+
| `records` | A record folder already exists at the destination |
|
|
63
|
+
| `surface-roots` | Any destination already exists under `canon/`, refusing the whole plan |
|
|
64
|
+
| `scratch-evidence` | A destination is occupied |
|
|
65
|
+
| `record-layout` | A destination already exists, each move refused on its own |
|
|
66
|
+
| `rule-layout` | A destination holds different bytes. Neither file is touched and other moves apply. |
|
|
67
|
+
| `session-defaults` | The settings file is not valid JSON, or `permissions`, `deny`, `allow`, `hooks`, or `PreToolUse` has another shape |
|
|
68
68
|
|
|
69
69
|
A `rule-layout` destination holding the same bytes marks a duplicate, which `--write` deletes.
|
|
70
70
|
|
|
@@ -75,8 +75,8 @@ Replace a citation of a skill with no successor by hand, or delete the sentence.
|
|
|
75
75
|
|
|
76
76
|
- `canvas` is now `canvas-board`. The sweep rewrites only `canon:canvas` and `skills/canvas`, so replace a bare mention by hand. <!-- canon-keep-retired -->
|
|
77
77
|
- `draft-ready` is rewritten only as `canon:draft-ready` and `skills/draft-ready`, since bare it also describes a pull request, so replace a bare mention by hand. <!-- canon-keep-retired -->
|
|
78
|
-
- `design-extract` and `sketch-design` are replaced by `canon:canvas-board`. <!-- canon-keep-retired -->
|
|
79
|
-
- `review-branch`, `seed-sync`, `restate-plainly`, `bash-cli-script`, `canon-operator`,
|
|
78
|
+
- `design-extract` and `sketch-design` are replaced by `canon:canvas-board`, and `canon-feedback-triage` by `canon:issue-triage`. <!-- canon-keep-retired -->
|
|
79
|
+
- `review-branch`, `seed-sync`, `restate-plainly`, `bash-cli-script`, `canon-operator`, and `canon-rollout` have no successor <!-- canon-keep-retired -->
|
|
80
80
|
- `create-standard`, `markdown-propose`, `memory-review`, `project-commands`, `repo-metadata`, `ux-walkthrough`, and `plan-ready` have no successor <!-- canon-keep-retired -->
|
|
81
81
|
|
|
82
82
|
The `memory` record kind left `canon records`, so `canon records validate memory` refuses as an unknown kind and `canon records migrate` is gone.
|
|
@@ -108,11 +108,17 @@ It moves each surface with `git mv` and repoints every tracked citation. No othe
|
|
|
108
108
|
|
|
109
109
|
The seeded `.claude/settings.json` now denies the `Agent` and `Task` tools and both merge commands, and sets `autoMemoryEnabled` to `false`. `canon claude sync` never rewrites a settings file you hold, so it warns while the file holds no default or lacks a merge entry. Removing `Agent` and `Task` later draws no warning.
|
|
110
110
|
|
|
111
|
+
The plan guard takes two steps. `canon claude init` adds the absent `.claude/hooks/plan-guard.sh`, and the verb below then wires it into `hooks.PreToolUse` for `Bash` and `Write|Edit`. It skips a tool whose matcher already runs the guard, and wires nothing while the script is absent.
|
|
112
|
+
|
|
111
113
|
```bash
|
|
112
114
|
canon migrate session-defaults --json
|
|
113
115
|
canon migrate session-defaults --write --json
|
|
114
116
|
```
|
|
115
117
|
|
|
116
|
-
The verb only adds a key that is absent
|
|
118
|
+
The verb only adds a key or entry that is absent, so a project migrated before an entry runs `--write` once more.
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
- A person who wants auto memory back sets it in their own `.claude/settings.local.json`
|
|
121
|
+
- `Bash(canon pr merge:*)` and `Bash(gh pr merge:*)` stop a session merging
|
|
122
|
+
- A rerun restores `Agent` and `Task` for a person who removed them to allow subagents, so remove them again after
|
|
123
|
+
- `git push --force-with-lease` and its `origin HEAD` form are allowed exactly, so a worker pushes after a rebase without waiting
|
|
124
|
+
- Any other lease push still goes through auto mode's review, and `canon claude sync` warns while either entry is missing
|
|
@@ -41,7 +41,7 @@ See [target projects](../target/projects.md).
|
|
|
41
41
|
|
|
42
42
|
Plan and build in session 1, then review and ship in a fresh session 2 when you want a cold reviewer on the diff.
|
|
43
43
|
|
|
44
|
-
- For a pile of findings, invoke `canon:plan-intake`,
|
|
44
|
+
- For a pile of findings, invoke `canon:plan-intake`, for a grown backlog `canon:backlog-triage`, and for open GitHub issues `canon:issue-triage`
|
|
45
45
|
- For an unmeasured state with live approaches, invoke `canon:plan-groundwork`
|
|
46
46
|
- Invoke `canon:plan-feature`, implement, and run the `CLAUDE.md` commands until they pass
|
|
47
47
|
- For UI changes, invoke `canon:ui-checklist`
|
|
@@ -22,6 +22,7 @@ Groups run in the order a project meets them. Every name `canon claude skills li
|
|
|
22
22
|
| `canon:plan-intake` | The input is a pile of findings |
|
|
23
23
|
| `canon:plan-intake-answer` | An intake folder holds unread slots |
|
|
24
24
|
| `canon:backlog-triage` | The backlog needs pruning |
|
|
25
|
+
| `canon:issue-triage` | Open GitHub issues need measuring before planning |
|
|
25
26
|
| `canon:plan-groundwork` | The state is unmeasured and approaches compete |
|
|
26
27
|
| `canon:decision-escalate` | Open decisions turn on your preference |
|
|
27
28
|
| `canon:proposal-critique` | A proposal needs questioning before it is built |
|
package/governance/detect.toml
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Rule rule ledger
|
|
3
|
+
description: Decisions behind governance/rules/claude/rule.md, one row per bullet
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rule rule ledger
|
|
7
|
+
|
|
8
|
+
The rule is a core rule, so every row is a house choice rather than a citation. Each names what goes wrong without its bullet and the option the bullet beat, with the date it was decided.
|
|
9
|
+
|
|
10
|
+
## Rule files
|
|
11
|
+
|
|
12
|
+
| Bullet | Source | Read |
|
|
13
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
14
|
+
| Load create-rule before an edit | House choice: a session hand-editing rules without the skill shipped defects `canon gov audit` passed (issue 2842). It beat relying on the audit alone | 2026-10-29 |
|
|
15
|
+
| Report an unresolved skill | House choice: a project that installed governance without the plugin has no `create-rule`, and a silent skip hides that. It beat omitting the line | 2026-10-29 |
|
|
16
|
+
| Audit each touched rule | House choice: the audit catches the shape and ledger defects the skill's checklist names, so running it before the commit closes the loop. It beat leaving the audit to the pre-push gate alone | 2026-10-29 |
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Route edits to a rule file to the create-rule skill for its shape and checklist
|
|
3
|
+
paths:
|
|
4
|
+
- '.claude/rules/project/**/*.md'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rule standards
|
|
8
|
+
|
|
9
|
+
## Rule files
|
|
10
|
+
|
|
11
|
+
- Load the `canon:create-rule` skill before a hand edit to a rule, since it carries the frontmatter, the shape limits, and the bullet checklist.
|
|
12
|
+
- Report it rather than proceeding silently when `canon:create-rule` does not resolve, as in a project that installed governance without the plugin.
|
|
13
|
+
- Run `canon gov audit` over each rule the edit touched before committing it.
|
package/package.json
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Commits the
|
|
2
|
+
# Commits the recaptured home baseline onto the refresh branch and opens or
|
|
3
3
|
# updates its pull request, called by refresh-capture-frames.yml after the
|
|
4
|
-
# capture
|
|
4
|
+
# capture step. The step holds GH_TOKEN and HUSKY, since the token scope is
|
|
5
5
|
# read off the workflow.
|
|
6
6
|
#
|
|
7
7
|
# Hooks stay off, since pre-push runs a check whose format step needs shfmt,
|
|
8
8
|
# which the runner lacks. verify.yml checks the pull request.
|
|
9
9
|
#
|
|
10
|
-
# The
|
|
10
|
+
# The baseline commits where they rendered, then only the files that commit wrote
|
|
11
11
|
# or dropped carry onto main fetched now, so a merge landing mid-run neither
|
|
12
|
-
# leaves the branch on an older base nor loses a frame it moved. The pull
|
|
12
|
+
# leaves the branch on an older base nor loses a baseline frame it moved. The pull
|
|
13
13
|
# request resolves by number, since every merged refresh leaves a closed one on
|
|
14
14
|
# the same head.
|
|
15
15
|
set -e
|
|
16
16
|
|
|
17
17
|
branch=chore/refresh-capture-frames
|
|
18
|
-
title="chore(web): refresh
|
|
18
|
+
title="chore(web): refresh home baseline"
|
|
19
19
|
|
|
20
|
-
if [ -z "$(git status --porcelain assets/
|
|
21
|
-
echo "
|
|
20
|
+
if [ -z "$(git status --porcelain assets/evidence)" ]; then
|
|
21
|
+
echo "The home baseline is current, nothing to refresh."
|
|
22
22
|
exit 0
|
|
23
23
|
fi
|
|
24
24
|
git config user.name "github-actions[bot]"
|
|
25
25
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" # canon-allow-reference: the bot account id, not a commit
|
|
26
|
-
git add assets/
|
|
26
|
+
git add assets/evidence
|
|
27
27
|
git commit -m "$title"
|
|
28
28
|
rendered=$(git rev-parse HEAD)
|
|
29
29
|
written=$(git diff --name-only --no-renames --diff-filter=d HEAD~1 HEAD)
|
|
@@ -36,8 +36,8 @@ fi
|
|
|
36
36
|
if [ -n "$dropped" ]; then
|
|
37
37
|
printf '%s\n' "$dropped" | xargs -d '\n' git rm -q --ignore-unmatch --
|
|
38
38
|
fi
|
|
39
|
-
if git diff --cached --quiet -- assets/
|
|
40
|
-
echo "Main already carries
|
|
39
|
+
if git diff --cached --quiet -- assets/evidence; then
|
|
40
|
+
echo "Main already carries this baseline, nothing to refresh."
|
|
41
41
|
exit 0
|
|
42
42
|
fi
|
|
43
43
|
git commit -m "$title"
|
|
@@ -50,11 +50,10 @@ if [ -z "$number" ]; then
|
|
|
50
50
|
--title "$title" \
|
|
51
51
|
--body "## Summary
|
|
52
52
|
|
|
53
|
-
- Refreshes the
|
|
53
|
+
- Refreshes the home page baseline from main so canon pr evidence compares against a current render.
|
|
54
54
|
|
|
55
55
|
## Key changes
|
|
56
56
|
|
|
57
|
-
- Regenerates the count and token bearing frames, their images, and their stamps from main.
|
|
58
57
|
- Recaptures the home page baseline under assets/evidence/home.
|
|
59
58
|
- Merges itself by squash once its required check passes."
|
|
60
59
|
number=$(gh pr list --head "$branch" --state open --json number --jq '.[0].number')
|
package/src/audits/catalog.ts
CHANGED
|
@@ -379,7 +379,8 @@ function restatedCounts(record: unknown): Record<string, number> | undefined {
|
|
|
379
379
|
* a short one are fixed by different passes.
|
|
380
380
|
*/
|
|
381
381
|
function ruleCounts(record: unknown): Record<string, number> | undefined {
|
|
382
|
-
const
|
|
382
|
+
const root = asObject(record)
|
|
383
|
+
const rules = root?.rules
|
|
383
384
|
if (!Array.isArray(rules)) return undefined
|
|
384
385
|
|
|
385
386
|
let shapeRules = 0
|
|
@@ -397,7 +398,11 @@ function ruleCounts(record: unknown): Record<string, number> | undefined {
|
|
|
397
398
|
if ((lengthOf(ledger.mismatches) ?? 0) > 0) ledgerMismatches += 1
|
|
398
399
|
}
|
|
399
400
|
|
|
400
|
-
|
|
401
|
+
// A target ledger with no rule sits beside `rules` rather than in it. A
|
|
402
|
+
// binary predating it carries no `orphans` key, which reads as none.
|
|
403
|
+
const orphanLedgers = lengthOf(root?.orphans) ?? 0
|
|
404
|
+
|
|
405
|
+
return { shapeRules, missingLedgers, ledgerMismatches, orphanLedgers }
|
|
401
406
|
}
|
|
402
407
|
|
|
403
408
|
function findingsOnly(record: unknown): Record<string, number> | undefined {
|
package/src/capture/render.ts
CHANGED
|
@@ -34,10 +34,13 @@ const FONT_PROBE_SIZE = 72
|
|
|
34
34
|
const FONT_PROBE_TEXT = 'canon capture 0123456789'
|
|
35
35
|
const ABSENT_FAMILY = '__canon_absent_family__'
|
|
36
36
|
|
|
37
|
+
export type ColorScheme = 'light' | 'dark'
|
|
38
|
+
|
|
37
39
|
export interface CaptureOptions {
|
|
38
40
|
selector: string
|
|
39
41
|
outDir?: string
|
|
40
42
|
width?: number
|
|
43
|
+
colorScheme?: ColorScheme
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export type CaptureResult =
|
|
@@ -60,9 +63,7 @@ export async function captureSources(
|
|
|
60
63
|
const browser = await chromium.launch()
|
|
61
64
|
try {
|
|
62
65
|
return await Promise.all(
|
|
63
|
-
sources.map((source) =>
|
|
64
|
-
captureOne(browser, source, options.selector, options.width),
|
|
65
|
-
),
|
|
66
|
+
sources.map((source) => captureOne(browser, source, options)),
|
|
66
67
|
)
|
|
67
68
|
} finally {
|
|
68
69
|
await browser.close()
|
|
@@ -76,8 +77,7 @@ export async function captureSources(
|
|
|
76
77
|
async function captureOne(
|
|
77
78
|
browser: Browser,
|
|
78
79
|
source: CaptureSource,
|
|
79
|
-
selector:
|
|
80
|
-
width: number | undefined,
|
|
80
|
+
{ selector, width, colorScheme }: CaptureOptions,
|
|
81
81
|
): Promise<CaptureResult> {
|
|
82
82
|
/**
|
|
83
83
|
* A viewport rather than a wrapper, since a media query answers to the
|
|
@@ -90,11 +90,13 @@ async function captureOne(
|
|
|
90
90
|
...(width === undefined
|
|
91
91
|
? {}
|
|
92
92
|
: { viewport: { width, height: DEFAULT_VIEWPORT_HEIGHT } }),
|
|
93
|
+
...(colorScheme === undefined ? {} : { colorScheme }),
|
|
93
94
|
})
|
|
94
95
|
try {
|
|
95
96
|
await page.goto(
|
|
96
97
|
source.kind === 'url' ? source.url : pathToFileURL(source.htmlPath).href,
|
|
97
98
|
)
|
|
99
|
+
if (colorScheme !== undefined) await applyColorScheme(page, colorScheme)
|
|
98
100
|
const element = page.locator(selector).first()
|
|
99
101
|
if ((await element.count()) === 0) {
|
|
100
102
|
return failed(source, `no element matched ${selector}`)
|
|
@@ -177,6 +179,29 @@ function sourceIdentifier(source: CaptureSource): string {
|
|
|
177
179
|
return source.kind === 'url' ? source.url : source.htmlPath
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Writes the theme the three switches a stylesheet reads, being the media
|
|
184
|
+
* query, a `data-theme` attribute, and a `light` or `dark` class on the root,
|
|
185
|
+
* which is the set Frameboard's theme toggle writes on every frame. The media
|
|
186
|
+
* query alone leaves a stylesheet keyed on the attribute in its default set,
|
|
187
|
+
* which is dark for one `canon design css` emits.
|
|
188
|
+
*
|
|
189
|
+
* Written after the load event, so a page script restoring a stored theme on
|
|
190
|
+
* load has already run and cannot overwrite it.
|
|
191
|
+
*/
|
|
192
|
+
async function applyColorScheme(
|
|
193
|
+
page: Page,
|
|
194
|
+
scheme: ColorScheme,
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
await page.evaluate((theme) => {
|
|
197
|
+
const root = document.documentElement
|
|
198
|
+
root.setAttribute('data-theme', theme)
|
|
199
|
+
root.style.colorScheme = theme
|
|
200
|
+
root.classList.remove('light', 'dark')
|
|
201
|
+
root.classList.add(theme)
|
|
202
|
+
}, scheme)
|
|
203
|
+
}
|
|
204
|
+
|
|
180
205
|
function failed(source: CaptureSource, reason: string): CaptureResult {
|
|
181
206
|
return { status: 'failed', source: sourceIdentifier(source), reason }
|
|
182
207
|
}
|
package/src/capture/stamp.ts
CHANGED
|
@@ -6,13 +6,11 @@ const STAMP_EXTENSION = '.stamp'
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Provenance for one rendered PNG: the markup it came from and a digest over
|
|
9
|
-
* each side of the pair.
|
|
10
|
-
*
|
|
11
|
-
* colon, and the space after it are a contract between the writer and the gate
|
|
12
|
-
* rather than formatting.
|
|
9
|
+
* each side of the pair. Nothing reads the digests back today, so no check
|
|
10
|
+
* fails when either side moves.
|
|
13
11
|
*
|
|
14
12
|
* Both sides are recorded because either can move alone. The markup digest
|
|
15
|
-
*
|
|
13
|
+
* would show an edit committed with no capture, and the image digest a PNG
|
|
16
14
|
* replaced under markup that never changed.
|
|
17
15
|
*/
|
|
18
16
|
export interface CaptureStamp {
|
package/src/claude/cases/git.ts
CHANGED
|
@@ -22,6 +22,11 @@ export const GIT_CASES: readonly SkillCase[] = [
|
|
|
22
22
|
prompt: 'Turn this bug into a GitHub issue and file it.',
|
|
23
23
|
expect: 'git-issue',
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
prompt:
|
|
27
|
+
'Log this bug, but add it to the open issue if one already covers it.',
|
|
28
|
+
expect: 'git-issue',
|
|
29
|
+
},
|
|
25
30
|
{
|
|
26
31
|
prompt: 'Close #42 without merging, #57 supersedes it.',
|
|
27
32
|
expect: 'git-pr-close',
|
|
@@ -96,6 +96,10 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
96
96
|
'Our backlog only ever grows, measure each row and suggest which to decline.',
|
|
97
97
|
expect: 'backlog-triage',
|
|
98
98
|
},
|
|
99
|
+
{
|
|
100
|
+
prompt: 'File the open issues on this repo into an intake folder.',
|
|
101
|
+
expect: 'issue-triage',
|
|
102
|
+
},
|
|
99
103
|
// Rot across documents nobody changed is what this reads, rather than a
|
|
100
104
|
// stated rule broken on a given line. It is also not `target-check`, which
|
|
101
105
|
// asks whether a document exists and holds its sections rather than whether
|
|
@@ -82,6 +82,7 @@ export const LEDGER_SKILLS: readonly string[] = [
|
|
|
82
82
|
join(SHIPPED_CORPUS, 'write-human'),
|
|
83
83
|
join(SHIPPED_CORPUS, 'test-first'),
|
|
84
84
|
join(SHIPPED_CORPUS, 'ui-craft'),
|
|
85
|
+
join(SHIPPED_CORPUS, 'proposal-critique'),
|
|
85
86
|
]
|
|
86
87
|
|
|
87
88
|
function references(...names: string[]): string[] {
|
|
@@ -125,6 +126,7 @@ export const BULLET_LEDGER_SKILLS: Readonly<Record<string, BulletLedgerScope>> =
|
|
|
125
126
|
[join(SHIPPED_CORPUS, 'write-human')]: { exempt: [], body: true },
|
|
126
127
|
[join(SHIPPED_CORPUS, 'systematic-debugging')]: { exempt: [], body: true },
|
|
127
128
|
[join(SHIPPED_CORPUS, 'build-in-slices')]: { exempt: [], body: true },
|
|
129
|
+
[join(SHIPPED_CORPUS, 'proposal-critique')]: { exempt: [], body: true },
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
/** Where a per-bullet ledger lives, as `<skill>/<file>.md` below it. */
|
|
@@ -139,6 +141,15 @@ export function skillLedgerPath(folder: string, file: string): string {
|
|
|
139
141
|
return join(SKILL_LEDGERS, folder, inside.startsWith('..') ? file : inside)
|
|
140
142
|
}
|
|
141
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Where a target keeps the ledgers behind its own skills, as `<skill>/<file>.md`
|
|
146
|
+
* below it. A tree holding `governance/rules/` is the toolkit, which keeps its
|
|
147
|
+
* ledgers at `SKILL_LEDGERS`, so this folder is never read there.
|
|
148
|
+
*/
|
|
149
|
+
export const PROJECT_SKILL_LEDGERS = join('canon', 'ledger', 'skills')
|
|
150
|
+
|
|
151
|
+
const TOOLKIT_MARKER = join('governance', 'rules')
|
|
152
|
+
|
|
142
153
|
/** The closing H2s a practice skill carries, matched exactly outside fences. */
|
|
143
154
|
export const PRACTICE_SECTIONS: readonly string[] = [
|
|
144
155
|
'Excuses and rebuttals',
|
|
@@ -312,6 +323,9 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
|
|
|
312
323
|
present.map((corpus) => readCorpus(root, corpus.rel, corpus.dir)),
|
|
313
324
|
)
|
|
314
325
|
const sources = perCorpus.flat()
|
|
326
|
+
const projectLedgers = existsSync(join(root, TOOLKIT_MARKER))
|
|
327
|
+
? []
|
|
328
|
+
: await readProjectLedgers(root, sources)
|
|
315
329
|
|
|
316
330
|
return {
|
|
317
331
|
corpora: present.map((corpus, index) => ({
|
|
@@ -344,6 +358,7 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
|
|
|
344
358
|
sources,
|
|
345
359
|
),
|
|
346
360
|
...sources.flatMap(practiceFindings),
|
|
361
|
+
...projectLedgers,
|
|
347
362
|
],
|
|
348
363
|
referenceContents: sources.flatMap((source) =>
|
|
349
364
|
source.bodies.flatMap(contentsFindings),
|
|
@@ -483,6 +498,56 @@ async function readReferenceLedgers(
|
|
|
483
498
|
)
|
|
484
499
|
}
|
|
485
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Pairs each file a target wrote under `PROJECT_SKILL_LEDGERS` with the skill
|
|
503
|
+
* file of that name, `SKILL.md` with the body and any other with the reference
|
|
504
|
+
* at `references/<file>`. Nothing is owed, since a project has no list to owe a
|
|
505
|
+
* ledger against, so only a ledger that exists is read, and one naming a skill
|
|
506
|
+
* or a file that is gone is reported rather than skipped.
|
|
507
|
+
*/
|
|
508
|
+
async function readProjectLedgers(
|
|
509
|
+
root: string,
|
|
510
|
+
sources: readonly SkillSource[],
|
|
511
|
+
): Promise<SkillFinding[]> {
|
|
512
|
+
const dir = join(root, PROJECT_SKILL_LEDGERS)
|
|
513
|
+
if (!existsSync(dir)) return []
|
|
514
|
+
const paths = [
|
|
515
|
+
...new Bun.Glob('**/*.md').scanSync({ cwd: dir, onlyFiles: true }),
|
|
516
|
+
].sort()
|
|
517
|
+
|
|
518
|
+
const perLedger = await Promise.all(
|
|
519
|
+
paths.map(async (path): Promise<SkillFinding[]> => {
|
|
520
|
+
const ledgerRel = join(PROJECT_SKILL_LEDGERS, path)
|
|
521
|
+
const [folder, ...rest] = path.split('/')
|
|
522
|
+
const owners = sources.filter((source) => source.folder === folder)
|
|
523
|
+
if (rest.length === 0 || owners.length === 0) {
|
|
524
|
+
return [{ rel: ledgerRel, detail: `missing skill: ${folder}` }]
|
|
525
|
+
}
|
|
526
|
+
const file = rest.join('/')
|
|
527
|
+
const target = file === 'SKILL.md' ? file : join('references', file)
|
|
528
|
+
const rows = countLedgerRows(await Bun.file(join(dir, path)).text())
|
|
529
|
+
|
|
530
|
+
return owners.flatMap((owner) => {
|
|
531
|
+
const rel = join(owner.rel, target)
|
|
532
|
+
const body = owner.bodies.find((entry) => entry.rel === rel)
|
|
533
|
+
if (body === undefined) {
|
|
534
|
+
return [{ rel: ledgerRel, detail: `missing file: ${rel}` }]
|
|
535
|
+
}
|
|
536
|
+
return referenceLedgerFindings({
|
|
537
|
+
rel,
|
|
538
|
+
ledgerRel,
|
|
539
|
+
mismatches: compareLedger(
|
|
540
|
+
ledgerGroups(body.text, target === 'SKILL.md'),
|
|
541
|
+
rows,
|
|
542
|
+
),
|
|
543
|
+
sources: [],
|
|
544
|
+
})
|
|
545
|
+
})
|
|
546
|
+
}),
|
|
547
|
+
)
|
|
548
|
+
return perLedger.flat()
|
|
549
|
+
}
|
|
550
|
+
|
|
486
551
|
/**
|
|
487
552
|
* Directives per H2, `null` for any above the first. A contents list repeats
|
|
488
553
|
* the headings rather than stating a rule, so `## Contents` takes no rows. A
|
package/src/cli/help.ts
CHANGED
|
@@ -99,7 +99,7 @@ const EXAMPLES = [
|
|
|
99
99
|
'canon indexes regen --dry-run --json',
|
|
100
100
|
'canon docs list --json',
|
|
101
101
|
'canon docs agents',
|
|
102
|
-
'canon capture
|
|
102
|
+
'canon capture web/public/previews/design-tokens/index.html --selector body --out assets/evidence',
|
|
103
103
|
'canon inventory focus --json',
|
|
104
104
|
'canon drive http://localhost:4173 run.json --json',
|
|
105
105
|
'canon take http://localhost:4173 plan.json --out takes --json',
|
package/src/commands/capture.ts
CHANGED
|
@@ -31,6 +31,9 @@ const DEFAULT_SOURCE = 'assets'
|
|
|
31
31
|
*/
|
|
32
32
|
type Renderer = typeof import('@/capture/render')
|
|
33
33
|
type CaptureResult = Awaited<ReturnType<Renderer['captureSources']>>[number]
|
|
34
|
+
type ColorScheme = import('@/capture/render').ColorScheme
|
|
35
|
+
|
|
36
|
+
const COLOR_SCHEMES: readonly ColorScheme[] = ['light', 'dark']
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
39
|
* Holds wiring only. Every browser reference sits behind a dynamic import,
|
|
@@ -58,6 +61,10 @@ export function register(program: Command): void {
|
|
|
58
61
|
'--width <px>',
|
|
59
62
|
'Viewport width in pixels, so a media query resolves at that width',
|
|
60
63
|
)
|
|
64
|
+
.option(
|
|
65
|
+
'--color-scheme <scheme>',
|
|
66
|
+
'Theme to render in, light or dark; defaults to whichever theme the page renders unset',
|
|
67
|
+
)
|
|
61
68
|
.addHelpText(
|
|
62
69
|
'after',
|
|
63
70
|
[
|
|
@@ -70,12 +77,21 @@ export function register(program: Command): void {
|
|
|
70
77
|
'font-family. --width takes one whole number and sets the viewport, so',
|
|
71
78
|
'run it once per breakpoint with its own --out.',
|
|
72
79
|
'',
|
|
80
|
+
'--color-scheme sets prefers-color-scheme, a data-theme attribute, and',
|
|
81
|
+
'a light or dark class on the root, so a stylesheet keyed on any of the',
|
|
82
|
+
'three renders that theme. Run it once per scheme with its own --out.',
|
|
83
|
+
'',
|
|
73
84
|
].join('\n'),
|
|
74
85
|
)
|
|
75
86
|
.action(
|
|
76
87
|
async (
|
|
77
88
|
source: string,
|
|
78
|
-
opts: {
|
|
89
|
+
opts: {
|
|
90
|
+
out?: string
|
|
91
|
+
selector?: string
|
|
92
|
+
width?: string
|
|
93
|
+
colorScheme?: string
|
|
94
|
+
},
|
|
79
95
|
): Promise<void> => {
|
|
80
96
|
/**
|
|
81
97
|
* Refused rather than defaulted, and refused ahead of every other
|
|
@@ -102,6 +118,15 @@ export function register(program: Command): void {
|
|
|
102
118
|
return
|
|
103
119
|
}
|
|
104
120
|
|
|
121
|
+
const colorScheme = parseColorScheme(opts.colorScheme)
|
|
122
|
+
if (colorScheme === null) {
|
|
123
|
+
frameError(
|
|
124
|
+
`--color-scheme takes ${COLOR_SCHEMES.join(' or ')}, got ${JSON.stringify(opts.colorScheme)}.`,
|
|
125
|
+
)
|
|
126
|
+
process.exitCode = 1
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
105
130
|
const isUrl = isUrlSource(source)
|
|
106
131
|
const sourcePath = isUrl ? source : resolve(process.cwd(), source)
|
|
107
132
|
if (!isUrl && !existsSync(sourcePath)) {
|
|
@@ -117,6 +142,7 @@ export function register(program: Command): void {
|
|
|
117
142
|
results = await renderer.captureSources(sourcePath, {
|
|
118
143
|
selector,
|
|
119
144
|
width,
|
|
145
|
+
colorScheme,
|
|
120
146
|
outDir: opts.out ? resolve(process.cwd(), opts.out) : undefined,
|
|
121
147
|
})
|
|
122
148
|
} catch (error) {
|
|
@@ -160,6 +186,17 @@ function parseWidth(raw: string | undefined): number | undefined {
|
|
|
160
186
|
return Number.isInteger(value) && value >= 1 ? value : Number.NaN
|
|
161
187
|
}
|
|
162
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Undefined for an absent flag and `null` for a value naming no scheme, so the
|
|
191
|
+
* caller can tell no scheme from a bad one.
|
|
192
|
+
*/
|
|
193
|
+
function parseColorScheme(
|
|
194
|
+
raw: string | undefined,
|
|
195
|
+
): ColorScheme | undefined | null {
|
|
196
|
+
if (raw === undefined) return undefined
|
|
197
|
+
return COLOR_SCHEMES.find((scheme) => scheme === raw) ?? null
|
|
198
|
+
}
|
|
199
|
+
|
|
163
200
|
/**
|
|
164
201
|
* Closes an open frame around a failure that stopped the whole run rather than
|
|
165
202
|
* one source. Two of them are setup states rather than defects and each names
|