@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.
Files changed (74) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/canon-cli/SKILL.md +3 -74
  3. package/claude/skills/canon-operator/REQUIREMENT.md +2 -2
  4. package/claude/skills/canon-operator/SKILL.md +5 -5
  5. package/claude/skills/canon-rollout/SKILL.md +7 -6
  6. package/claude/skills/design-taste/references/craft.md +1 -1
  7. package/claude/skills/design-taste/references/preflight.md +2 -2
  8. package/claude/skills/design-taste/references/tells.md +1 -1
  9. package/claude/skills/draft-readme/REQUIREMENT.md +2 -0
  10. package/claude/skills/draft-readme/SKILL.md +9 -1
  11. package/claude/skills/git-followup/SKILL.md +1 -1
  12. package/claude/skills/git-issue/SKILL.md +2 -2
  13. package/{standards → claude/skills/git-issue/references}/issue.md +3 -8
  14. package/claude/skills/git-pr/SKILL.md +2 -2
  15. package/claude/skills/index-lookup/REQUIREMENT.md +2 -2
  16. package/claude/skills/project-commands/REQUIREMENT.md +1 -1
  17. package/claude/skills/seed-sync/REQUIREMENT.md +1 -1
  18. package/claude/skills/target-check/REQUIREMENT.md +42 -0
  19. package/claude/skills/target-check/SKILL.md +91 -0
  20. package/claude/skills/target-check/references/domains.md +88 -0
  21. package/claude/skills/target-setup/REQUIREMENT.md +56 -0
  22. package/claude/skills/target-setup/SKILL.md +163 -0
  23. package/claude/skills/target-setup/references/detect.md +80 -0
  24. package/claude/skills/{setup-indexes/SKILL.md → target-setup/references/indexes.md} +59 -23
  25. package/claude/skills/target-setup/references/verify.md +95 -0
  26. package/claude/skills/ui-checklist/SKILL.md +17 -0
  27. package/docs/agents/commands.md +1 -1
  28. package/docs/agents/sandbox.md +1 -1
  29. package/docs/agents/scripting.md +3 -2
  30. package/docs/target-projects.md +13 -11
  31. package/docs/workflow/ai-workflow.md +19 -23
  32. package/governance/rules/ci/700-ci-workflow.md +11 -0
  33. package/governance/rules/ui/400-ui.md +0 -5
  34. package/governance/rules/ui/460-design-taste.md +1 -1
  35. package/governance/rules/ui/465-interface-casing.md +31 -0
  36. package/governance/stacks/astro.toml +1 -1
  37. package/governance/stacks/react.toml +1 -1
  38. package/package.json +1 -1
  39. package/scripts/core/regen-hero.sh +1 -1
  40. package/scripts/core/regen-web-previews.ts +1 -0
  41. package/src/claude/cases/setup.ts +20 -11
  42. package/src/commands/design.ts +8 -2
  43. package/src/commands/sync.ts +1 -1
  44. package/src/design/css.ts +5 -2
  45. package/src/design/render.ts +46 -6
  46. package/src/gate/measures.ts +57 -0
  47. package/src/gate/stages.ts +10 -14
  48. package/src/gov/list.ts +1 -1
  49. package/src/sandbox/census.ts +3 -3
  50. package/src/shipped/references.ts +1 -1
  51. package/standards/index.md +0 -1
  52. package/standards/readme.md +22 -3
  53. package/tooling/astro/manifest.toml +1 -0
  54. package/tooling/nextjs/manifest.toml +1 -0
  55. package/tooling/web/configs/.github/workflows/readme-screenshot.yml +110 -0
  56. package/tooling/web/configs/e2e/screenshot.ts +69 -15
  57. package/tooling/web/configs/scripts/lib/preview-server.sh +31 -0
  58. package/tooling/web/configs/scripts/readme-screenshot.sh +26 -0
  59. package/tooling/web/configs/scripts/screenshot.sh +1 -24
  60. package/tooling/web/manifest.toml +1 -0
  61. package/tooling/web/reference.md +6 -1
  62. package/claude/skills/setup-gov/REQUIREMENT.md +0 -37
  63. package/claude/skills/setup-gov/SKILL.md +0 -77
  64. package/claude/skills/setup-indexes/REQUIREMENT.md +0 -48
  65. package/claude/skills/setup-init/REQUIREMENT.md +0 -46
  66. package/claude/skills/setup-init/SKILL.md +0 -133
  67. package/claude/skills/setup-plugins/REQUIREMENT.md +0 -42
  68. package/claude/skills/setup-plugins/SKILL.md +0 -81
  69. package/claude/skills/setup-plugins/references/plugin-catalog.md +0 -53
  70. package/claude/skills/setup-smoke/REQUIREMENT.md +0 -38
  71. package/claude/skills/setup-smoke/SKILL.md +0 -53
  72. package/claude/skills/setup-verify/REQUIREMENT.md +0 -39
  73. package/claude/skills/setup-verify/SKILL.md +0 -51
  74. package/scripts/core/regen-tooling-paths.sh +0 -61
