@erclx/canon 4.102.0 → 4.104.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-cli/SKILL.md +3 -74
- package/claude/skills/canon-operator/REQUIREMENT.md +2 -2
- package/claude/skills/canon-operator/SKILL.md +5 -5
- package/claude/skills/canon-rollout/SKILL.md +7 -6
- package/claude/skills/design-taste/references/craft.md +1 -1
- package/claude/skills/design-taste/references/preflight.md +2 -2
- package/claude/skills/design-taste/references/tells.md +1 -1
- package/claude/skills/draft-readme/REQUIREMENT.md +2 -0
- package/claude/skills/draft-readme/SKILL.md +9 -1
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-issue/SKILL.md +2 -2
- package/{standards → claude/skills/git-issue/references}/issue.md +3 -8
- package/claude/skills/git-pr/SKILL.md +2 -2
- package/claude/skills/index-lookup/REQUIREMENT.md +2 -2
- package/claude/skills/project-commands/REQUIREMENT.md +1 -1
- package/claude/skills/seed-sync/REQUIREMENT.md +1 -1
- package/claude/skills/target-check/REQUIREMENT.md +42 -0
- package/claude/skills/target-check/SKILL.md +91 -0
- package/claude/skills/target-check/references/domains.md +88 -0
- package/claude/skills/target-setup/REQUIREMENT.md +56 -0
- package/claude/skills/target-setup/SKILL.md +163 -0
- package/claude/skills/target-setup/references/detect.md +80 -0
- package/claude/skills/{setup-indexes/SKILL.md → target-setup/references/indexes.md} +59 -23
- package/claude/skills/target-setup/references/verify.md +95 -0
- package/claude/skills/ui-checklist/SKILL.md +17 -0
- package/docs/agents/commands.md +1 -1
- package/docs/agents/sandbox.md +1 -1
- package/docs/agents/scripting.md +3 -2
- package/docs/target-projects.md +13 -11
- package/docs/workflow/ai-workflow.md +19 -23
- package/governance/rules/ci/700-ci-workflow.md +11 -0
- package/governance/rules/ui/400-ui.md +0 -5
- package/governance/rules/ui/460-design-taste.md +1 -1
- package/governance/rules/ui/465-interface-casing.md +31 -0
- package/governance/stacks/astro.toml +1 -1
- package/governance/stacks/react.toml +1 -1
- package/package.json +1 -1
- package/scripts/core/regen-hero.sh +1 -1
- package/scripts/core/regen-web-previews.ts +1 -0
- package/src/claude/cases/setup.ts +20 -11
- package/src/commands/design.ts +8 -2
- package/src/commands/sync.ts +1 -1
- package/src/design/css.ts +5 -2
- package/src/design/render.ts +46 -6
- package/src/gate/measures.ts +57 -0
- package/src/gate/stages.ts +10 -14
- package/src/gov/list.ts +1 -1
- package/src/sandbox/census.ts +3 -3
- package/src/shipped/references.ts +1 -1
- package/standards/index.md +0 -1
- package/standards/readme.md +22 -3
- package/tooling/astro/manifest.toml +1 -0
- package/tooling/nextjs/manifest.toml +1 -0
- package/tooling/web/configs/.github/workflows/readme-screenshot.yml +110 -0
- package/tooling/web/configs/e2e/screenshot.ts +69 -15
- package/tooling/web/configs/scripts/lib/preview-server.sh +31 -0
- package/tooling/web/configs/scripts/readme-screenshot.sh +26 -0
- package/tooling/web/configs/scripts/screenshot.sh +1 -24
- package/tooling/web/manifest.toml +1 -0
- package/tooling/web/reference.md +6 -1
- package/claude/skills/setup-gov/REQUIREMENT.md +0 -37
- package/claude/skills/setup-gov/SKILL.md +0 -77
- package/claude/skills/setup-indexes/REQUIREMENT.md +0 -48
- package/claude/skills/setup-init/REQUIREMENT.md +0 -46
- package/claude/skills/setup-init/SKILL.md +0 -133
- package/claude/skills/setup-plugins/REQUIREMENT.md +0 -42
- package/claude/skills/setup-plugins/SKILL.md +0 -81
- package/claude/skills/setup-plugins/references/plugin-catalog.md +0 -53
- package/claude/skills/setup-smoke/REQUIREMENT.md +0 -38
- package/claude/skills/setup-smoke/SKILL.md +0 -53
- package/claude/skills/setup-verify/REQUIREMENT.md +0 -39
- package/claude/skills/setup-verify/SKILL.md +0 -51
- package/scripts/core/regen-tooling-paths.sh +0 -61
package/docs/target-projects.md
CHANGED
|
@@ -51,19 +51,19 @@ claude --plugin-dir <toolkit>/claude
|
|
|
51
51
|
Two steps, in order:
|
|
52
52
|
|
|
53
53
|
1. Run the framework's own scaffold if the project needs one, such as `bun init`, `npm create vite`, or `npm create astro`. The toolkit does not wrap framework scaffolding.
|
|
54
|
-
2. Invoke `canon:setup
|
|
54
|
+
2. Invoke `canon:target-setup` in Claude Code. The skill detects the stack, resolves flags, previews the full chain, and runs it end-to-end.
|
|
55
55
|
|
|
56
56
|
The chain is:
|
|
57
57
|
|
|
58
58
|
- `canon init` installs base tooling, Claude seeds, and governance rules into `.claude/rules/` in the same pass
|
|
59
59
|
- `canon tooling sync <stack> --write` adds stack-specific deps, scripts, and gitignore entries
|
|
60
60
|
- The agent reads `canon tooling reference <stack>` (plus parents) as its audit context, follows it to generate eslint, vitest, playwright configs and the stack's setup script, and extends `canon/context/ci.md` and `canon/context/development.md` per the reference's extend sections <!-- audit-ignore-citations: canon/context/development.md -->
|
|
61
|
-
- `
|
|
62
|
-
- `
|
|
61
|
+
- The `verify` phase runs the installed `package.json` scripts (lint, typecheck, check, test, build) and reports pass or fail
|
|
62
|
+
- The `indexes` phase bootstraps the `index.md` system over the project's own documentation folders, confirming candidate folders with the operator rather than running unattended
|
|
63
63
|
|
|
64
|
-
The chain stops at the project edge. `repo-metadata` and `git-commit` also ship, reaching a remote and the project's history respectively, and neither runs as part of it. The chain serves a fresh scaffold and names a destination for the three states it does not. An existing project goes to `canon:canon-operator`, which reads what the project already carries before naming a per-domain command. An install wanting the Claude layer without the tooling chain runs `canon claude init` for the seed docs and then `canon:setup
|
|
64
|
+
The chain stops at the project edge. `repo-metadata` and `git-commit` also ship, reaching a remote and the project's history respectively, and neither runs as part of it. The chain serves a fresh scaffold and names a destination for the three states it does not. An existing project goes to `canon:canon-operator`, which reads what the project already carries before naming a per-domain command. An install wanting the Claude layer without the tooling chain runs `canon claude init` for the seed docs and then `canon:target-setup indexes` for the index system. A language the toolkit ships no stack for is the one of the three the chain still runs for, on `base`, with the fallback marked in the preview so it can be declined there.
|
|
65
65
|
|
|
66
|
-
Run `canon:setup
|
|
66
|
+
Run `canon:target-setup verify deep` by hand once the default depth passes, for the heavier server smoke, end-to-end, and screenshot pass, since the same flakiness reasons that keep those stages out of the default depth keep them out of this unattended chain too.
|
|
67
67
|
|
|
68
68
|
Keep the `## Scripts` table in `canon/context/development.md` current as scripts are added. Base tooling seeds that entry with the commands it installs, and each stack reference extends the table. `project-commands` reads it to start the app or run a check on request, so a command missing from the table cannot be run that way. A project whose entry outgrew one file and split into `canon/context/development/` keeps the table in `overview.md`, which is where the skill looks next. <!-- audit-ignore-citations: canon/context/development.md -->
|
|
69
69
|
|
|
@@ -86,13 +86,13 @@ Each diagram entry records the commit and date it was last verified against, and
|
|
|
86
86
|
|
|
87
87
|
The default path is `base`. `canon init` on `base` installs base tooling configs, Claude seeds, and governance core rules, and scaffolds an empty `.claude/wiki/`. Most projects need nothing more.
|
|
88
88
|
|
|
89
|
-
Escalate only for real web apps. The `setup
|
|
89
|
+
Escalate only for real web apps. The `target-setup` skill reads `package.json` and root configs, then picks the matching tooling stack (`canon tooling list --json` names the current set) and the matching governance stack (`react`, `astro`, `node`).
|
|
90
90
|
|
|
91
91
|
`node-server` is named rather than detected. It carries the server-side security and persistence rules for a project writing request handlers or a persistence layer in TypeScript, and the detect step matches a runtime or a framework against stack names, so nothing there marks a project as a backend. Pass it deliberately with `canon init --stack node-server` or `canon gov install node-server <target>`.
|
|
92
92
|
|
|
93
93
|
Markdown-heavy projects, CLI tools, docs sites, research notebooks, and scripting repos stay on `base`. Escalation is a ceiling move, not a default.
|
|
94
94
|
|
|
95
|
-
A project the toolkit ships no stack for lands on `base` the same way, and the skill marks that resolution as a fallback in its preview rather than reporting it as a match. Configs, seeds, and gitignore entries land either way, and the JavaScript development dependencies, scripts, and hook activation land only where a `package.json` exists to carry them. A project outside that ecosystem declines at the preview and takes `canon:setup
|
|
95
|
+
A project the toolkit ships no stack for lands on `base` the same way, and the skill marks that resolution as a fallback in its preview rather than reporting it as a match. Configs, seeds, and gitignore entries land either way, and the JavaScript development dependencies, scripts, and hook activation land only where a `package.json` exists to carry them. A project outside that ecosystem declines at the preview and takes `canon:target-setup gov` for the governance layer alone, which is language-neutral.
|
|
96
96
|
|
|
97
97
|
Run `canon tooling list --json` and `canon gov list --json` to see the current catalogs. Never hardcode stack names.
|
|
98
98
|
|
|
@@ -115,9 +115,9 @@ The plugin corpus carries runtime behavior rather than reference prose alone, be
|
|
|
115
115
|
|
|
116
116
|
When a new need appears after scaffold, install the one domain without re-running `canon init`.
|
|
117
117
|
|
|
118
|
-
- Governance rule for a newly adopted library: invoke `canon:setup
|
|
118
|
+
- Governance rule for a newly adopted library: invoke `canon:target-setup gov`, or run `canon gov install <stack> --add <rule> <path>`
|
|
119
119
|
- Project-specific rule the toolkit does not ship: invoke `canon:create-rule`. It scaffolds a rule into `.claude/rules/` with a non-colliding number, and `canon gov sync` leaves it untouched.
|
|
120
|
-
- Index.md system for a markdown-heavy folder that emerged: invoke `canon:setup
|
|
120
|
+
- Index.md system for a markdown-heavy folder that emerged: invoke `canon:target-setup indexes`
|
|
121
121
|
|
|
122
122
|
Standards and snippets are not on that list, and there is nothing to add for either. Neither installs into a project, by default or by flag, so a session reads a standard with `canon standards <name>` and a snippet through its `@` reference off the live plugin symlink, both resolving against the toolkit rather than a project copy. A project holding a `.claude/standards/` or `.claude/snippets/` folder from an older toolkit is carrying a stale artifact nothing reads, and deleting it is safe.
|
|
123
123
|
|
|
@@ -215,6 +215,8 @@ Neither `canon sync --check` nor any other command notices a surface still sitti
|
|
|
215
215
|
|
|
216
216
|
`canon sync --check <path>` reports what has drifted without writing anything. It splits each difference by cause, which is the question that decides what to do next.
|
|
217
217
|
|
|
218
|
+
`canon:target-check` is the same reading grouped by domain rather than by cause, run from inside the project. It reports the toolkit version, the `canon/` folder, governance rules, tooling, and seeds from the verbs that own each comparison, and reports the Claude harness as unchecked because no verb reads it. It repairs nothing, so it is what to run before deciding rather than instead of deciding.
|
|
219
|
+
|
|
218
220
|
#### The binary first
|
|
219
221
|
|
|
220
222
|
The report opens by naming the binary running it. The installed version reads against the newest published one, and a version behind that points you at `canon upgrade`, since every section under it is a reading taken by whichever toolkit you happen to have. An unreachable registry reports unknown with its reason rather than failing, so `--exit-code` still gates on the drift the check measured locally and an offline machine stays green.
|
|
@@ -298,7 +300,7 @@ cd <your-project>
|
|
|
298
300
|
claude
|
|
299
301
|
```
|
|
300
302
|
|
|
301
|
-
In the session, invoke `canon:setup
|
|
303
|
+
In the session, invoke `canon:target-setup`. The skill detects no framework and resolves tooling to `base` and governance to `base`. The preview marks both stacks as fallbacks, since neither came from a match, then the chain runs `canon init`.
|
|
302
304
|
|
|
303
305
|
Ongoing: run `canon sync --check .` to see what has drifted, then invoke `canon:seed-sync` for seed drift or `canon sync .` for a catch-all refresh.
|
|
304
306
|
|
|
@@ -309,7 +311,7 @@ bun create vite my-app && cd my-app
|
|
|
309
311
|
claude
|
|
310
312
|
```
|
|
311
313
|
|
|
312
|
-
Invoke `canon:setup
|
|
314
|
+
Invoke `canon:target-setup`. The skill reads `package.json` and the Vite config, resolves tooling to `vite-react` and governance to `react`, and runs `canon init` with the resolved flags.
|
|
313
315
|
|
|
314
316
|
Ongoing maintenance:
|
|
315
317
|
|
|
@@ -200,19 +200,14 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
200
200
|
|
|
201
201
|
### Set up a project
|
|
202
202
|
|
|
203
|
-
| Skill | When to use
|
|
204
|
-
| ---------------------- |
|
|
205
|
-
| `canon:setup
|
|
206
|
-
| `canon:canon-operator` | On a project that already exists, to read what it carries before an install is picked
|
|
207
|
-
| `canon:
|
|
208
|
-
| `canon:
|
|
209
|
-
| `canon:
|
|
210
|
-
| `canon:
|
|
211
|
-
| `canon:setup-smoke` | After `setup-verify` passes, to check the dev and preview servers, end-to-end tests, and the screenshot harness |
|
|
212
|
-
| `canon:sketch-design` | Before `design-extract`'s greenfield path, to trace a design direction from reference images or URLs |
|
|
213
|
-
| `canon:design-extract` | Before the first UI feature, to draft `canon/DESIGN.md` |
|
|
214
|
-
| `canon:draft-diagram` | Once the architecture is written, to draft per-kind entries under `.canon/diagrams/` |
|
|
215
|
-
| `canon:repo-metadata` | When the GitHub About text, homepage, or topics may have drifted, to reconcile them against the README |
|
|
203
|
+
| Skill | When to use |
|
|
204
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
205
|
+
| `canon:target-setup` | On a fresh scaffold, to detect the stack and run the install chain, or to reach one phase of it alone |
|
|
206
|
+
| `canon:canon-operator` | On a project that already exists, to read what it carries before an install is picked |
|
|
207
|
+
| `canon:sketch-design` | Before `design-extract`'s greenfield path, to trace a design direction from reference images or URLs |
|
|
208
|
+
| `canon:design-extract` | Before the first UI feature, to draft `canon/DESIGN.md` |
|
|
209
|
+
| `canon:draft-diagram` | Once the architecture is written, to draft per-kind entries under `.canon/diagrams/` |
|
|
210
|
+
| `canon:repo-metadata` | When the GitHub About text, homepage, or topics may have drifted, to reconcile them against the README |
|
|
216
211
|
|
|
217
212
|
### Decide what to build
|
|
218
213
|
|
|
@@ -289,16 +284,17 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
289
284
|
|
|
290
285
|
### Keep the project current with the toolkit
|
|
291
286
|
|
|
292
|
-
| Skill | When to use
|
|
293
|
-
| -------------------------------- |
|
|
294
|
-
| `canon:
|
|
295
|
-
| `canon:
|
|
296
|
-
| `canon:migration-
|
|
297
|
-
| `canon:migration-
|
|
298
|
-
| `canon:migration-
|
|
299
|
-
| `canon:
|
|
300
|
-
| `canon:canon-feedback-
|
|
301
|
-
| `canon:canon-
|
|
287
|
+
| Skill | When to use |
|
|
288
|
+
| -------------------------------- | -------------------------------------------------------------------------------------- |
|
|
289
|
+
| `canon:target-check` | In a target project, to report per domain what it holds against what the toolkit ships |
|
|
290
|
+
| `canon:seed-sync` | After a toolkit update, to reconcile installed seeds without losing customizations |
|
|
291
|
+
| `canon:migration-claude-md` | When `CLAUDE.md` grew past what always-load context should carry |
|
|
292
|
+
| `canon:migration-context` | When `docs/` holds agent-flavored files belonging in `canon/context/` |
|
|
293
|
+
| `canon:migration-superseded` | When a drift report names a `.claude/` file a folder has replaced |
|
|
294
|
+
| `canon:migration-standards-drop` | When the project still holds an installed `.claude/standards/` tree |
|
|
295
|
+
| `canon:canon-feedback-file` | When something in the toolkit is broken, missing, or off |
|
|
296
|
+
| `canon:canon-feedback-triage` | In the toolkit repo, to work through the open feedback issues |
|
|
297
|
+
| `canon:canon-rollout` | In the toolkit repo, to take one change out to every consuming project at once |
|
|
302
298
|
|
|
303
299
|
### Generate an artifact on demand
|
|
304
300
|
|
|
@@ -39,6 +39,17 @@ paths:
|
|
|
39
39
|
- Install with `bun install --frozen-lockfile`.
|
|
40
40
|
- Key the Playwright browser cache on the Playwright version string, never a static key.
|
|
41
41
|
|
|
42
|
+
## Commit-back
|
|
43
|
+
|
|
44
|
+
- Commit a refreshed file back to the pull request branch only when the branch's own change moved it. Report the diff without committing when something outside the branch can move it, such as a count read at build time or a font that renders differently per machine, since that commit lands on a branch that never asked for it.
|
|
45
|
+
- Commit only what a deterministic step regenerates from source, and run the diff check before the commit step so an unchanged capture pushes nothing.
|
|
46
|
+
- Skip a pull request from a fork with `github.event.pull_request.head.repo.full_name == github.repository`, since the default token cannot push to a fork's branch.
|
|
47
|
+
- Set the concurrency group on the ref so a newer push cancels the run committing onto a stale head.
|
|
48
|
+
- Set `HUSKY: 0` on the commit step when the runner lacks a tool a hook shells out to.
|
|
49
|
+
- Never commit back to the trunk. A commit with no pull request and no reviewer needs an operator's decision, not a workflow's.
|
|
50
|
+
- Leave `workflow_dispatch` off a commit-back workflow. A manual run has no branch to commit onto, so the trigger would pass and do nothing.
|
|
51
|
+
- Fail a template's first step while a path list still holds its placeholder, so an unfinished install fails loudly rather than never firing.
|
|
52
|
+
|
|
42
53
|
## Authority
|
|
43
54
|
|
|
44
55
|
- Load the `canon:ci-workflow` skill for the workflow template and the per-project adaptation. Report it rather than proceeding silently when the skill does not resolve.
|
|
@@ -9,11 +9,6 @@ paths:
|
|
|
9
9
|
|
|
10
10
|
# UI copy standards
|
|
11
11
|
|
|
12
|
-
## Casing
|
|
13
|
-
|
|
14
|
-
- Use sentence case for all rendered UI text: headings, labels, nav items, tab titles, and dialog titles.
|
|
15
|
-
- Retain original casing for proper nouns and product names: `GitHub`, `macOS`, `TypeScript`.
|
|
16
|
-
|
|
17
12
|
## Punctuation
|
|
18
13
|
|
|
19
14
|
- Do not use em dashes (`—`). Use a comma or period, or restructure the sentence.
|
|
@@ -25,5 +25,5 @@ paths:
|
|
|
25
25
|
|
|
26
26
|
- Accessibility, keyboard interaction and ARIA are a separate topic. `410-a11y` routes them, on every rendered path.
|
|
27
27
|
- States, empty and loading coverage, and destructive confirmation are a separate topic. `430-ux-completeness` routes them.
|
|
28
|
-
- Rendered copy casing
|
|
28
|
+
- Rendered copy casing is a separate topic. `465-interface-casing` routes it. Button labels and error wording are separate too, and `400-ui` routes them.
|
|
29
29
|
- The floor those three carry is not restated in the skill, and it is not traded against a preference where the two collide.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Enforce casing for interface text in markup, MDX, and content modules, where copy lives in a content folder
|
|
3
|
+
paths:
|
|
4
|
+
- '**/*.tsx'
|
|
5
|
+
- '**/*.jsx'
|
|
6
|
+
- '**/*.astro'
|
|
7
|
+
- '**/*.html'
|
|
8
|
+
- '**/*.mdx'
|
|
9
|
+
- '**/content/**/*.ts'
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Interface casing standards
|
|
13
|
+
|
|
14
|
+
## Sentence case
|
|
15
|
+
|
|
16
|
+
- Use sentence case for all rendered interface text: headings, labels, nav items, tab titles, and dialog titles.
|
|
17
|
+
- Retain original casing for proper nouns and product names: `GitHub`, `macOS`, `TypeScript`.
|
|
18
|
+
- Treat title case on every heading as a template tell, since it reads as a default rather than a choice.
|
|
19
|
+
|
|
20
|
+
## Uppercase labels
|
|
21
|
+
|
|
22
|
+
- Keep small uppercase labels above headings to one per three sections, with the first screen counting as one. More than `ceil(sections / 3)` is too many, and the fix is usually to delete the label, since a section's position already says what it is.
|
|
23
|
+
- Treat the count as a judgment, since a glob cannot check it.
|
|
24
|
+
|
|
25
|
+
## Where it applies
|
|
26
|
+
|
|
27
|
+
- The rule covers markup, MDX, and content modules under a `content/` folder. Copy kept elsewhere, such as a locale file, is not reached, so apply the same casing there without the rule firing.
|
|
28
|
+
|
|
29
|
+
## Overriding
|
|
30
|
+
|
|
31
|
+
- Depart from sentence case where the case needs it, such as an acronym, a brand mark, or a set-in-caps identity the design chose on purpose.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
extends = "node"
|
|
2
|
-
rules = ["210-astro", "300-testing-ts", "305-e2e-reliability", "306-test-scope", "350-security-web", "400-ui", "410-a11y", "420-forms", "430-ux-completeness", "440-surface-capture", "450-link-behavior", "460-design-taste", "470-motion"]
|
|
2
|
+
rules = ["210-astro", "300-testing-ts", "305-e2e-reliability", "306-test-scope", "350-security-web", "400-ui", "410-a11y", "420-forms", "430-ux-completeness", "440-surface-capture", "450-link-behavior", "460-design-taste", "465-interface-casing","470-motion"]
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
extends = "node"
|
|
2
|
-
rules = ["200-react", "230-nextjs", "250-tailwind", "300-testing-ts", "305-e2e-reliability", "306-test-scope", "310-zod", "350-security-web", "400-ui", "410-a11y", "420-forms", "430-ux-completeness", "440-surface-capture", "450-link-behavior", "460-design-taste", "470-motion"]
|
|
2
|
+
rules = ["200-react", "230-nextjs", "250-tailwind", "300-testing-ts", "305-e2e-reliability", "306-test-scope", "310-zod", "350-security-web", "400-ui", "410-a11y", "420-forms", "430-ux-completeness", "440-surface-capture", "450-link-behavior", "460-design-taste", "465-interface-casing","470-motion"]
|
package/package.json
CHANGED
|
@@ -1,39 +1,40 @@
|
|
|
1
1
|
import type { SkillCase } from '@/claude/skills-rank'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* `setup
|
|
4
|
+
* `target-setup`, `migration-*`, `canon-*`, and `create-rule`: scaffolding,
|
|
5
5
|
* proposal-only migrations, and the toolkit's own reference and feedback
|
|
6
6
|
* surfaces.
|
|
7
|
+
*
|
|
8
|
+
* The five `target-setup` cases are one skill read through five entry phrasings
|
|
9
|
+
* rather than five rows that collapsed to a duplicate. Each names a different
|
|
10
|
+
* phase, and the merged description has to carry every one of them, which is
|
|
11
|
+
* the discrimination the six-skill family failed at from the other direction.
|
|
7
12
|
*/
|
|
8
13
|
export const SETUP_CASES: readonly SkillCase[] = [
|
|
9
14
|
{
|
|
10
15
|
prompt:
|
|
11
16
|
'This project has no rules installed yet, get the right governance in place.',
|
|
12
|
-
expect: 'setup
|
|
17
|
+
expect: 'target-setup',
|
|
13
18
|
},
|
|
14
19
|
{
|
|
15
20
|
prompt:
|
|
16
21
|
"Get the index.md system bootstrapped across this project's folders.",
|
|
17
|
-
expect: 'setup
|
|
22
|
+
expect: 'target-setup',
|
|
18
23
|
},
|
|
19
24
|
{
|
|
20
25
|
prompt:
|
|
21
26
|
'This is a brand-new project, get the toolkit bootstrapped in one shot.',
|
|
22
|
-
expect: 'setup
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
prompt: 'Get the usual Claude Code plugins provisioned on this machine.',
|
|
26
|
-
expect: 'setup-plugins',
|
|
27
|
+
expect: 'target-setup',
|
|
27
28
|
},
|
|
28
29
|
{
|
|
29
30
|
prompt:
|
|
30
31
|
"Run through the generated scaffold's scripts and confirm each one passes.",
|
|
31
|
-
expect: 'setup
|
|
32
|
+
expect: 'target-setup',
|
|
32
33
|
},
|
|
33
34
|
{
|
|
34
35
|
prompt:
|
|
35
36
|
'Check that the dev server actually starts and the end-to-end suite passes against the scaffold.',
|
|
36
|
-
expect: 'setup
|
|
37
|
+
expect: 'target-setup',
|
|
37
38
|
},
|
|
38
39
|
{
|
|
39
40
|
prompt:
|
|
@@ -76,7 +77,7 @@ export const SETUP_CASES: readonly SkillCase[] = [
|
|
|
76
77
|
{
|
|
77
78
|
prompt:
|
|
78
79
|
"Whatever the right toolkit command is, get this project's rules installed.",
|
|
79
|
-
expect: 'setup
|
|
80
|
+
expect: 'target-setup',
|
|
80
81
|
},
|
|
81
82
|
{
|
|
82
83
|
prompt:
|
|
@@ -91,6 +92,14 @@ export const SETUP_CASES: readonly SkillCase[] = [
|
|
|
91
92
|
'Take this change out to every project that installed the toolkit and get each one to a pull request.',
|
|
92
93
|
expect: 'canon-rollout',
|
|
93
94
|
},
|
|
95
|
+
// Reporting per domain is what separates this from canon-operator, which
|
|
96
|
+
// diagnoses on the way to running the operation an intent named. This answers
|
|
97
|
+
// one fixed question across a fixed domain list and repairs nothing.
|
|
98
|
+
{
|
|
99
|
+
prompt:
|
|
100
|
+
'Report what this project has fallen behind the toolkit on, domain by domain, and fix nothing.',
|
|
101
|
+
expect: 'target-check',
|
|
102
|
+
},
|
|
94
103
|
{
|
|
95
104
|
prompt:
|
|
96
105
|
"This project needs its own coding rule that the toolkit doesn't ship.",
|
package/src/commands/design.ts
CHANGED
|
@@ -119,7 +119,11 @@ export function register(program: Command): void {
|
|
|
119
119
|
'Output directory',
|
|
120
120
|
creationRel(process.cwd(), SCRATCH, 'render', 'design'),
|
|
121
121
|
)
|
|
122
|
-
.
|
|
122
|
+
.option(
|
|
123
|
+
'--embed-fonts',
|
|
124
|
+
'Declare each vendored face the typography table names, and set the page body in the body stack',
|
|
125
|
+
)
|
|
126
|
+
.action((opts: { source: string; out: string; embedFonts?: boolean }) => {
|
|
123
127
|
const sourcePath = resolve(process.cwd(), opts.source)
|
|
124
128
|
const outDir = resolve(process.cwd(), opts.out)
|
|
125
129
|
const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
|
|
@@ -133,7 +137,9 @@ export function register(program: Command): void {
|
|
|
133
137
|
process.stderr.write(
|
|
134
138
|
`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}Render design tokens${NC}\n`,
|
|
135
139
|
)
|
|
136
|
-
const result = renderDesignDoc(sourcePath, outDir
|
|
140
|
+
const result = renderDesignDoc(sourcePath, outDir, {
|
|
141
|
+
embedFonts: opts.embedFonts,
|
|
142
|
+
})
|
|
137
143
|
process.stderr.write(
|
|
138
144
|
`${GREY}│${NC} ${GREEN}✓${NC} ${result.htmlPath}\n${GREY}│${NC} ${GREEN}✓${NC} ${result.cssPath}\n${GREY}└${NC}\n`,
|
|
139
145
|
)
|
package/src/commands/sync.ts
CHANGED
|
@@ -109,7 +109,7 @@ function renderCheck(report: CheckReport): void {
|
|
|
109
109
|
logStep('Not a toolkit project')
|
|
110
110
|
logWarn('No .claude/ directory and no CLAUDE.md at the target.')
|
|
111
111
|
logInfo(
|
|
112
|
-
'Run `canon init` to install, or /canon:setup
|
|
112
|
+
'Run `canon init` to install, or /canon:target-setup to resolve a stack.',
|
|
113
113
|
)
|
|
114
114
|
outro()
|
|
115
115
|
return
|
package/src/design/css.ts
CHANGED
|
@@ -136,14 +136,17 @@ ${component.rules}`,
|
|
|
136
136
|
* installed. Defaults to the mono stack's primary family; teach passes its own
|
|
137
137
|
* three faces instead of widening this default for every consumer.
|
|
138
138
|
*/
|
|
139
|
-
function fontFaceBlock(
|
|
139
|
+
export function fontFaceBlock(
|
|
140
|
+
faces: readonly FontFace[],
|
|
141
|
+
display: 'swap' | 'block' = 'swap',
|
|
142
|
+
): string {
|
|
140
143
|
return faces
|
|
141
144
|
.map(
|
|
142
145
|
(face) => `@font-face {
|
|
143
146
|
font-family: '${face.family}';
|
|
144
147
|
font-weight: ${face.weight};
|
|
145
148
|
font-style: normal;
|
|
146
|
-
font-display:
|
|
149
|
+
font-display: ${display};
|
|
147
150
|
src: url(data:font/woff2;base64,${face.base64}) format('woff2');
|
|
148
151
|
}`,
|
|
149
152
|
)
|
package/src/design/render.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { mkdirSync, writeFileSync } from 'node:fs'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import type { Cell, DesignDoc, Row } from '@/design/parse'
|
|
4
4
|
import { parseDesignDoc } from '@/design/parse'
|
|
5
|
+
import { fontFaceBlock } from '@/design/css'
|
|
6
|
+
import type { FontFace } from '@/design/fonts'
|
|
7
|
+
import { FONT_FACES } from '@/design/fonts'
|
|
5
8
|
import { colorValue } from '@/design/tokens'
|
|
6
9
|
|
|
7
10
|
export interface RenderResult {
|
|
@@ -19,19 +22,55 @@ const FAVICON_HREF = `data:image/svg+xml,${encodeURIComponent(
|
|
|
19
22
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80"><path d="M34,20 L15,28 L15,72 L34,80 Z M66,20 L85,28 L85,72 L66,80 Z" fill="${FAVICON_COLOR}" /><rect x="44" y="15" width="12" height="70" rx="2" fill="${FAVICON_COLOR}" /></svg>`,
|
|
20
23
|
)}`
|
|
21
24
|
|
|
25
|
+
export interface RenderOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Declare each vendored face the typography table names, and let the page
|
|
28
|
+
* body take the body role's stack. Off by default, since a face is a large
|
|
29
|
+
* base64 block a target's render would otherwise grow by unasked.
|
|
30
|
+
*/
|
|
31
|
+
readonly embedFonts?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
export function renderDesignDoc(
|
|
23
35
|
sourcePath: string,
|
|
24
36
|
outDir: string,
|
|
37
|
+
options: RenderOptions = {},
|
|
25
38
|
): RenderResult {
|
|
26
39
|
const doc = parseDesignDoc(sourcePath)
|
|
27
40
|
mkdirSync(outDir, { recursive: true })
|
|
28
41
|
const cssPath = join(outDir, 'design.css')
|
|
29
42
|
const htmlPath = join(outDir, 'index.html')
|
|
30
|
-
|
|
31
|
-
writeFileSync(
|
|
43
|
+
const faces = options.embedFonts ? namedFaces(doc) : []
|
|
44
|
+
writeFileSync(cssPath, buildCss(doc, faces))
|
|
45
|
+
writeFileSync(htmlPath, buildHtml(doc, bodyFamily(doc, faces)))
|
|
32
46
|
return { htmlPath, cssPath }
|
|
33
47
|
}
|
|
34
48
|
|
|
49
|
+
/** The vendored faces whose family appears in a typography `Family` cell. */
|
|
50
|
+
function namedFaces(doc: DesignDoc): FontFace[] {
|
|
51
|
+
const named = new Set(
|
|
52
|
+
doc.typography.flatMap((row) =>
|
|
53
|
+
val(row, 'Family')
|
|
54
|
+
.split(',')
|
|
55
|
+
.map((family) => family.trim().replace(/^['"]|['"]$/g, '')),
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
return FONT_FACES.filter((face) => named.has(face.family))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A stack lands raw in a `<style>` block, where HTML escaping does nothing. */
|
|
62
|
+
const SAFE_STACK = /^[\w\s,'"-]+$/
|
|
63
|
+
|
|
64
|
+
/** The stack the body role names, once a face for it is embedded. */
|
|
65
|
+
function bodyFamily(doc: DesignDoc, faces: readonly FontFace[]): string {
|
|
66
|
+
if (!faces.length) return 'system-ui, sans-serif'
|
|
67
|
+
const row =
|
|
68
|
+
doc.typography.find((r) => slug(val(r, 'Role')) === 'body') ??
|
|
69
|
+
doc.typography[0]
|
|
70
|
+
const family = val(row, 'Family')
|
|
71
|
+
return SAFE_STACK.test(family) ? family : 'system-ui, sans-serif'
|
|
72
|
+
}
|
|
73
|
+
|
|
35
74
|
function slug(s: string): string {
|
|
36
75
|
return s
|
|
37
76
|
.toLowerCase()
|
|
@@ -105,7 +144,7 @@ function confidence(doc: DesignDoc): Confidence {
|
|
|
105
144
|
return { tagged, total }
|
|
106
145
|
}
|
|
107
146
|
|
|
108
|
-
function buildCss(doc: DesignDoc): string {
|
|
147
|
+
function buildCss(doc: DesignDoc, faces: readonly FontFace[]): string {
|
|
109
148
|
const lines: string[] = [':root {']
|
|
110
149
|
for (const row of doc.color) {
|
|
111
150
|
if (val(row, 'Value')) {
|
|
@@ -135,7 +174,8 @@ function buildCss(doc: DesignDoc): string {
|
|
|
135
174
|
}
|
|
136
175
|
}
|
|
137
176
|
lines.push('}')
|
|
138
|
-
|
|
177
|
+
const root = lines.join('\n') + '\n'
|
|
178
|
+
return faces.length ? `${fontFaceBlock(faces, 'block')}\n\n${root}` : root
|
|
139
179
|
}
|
|
140
180
|
|
|
141
181
|
function escape(s: string): string {
|
|
@@ -178,7 +218,7 @@ function previewChrome(): string {
|
|
|
178
218
|
return [' :root {', ...lines, ' }'].join('\n')
|
|
179
219
|
}
|
|
180
220
|
|
|
181
|
-
function buildHtml(doc: DesignDoc): string {
|
|
221
|
+
function buildHtml(doc: DesignDoc, bodyStack: string): string {
|
|
182
222
|
const sections = [
|
|
183
223
|
sectionPersonality(doc.personality),
|
|
184
224
|
sectionColor(doc.color),
|
|
@@ -205,7 +245,7 @@ function buildHtml(doc: DesignDoc): string {
|
|
|
205
245
|
<link rel="stylesheet" href="design.css">
|
|
206
246
|
<style>
|
|
207
247
|
${previewChrome()}
|
|
208
|
-
body { font-family:
|
|
248
|
+
body { font-family: ${bodyStack}; margin: 2rem; max-width: 960px; color: var(--preview-ink); background: var(--preview-paper); }
|
|
209
249
|
h1 { margin-top: 0; }
|
|
210
250
|
h2 { margin-top: 2rem; border-bottom: 1px solid var(--preview-rule); padding-bottom: 0.25rem; }
|
|
211
251
|
table { border-collapse: collapse; width: 100%; margin-top: 0.5rem; }
|
package/src/gate/measures.ts
CHANGED
|
@@ -629,6 +629,63 @@ export const shippedReferences: Measure = async (ctx) => {
|
|
|
629
629
|
}
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
+
const RAW_FIELD_FILE_REFERENCE = /(?:^|\s)(?:-f|--raw-field)[ =]\S+=@/
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* A `gh api` raw-string flag carrying a file reference, over the seven shipped
|
|
636
|
+
* corpora.
|
|
637
|
+
*
|
|
638
|
+
* `-f body=@<path>` sends the literal path as the field value, so a comment
|
|
639
|
+
* edited that way is overwritten with the path itself and loses the marker a
|
|
640
|
+
* later read resolves it by. `-F` is the flag that reads the file. The pattern
|
|
641
|
+
* requires `=@`, so `-F`, `--field`, and a raw string such as `-f body="text"`
|
|
642
|
+
* all pass.
|
|
643
|
+
*
|
|
644
|
+
* It scans prose rather than parsing a command, so a body quoting the wrong
|
|
645
|
+
* spelling to explain it fails too. Reword it rather than exempting it, since an
|
|
646
|
+
* exemption is what lets a real occurrence hide.
|
|
647
|
+
*/
|
|
648
|
+
export const rawFieldFileReference: Measure = async (ctx) => {
|
|
649
|
+
const files = shippedCorpusFiles(ctx.root)
|
|
650
|
+
|
|
651
|
+
if (files.length === 0) {
|
|
652
|
+
return {
|
|
653
|
+
emissions: [],
|
|
654
|
+
unmeasured: `No corpus under ${SHIPPED_CORPORA.join(', ')} is present, so no shipped file was read.`,
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const found = files.flatMap((file) =>
|
|
659
|
+
readFileSync(join(ctx.root, file), 'utf8')
|
|
660
|
+
.split('\n')
|
|
661
|
+
.flatMap((text, index) =>
|
|
662
|
+
RAW_FIELD_FILE_REFERENCE.test(text) ? [`${file}:${index + 1}`] : [],
|
|
663
|
+
),
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
if (found.length === 0) {
|
|
667
|
+
return {
|
|
668
|
+
emissions: [
|
|
669
|
+
info(
|
|
670
|
+
`No raw-field file reference across ${files.length} files in ${SHIPPED_CORPORA.length} shipped corpora`,
|
|
671
|
+
),
|
|
672
|
+
],
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
emissions: found.map((location) =>
|
|
678
|
+
warn(
|
|
679
|
+
`${location} passes a file reference through a raw-string flag, which posts the path as the field value. Use -F instead`,
|
|
680
|
+
),
|
|
681
|
+
),
|
|
682
|
+
failure:
|
|
683
|
+
found.length === 1
|
|
684
|
+
? 'One raw-field file reference in the shipped corpora. Change -f or --raw-field to -F or --field where the value reads a file with @, or reword a prose line quoting the wrong spelling.'
|
|
685
|
+
: `${found.length} raw-field file references in the shipped corpora. Change -f or --raw-field to -F or --field where the value reads a file with @, or reword a prose line quoting the wrong spelling.`,
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
632
689
|
/**
|
|
633
690
|
* Every git-tracked file quoting a listed client command with the wrong
|
|
634
691
|
* argument, over the whole tree rather than one corpus, since a wrong
|
package/src/gate/stages.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
recordIdempotence,
|
|
11
11
|
sandboxCoverage,
|
|
12
12
|
seedStandards,
|
|
13
|
+
rawFieldFileReference,
|
|
13
14
|
shippedReferences,
|
|
14
15
|
standardCriteria,
|
|
15
16
|
unreferencedRules,
|
|
@@ -207,20 +208,6 @@ export const STAGES: readonly Stage[] = [
|
|
|
207
208
|
],
|
|
208
209
|
success: 'Hero clean',
|
|
209
210
|
},
|
|
210
|
-
{
|
|
211
|
-
id: 'tooling-paths',
|
|
212
|
-
label: 'Tooling paths',
|
|
213
|
-
checks: [
|
|
214
|
-
script('regen-tooling-paths.sh', 'Tooling-path regen failed'),
|
|
215
|
-
{
|
|
216
|
-
kind: 'drift',
|
|
217
|
-
pathspec: 'claude/skills/canon-cli/SKILL.md',
|
|
218
|
-
failure:
|
|
219
|
-
'The overwrite contract drifted from what the stacks hold. Run bun run check and commit claude/skills/canon-cli/SKILL.md.',
|
|
220
|
-
},
|
|
221
|
-
],
|
|
222
|
-
success: 'Tooling paths clean',
|
|
223
|
-
},
|
|
224
211
|
{
|
|
225
212
|
// `canon/DESIGN.md`, the base stylesheet, the web stylesheet, and the
|
|
226
213
|
// tab icon are all written from `src/design/tokens.ts` and none is edited
|
|
@@ -423,6 +410,15 @@ export const STAGES: readonly Stage[] = [
|
|
|
423
410
|
skipped: 'No shipped corpus changed, so no reference was read',
|
|
424
411
|
checks: [{ kind: 'measure', measure: shippedReferences }],
|
|
425
412
|
},
|
|
413
|
+
{
|
|
414
|
+
// Same corpora as shipped-references and a separate stage, since the
|
|
415
|
+
// verdict is a flag spelling rather than an unresolvable reference.
|
|
416
|
+
id: 'raw-field-file-reference',
|
|
417
|
+
label: 'Raw-field file references',
|
|
418
|
+
scope: new RegExp(SHIPPED_CORPORA.map((corpus) => `^${corpus}/`).join('|')),
|
|
419
|
+
skipped: 'No shipped corpus changed, so no flag was read',
|
|
420
|
+
checks: [{ kind: 'measure', measure: rawFieldFileReference }],
|
|
421
|
+
},
|
|
426
422
|
{
|
|
427
423
|
// Scoped to the two files a citation can drift between, so an edit to
|
|
428
424
|
// either one runs the check. web/src/content/copy.ts carries the anchors
|
package/src/gov/list.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface GovCatalog {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Reports each stack's own entries expanded, so a folder entry reaches a
|
|
33
|
-
* consumer as the rules it stands for. `setup
|
|
33
|
+
* consumer as the rules it stands for. the `setup` skill dedupes `--add` extras
|
|
34
34
|
* against this list, and a stack answering `core` there would re-add every
|
|
35
35
|
* rule that folder already carries.
|
|
36
36
|
*
|
package/src/sandbox/census.ts
CHANGED
|
@@ -71,9 +71,9 @@ export function listSkills(root: string): string[] {
|
|
|
71
71
|
*
|
|
72
72
|
* `<category>-<command>` is the rule `canon/context/sandbox/coverage.md` states, and it
|
|
73
73
|
* alone pairs 29 of 54 skills. The bare `<command>` fallback is what reaches the
|
|
74
|
-
* rest: `claude/setup
|
|
75
|
-
*
|
|
76
|
-
*
|
|
74
|
+
* rest: `claude/target-setup.sh` drives the `target-setup` skill, not a `claude-target-setup` that
|
|
75
|
+
* does not exist. Stating one spelling and shipping two is what let the audit
|
|
76
|
+
* report a paired skill as unpaired.
|
|
77
77
|
*
|
|
78
78
|
* Returns undefined for a scenario driving no skill at all, which is every
|
|
79
79
|
* `infra/` and `tooling/` scenario. Those exercise a CLI domain rather than a
|