@@ -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-init` in Claude Code. The skill detects the stack, resolves flags, previews the full chain, and runs it end-to-end.
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
- - `setup-verify` runs the installed `package.json` scripts (lint, typecheck, check, test, build) and reports pass or fail
62
- - `setup-indexes` bootstraps the `index.md` system over the project's own documentation folders, confirming candidate folders with the operator rather than running unattended
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-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.
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-smoke` by hand once `setup-verify` passes, for the heavier server smoke, end-to-end, and screenshot pass, since the same flakiness reasons that excluded those stages from `setup-verify` keep it out of this unattended chain too.
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-init` 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`).
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-gov` for the governance layer alone, which is language-neutral.
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-gov`, or run `canon gov install <stack> --add <rule> <path>`
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-indexes`
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-init`. 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`.
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-init`. 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.
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-init` | On a fresh scaffold, to detect the stack and run the whole install chain in one pass |
206
- | `canon:canon-operator` | On a project that already exists, to read what it carries before an install is picked |
207
- | `canon:setup-gov` | When the governance rules are wanted without the tooling chain |
208
- | `canon:setup-indexes` | When a markdown-heavy folder needs an `index.md` a session can browse |
209
- | `canon:setup-plugins` | On a new machine, to install the community and official plugins user-scoped |
210
- | `canon:setup-verify` | After the agent generates configs, to run the installed scripts and report pass or fail |
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:seed-sync` | After a toolkit update, to reconcile installed seeds without losing customizations |
295
- | `canon:migration-claude-md` | When `CLAUDE.md` grew past what always-load context should carry |
296
- | `canon:migration-context` | When `docs/` holds agent-flavored files belonging in `canon/context/` |
297
- | `canon:migration-superseded` | When a drift report names a `.claude/` file a folder has replaced |
298
- | `canon:migration-standards-drop` | When the project still holds an installed `.claude/standards/` tree |
299
- | `canon:canon-feedback-file` | When something in the toolkit is broken, missing, or off |
300
- | `canon:canon-feedback-triage` | In the toolkit repo, to work through the open feedback issues |
301
- | `canon:canon-rollout` | In the toolkit repo, to take one change out to every consuming project at once |
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, button labels and error wording are a separate topic. `400-ui` routes them.
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.102.0",
4
+ "version": "4.104.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -197,7 +197,7 @@ const FEATURED_SKILLS = [
197
197
  "review-pr",
198
198
  "task-board",
199
199
  "git-ship",
200
- "setup-init",
200
+ "target-setup",
201
201
  "systematic-debugging",
202
202
  ]
203
203
 
@@ -21,6 +21,7 @@ const root = new URL('../..', import.meta.url).pathname
21
21
  const { cssPath } = renderDesignDoc(
22
22
  `${root}${DESIGN_DOCUMENT}`,
23
23
  `${root}web/public/previews/design-tokens`,
24
+ { embedFonts: true },
24
25
  )
25
26
 
26
27
  const css = readFileSync(cssPath, 'utf8')
@@ -1,39 +1,40 @@
1
1
  import type { SkillCase } from '@/claude/skills-rank'
2
2
 
3
3
  /**
4
- * `setup-*`, `migration-*`, `canon-*`, and `create-rule`: scaffolding,
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-gov',
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-indexes',
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-init',
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-verify',
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-smoke',
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-gov',
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.",
@@ -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
- .action((opts: { source: string; out: string }) => {
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
  )
@@ -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-init to resolve a stack.',
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(faces: readonly FontFace[]): string {
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: swap;
149
+ font-display: ${display};
147
150
  src: url(data:font/woff2;base64,${face.base64}) format('woff2');
148
151
  }`,
149
152
  )
@@ -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
- writeFileSync(cssPath, buildCss(doc))
31
- writeFileSync(htmlPath, buildHtml(doc))
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
- return lines.join('\n') + '\n'
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: system-ui, sans-serif; margin: 2rem; max-width: 960px; color: var(--preview-ink); background: var(--preview-paper); }
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; }
@@ -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
@@ -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-gov` dedupes `--add` extras
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
  *
@@ -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-init.sh` drives the `setup-init` skill, not a
75
- * `claude-setup-init` that does not exist. Stating one spelling and shipping two
76
- * is what let the audit report a paired skill as unpaired.
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