@erclx/canon 5.40.0 → 5.41.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 (49) hide show
  1. package/README.md +34 -17
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
  4. package/claude/skills/auto-ship/SKILL.md +6 -0
  5. package/claude/skills/auto-ship/references/fan-in.md +59 -0
  6. package/claude/skills/create-skill/references/skill.md +1 -1
  7. package/claude/skills/decision-escalate/REQUIREMENT.md +1 -1
  8. package/claude/skills/decision-escalate/SKILL.md +1 -2
  9. package/claude/skills/git-branch/REQUIREMENT.md +17 -6
  10. package/claude/skills/git-branch/SKILL.md +63 -7
  11. package/claude/skills/plan-feature/references/batches.md +48 -1
  12. package/claude/skills/plan-feature/references/plan.md +2 -0
  13. package/claude/skills/plan-intake-answer/REQUIREMENT.md +1 -1
  14. package/claude/skills/plan-intake-answer/SKILL.md +1 -2
  15. package/claude/skills/review-address/references/rebase-conflicts.md +7 -0
  16. package/claude/skills/review-ui/SKILL.md +2 -0
  17. package/claude/skills/review-ui/references/close-out.md +47 -0
  18. package/claude/skills/review-ui/references/post.md +3 -1
  19. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -0
  20. package/claude/skills/role-worker/REQUIREMENT.md +1 -0
  21. package/claude/skills/role-worker/SKILL.md +9 -0
  22. package/claude/skills/teach-workspace/REQUIREMENT.md +1 -1
  23. package/claude/skills/teach-workspace/SKILL.md +1 -2
  24. package/docs/workflow/parallel-features.md +1 -0
  25. package/docs/workflow/skill-map.md +1 -1
  26. package/package.json +1 -1
  27. package/src/claude/cases/git.ts +4 -0
  28. package/src/commands/pr.ts +32 -9
  29. package/src/commands/records.ts +11 -3
  30. package/src/context/shape-gated.ts +1 -0
  31. package/src/markdown/scope.ts +6 -2
  32. package/src/pr/frames.ts +53 -9
  33. package/src/records/validate.ts +1 -0
  34. package/src/tasks/answers.ts +79 -2
  35. package/src/tasks/fan-in.ts +176 -0
  36. package/src/tasks/stack.ts +1 -1
  37. package/tooling/astro/reference.md +80 -39
  38. package/tooling/base/reference.md +63 -32
  39. package/tooling/base/seeds/canon/context/ci.md +7 -15
  40. package/tooling/base/seeds/canon/context/development.md +14 -14
  41. package/tooling/claude/reference.md +40 -22
  42. package/tooling/cloudflare/reference.md +31 -12
  43. package/tooling/go/reference.md +47 -23
  44. package/tooling/nextjs/reference.md +52 -19
  45. package/tooling/php/reference.md +35 -16
  46. package/tooling/python/reference.md +47 -30
  47. package/tooling/shell/reference.md +6 -5
  48. package/tooling/vite-react/reference.md +49 -21
  49. package/tooling/web/reference.md +130 -70
@@ -0,0 +1,176 @@
1
+ import type { Finding } from '@/records/validate'
2
+ import { PLAN_BRANCH_TYPE } from '@/tasks/branch'
3
+ import { constraintBullets, type StackPlan } from '@/tasks/stack'
4
+
5
+ const PLAN_PREFIX = 'feature-'
6
+
7
+ const MEMBER_OF = /\bfan[- ]in member of\s+/gi
8
+
9
+ const FANS_IN = /\bfans in\s+/gi
10
+
11
+ /**
12
+ * A backticked slug may drop the `feature-` prefix, and a bare one must carry
13
+ * it. Without the prefix a bare word is indistinguishable from the prose that
14
+ * follows the list, so `Fans in feature-a and the rest` would read `the` as a
15
+ * member.
16
+ */
17
+ const TICKED_SLUG = /^`((?:feature-)?[a-z0-9][a-z0-9-]*)`/i
18
+ const BARE_SLUG = /^(feature-[a-z0-9][a-z0-9-]*)/i
19
+
20
+ const LIST_SEPARATOR = /^\s*(?:,\s*(?:and\s+)?|and\s+)/i
21
+
22
+ function stemOf(slug: string): string {
23
+ const lower = slug.toLowerCase()
24
+
25
+ return lower.startsWith(PLAN_PREFIX) ? lower : `${PLAN_PREFIX}${lower}`
26
+ }
27
+
28
+ function leadingSlug(text: string): { stem: string; rest: string } | null {
29
+ const match = TICKED_SLUG.exec(text) ?? BARE_SLUG.exec(text)
30
+ const slug = match?.[1]
31
+ if (match === null || slug === undefined) return null
32
+
33
+ return { stem: stemOf(slug), rest: text.slice(match[0].length) }
34
+ }
35
+
36
+ /** Every slug in the list standing after each match of `phrase`, in order. */
37
+ function readList(text: string, phrase: RegExp, many: boolean): string[] {
38
+ const stems: string[] = []
39
+
40
+ for (const bullet of constraintBullets(text)) {
41
+ for (const match of bullet.matchAll(phrase)) {
42
+ let rest = bullet.slice((match.index ?? 0) + match[0].length)
43
+
44
+ for (;;) {
45
+ const slug = leadingSlug(rest)
46
+ if (slug === null) break
47
+ stems.push(slug.stem)
48
+ if (!many) break
49
+
50
+ const separator = LIST_SEPARATOR.exec(slug.rest)
51
+ if (separator === null) break
52
+ rest = slug.rest.slice(separator[0].length)
53
+ }
54
+ }
55
+ }
56
+
57
+ return [...new Set(stems)]
58
+ }
59
+
60
+ /** The integrators a member plan names, which a conforming member holds one of. */
61
+ export function readFanInIntegrators(text: string): string[] {
62
+ return readList(text, MEMBER_OF, false)
63
+ }
64
+
65
+ /** The members an integrator plan fans in, in the order its merges run. */
66
+ export function readFanInMembers(text: string): string[] {
67
+ return readList(text, FANS_IN, true)
68
+ }
69
+
70
+ /**
71
+ * The branch a member builds on, derived the way `planBranch` derives it, since
72
+ * the integrator merges and the dispatch gate reads the branch the worker took.
73
+ */
74
+ export function fanInBranch(stem: string): string {
75
+ const slug = stem.startsWith(PLAN_PREFIX)
76
+ ? stem.slice(PLAN_PREFIX.length)
77
+ : stem
78
+
79
+ return `${PLAN_BRANCH_TYPE}/${slug}`
80
+ }
81
+
82
+ function unmatched(stem: string, subject: string, message: string): Finding {
83
+ return { kind: 'fan-in-unmatched', record: `${stem}.md`, subject, message }
84
+ }
85
+
86
+ /**
87
+ * Every place a fan-in set's two sides disagree across the live plans.
88
+ *
89
+ * An absent plan is reported rather than skipped, unlike `stackFindings`. The
90
+ * caller hands over the live folder only, so a member archived after it merged
91
+ * reads as absent, and an integrator still listing it would otherwise take its
92
+ * leftover branch on origin as a member that finished.
93
+ *
94
+ * A member two integrators list is reported on each of them rather than paired
95
+ * with whichever sorts first, since either merge would carry it.
96
+ */
97
+ export function fanInFindings(plans: readonly StackPlan[]): Finding[] {
98
+ const integratorsOf = new Map(
99
+ plans.map((plan) => [plan.stem, readFanInIntegrators(plan.text)]),
100
+ )
101
+ const membersOf = new Map(
102
+ plans.map((plan) => [plan.stem, readFanInMembers(plan.text)]),
103
+ )
104
+ const listers = new Map<string, string[]>()
105
+ for (const [integrator, members] of membersOf) {
106
+ for (const member of members) {
107
+ listers.set(member, [...(listers.get(member) ?? []), integrator])
108
+ }
109
+ }
110
+
111
+ const findings: Finding[] = []
112
+
113
+ for (const { stem } of plans) {
114
+ const integrators = integratorsOf.get(stem) ?? []
115
+
116
+ for (const integrator of integrators) {
117
+ if (integrators.length > 1) {
118
+ findings.push(
119
+ unmatched(
120
+ stem,
121
+ integrator,
122
+ `is a fan-in member of ${integrators.map((name) => `\`${name}\``).join(' and ')}. A member fans in to one integrator.`,
123
+ ),
124
+ )
125
+ } else if (!membersOf.has(integrator)) {
126
+ findings.push(
127
+ unmatched(
128
+ stem,
129
+ integrator,
130
+ `is a fan-in member of \`${integrator}\`, which is not a live plan. Write the integrator's plan, or drop the member bullet.`,
131
+ ),
132
+ )
133
+ } else if (!(membersOf.get(integrator) ?? []).includes(stem)) {
134
+ findings.push(
135
+ unmatched(
136
+ stem,
137
+ integrator,
138
+ `is a fan-in member of \`${integrator}\`, whose \`Fans in\` bullet does not list it.`,
139
+ ),
140
+ )
141
+ }
142
+ }
143
+
144
+ for (const member of membersOf.get(stem) ?? []) {
145
+ const others = (listers.get(member) ?? []).filter((name) => name !== stem)
146
+
147
+ if (others.length > 0) {
148
+ findings.push(
149
+ unmatched(
150
+ stem,
151
+ member,
152
+ `fans in \`${member}\`, which ${others.map((name) => `\`${name}\``).join(' and ')} also fans in. A member fans in to one integrator.`,
153
+ ),
154
+ )
155
+ } else if (!integratorsOf.has(member)) {
156
+ findings.push(
157
+ unmatched(
158
+ stem,
159
+ member,
160
+ `fans in \`${member}\`, which is not a live plan. An archived member is not a finished one, so drop it from the list or restore its plan.`,
161
+ ),
162
+ )
163
+ } else if (!(integratorsOf.get(member) ?? []).includes(stem)) {
164
+ findings.push(
165
+ unmatched(
166
+ stem,
167
+ member,
168
+ `fans in \`${member}\`, which does not name it as its integrator.`,
169
+ ),
170
+ )
171
+ }
172
+ }
173
+ }
174
+
175
+ return findings
176
+ }
@@ -41,7 +41,7 @@ export interface StackPair {
41
41
  * One string per `**Constraints:**` bullet, with a wrapped continuation line
42
42
  * joined to the bullet it belongs to.
43
43
  */
44
- function constraintBullets(text: string): string[] {
44
+ export function constraintBullets(text: string): string[] {
45
45
  const bullets: string[] = []
46
46
 
47
47
  for (const line of splitPlanSections(text).get('Constraints') ?? []) {
@@ -4,25 +4,49 @@
4
4
 
5
5
  ## Overview
6
6
 
7
- The astro stack covers Astro + TypeScript projects: content sites, marketing sites, blogs, and docs. Interactive islands use React only. Output is static. It ships golden configs for `astro.config.mjs`, `vitest.config.ts` (using `getViteConfig` from `astro/config`), `playwright.config.ts` (preview-server backed), a `tsconfig.json` that extends `astro/tsconfigs/strict`, an `eslint.config.js` that keeps ESLint for this stack alone, and a `.prettierrc` that formats `.astro` files alone.
7
+ The astro stack covers Astro + TypeScript projects, being content sites, marketing sites, blogs, and docs. Interactive islands use React only and output is static.
8
+
9
+ It ships golden configs for `astro.config.mjs`, `vitest.config.ts`, `playwright.config.ts` (preview-server backed), and a `tsconfig.json` extending `astro/tsconfigs/strict`. An `eslint.config.js` keeps ESLint for this stack alone, and a `.prettierrc` formats `.astro` files alone.
8
10
 
9
11
  ## Scaffold checklist
10
12
 
11
13
  1. Scaffold with `bunx create-astro@latest`. Choose `TypeScript: Strict`. Skip git init and install.
12
- 2. Add React integration: `bunx astro add react`. Do not use `astro add tailwind`. That command installs the v3 integration. Tailwind v4 arrives via the web manifest.
13
- 3. Install web tooling: `canon tooling sync web . --write`
14
- 4. Install astro adapter: `canon tooling sync astro . --write`
14
+ 2. Add React with `bunx astro add react`. Never use `astro add tailwind`, which installs the v3 integration. Tailwind v4 arrives via the web manifest.
15
+ 3. Install web tooling with `canon tooling sync web . --write`.
16
+ 4. Install the astro adapter with `canon tooling sync astro . --write`.
15
17
  5. Extend the `ci` and `development` context entries under `canon/context/` per the web reference's extend sections plus the astro rows below.
16
18
  6. Run `bun run lint:fix`, then `bun run format`, then `bun run check`.
17
19
 
18
20
  ## What ships as golden configs
19
21
 
20
- - `astro.config.mjs`: `@astrojs/react` integration, `@tailwindcss/vite` in `vite.plugins`, `@/` path alias via `vite.resolve.alias`, `ASTRO_SITE` env for the `site` field. Port `4321` plus `WORKTREE_PORT_OFFSET` at `server.port`, with `strictPort` under `vite.server` and `vite.preview`. Astro merges the user's `vite` block into the config backing both its dev and its static preview server, and feeds `server.port` through as the preview port, so the port sits at the top level while the bind guarantee sits under `vite`.
21
- - `vitest.config.ts`: uses `getViteConfig` from `astro/config` (not `mergeConfig`). jsdom, globals, setup file, `passWithNoTests: true`, v8 coverage, `**/*.astro` in coverage excludes.
22
- - `playwright.config.ts`: all browsers, `webServer` runs `bun run build && bun run preview` on port `4321` plus `WORKTREE_PORT_OFFSET`, `reuseExistingServer: false`. Astro's dev/prod gap is wide (MDX, island hydration, asset optimization), so E2E always tests the built `dist/`. `DIST_PREBUILT` set in the environment drops the `build` half, running `bun run preview` alone against a `dist/` a prior CI job already produced.
23
- - `tsconfig.json`: extends `astro/tsconfigs/strict`, adds `skipLibCheck`, `vitest/globals` and `@testing-library/jest-dom/vitest` in types, `@/` paths.
24
- - `card.config.mjs`, `card-src/pages/og-card.astro`, and `scripts/check-card-exclusion.sh`: the social card route, its structural build exclusion, and the check enforcing it. See `## Social card route` below.
25
- - `eslint.config.js`: this stack's own flat config, since the web layer lints with oxlint, which parses no `.astro` file. Adds `eslint-plugin-astro` (`.astro` parser via `astro-eslint-parser`). React-hooks scoped to `.jsx`/`.tsx` only (`.astro` is not React). The shared block's `files` selector includes `.astro`, so `check-file/filename-naming-convention` reaches `.ts`, `.tsx`, and `.astro` under `KEBAB_CASE`, overriding Astro's own PascalCase component convention deliberately, on the ground that a component's name in markup comes from the import binding rather than the filename. `.js` and `.jsx` stay out of the rule's own pattern, matching `web` and `nextjs`. `check-file/folder-naming-convention` reaches every `src/**` folder except `__tests__` and the whole `pages/` subtree, which carries its own off-block for the bracket-named dynamic routes and nested slug folders Astro's file-based routing produces. See `canon/context/surfaces/tooling.md` for the measurement.
22
+ ### Astro and test configs
23
+
24
+ The configs below wire Astro into the web layer's test setup.
25
+
26
+ - `astro.config.mjs` adds `@astrojs/react`, `@tailwindcss/vite` in `vite.plugins`, the `@/` alias via `vite.resolve.alias`, and `ASTRO_SITE` for the `site` field.
27
+ - It serves port `4321` plus `WORKTREE_PORT_OFFSET` at `server.port`, with `strictPort` under `vite.server` and `vite.preview`.
28
+ - Astro feeds `server.port` through as the preview port but merges the `vite` block into the config backing both servers, so the port sits at the top level and the bind guarantee under `vite`.
29
+ - `vitest.config.ts` uses `getViteConfig` from `astro/config`, not `mergeConfig`, with jsdom, globals, the setup file, `passWithNoTests: true`, v8 coverage, and `**/*.astro` in coverage excludes.
30
+ - `tsconfig.json` adds `skipLibCheck`, `vitest/globals` and `@testing-library/jest-dom/vitest` in types, and `@/` paths.
31
+
32
+ ### Playwright config
33
+
34
+ `playwright.config.ts` runs all browsers, with `webServer` running `bun run build && bun run preview` on port `4321` plus `WORKTREE_PORT_OFFSET` and `reuseExistingServer: false`.
35
+
36
+ - Astro's dev/prod gap is wide (MDX, island hydration, asset optimization), so E2E always tests the built `dist/`.
37
+ - `DIST_PREBUILT` in the environment drops the `build` half, running `bun run preview` alone against a `dist/` a prior CI job produced.
38
+
39
+ ### ESLint config
40
+
41
+ `eslint.config.js` is this stack's own flat config, since the web layer lints with oxlint, which parses no `.astro` file. It adds `eslint-plugin-astro` with `astro-eslint-parser`.
42
+
43
+ - React hooks apply to `.jsx` and `.tsx` only, since `.astro` is not React.
44
+ - `check-file/filename-naming-convention` reaches `.ts`, `.tsx`, and `.astro` under `KEBAB_CASE`, overriding Astro's PascalCase on purpose, since markup names a component by its import binding.
45
+ - `.js` and `.jsx` stay out of that rule, matching `web` and `nextjs`.
46
+ - `check-file/folder-naming-convention` reaches every `src/**` folder except `__tests__` and the `pages/` subtree, which has its own off-block for bracket-named dynamic routes and slug folders.
47
+ - `canon/context/surfaces/tooling.md` carries the measurement behind both naming rules.
48
+
49
+ The social card files also ship as golden configs, being `card.config.mjs`, `card-src/pages/og-card.astro`, and `scripts/check-card-exclusion.sh`. See `## Social card route` below.
26
50
 
27
51
  ## Typecheck
28
52
 
@@ -30,73 +54,90 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
30
54
 
31
55
  ## Vitest scope
32
56
 
33
- Unit-test React island components (`.tsx`) only. Do not test `.astro` files. Page-level behavior is verified by Playwright against rendered output.
57
+ Unit-test React island components (`.tsx`) only. Do not test `.astro` files, since Playwright verifies page-level behavior against rendered output.
34
58
 
35
- Vitest is pinned to `^5` in the web manifest, which supports the Vite 8 that Astro 7 bundles. The earlier `^3` pin avoided `^4` bundling rolldown-vite, which ignored the esbuild JSX config in `@vitejs/plugin-react`. Astro now bundles Vite 8 itself, so that conflict no longer applies.
59
+ Vitest is pinned to `^5` in the web manifest, which supports the Vite 8 that Astro 7 bundles. The earlier `^3` pin dodged `^4` bundling rolldown-vite, which ignored the esbuild JSX config in `@vitejs/plugin-react`. Astro bundling Vite 8 itself ended that conflict.
36
60
 
37
61
  ## Formatting (extend)
38
62
 
39
- - Format `.astro` files with Prettier and every other file with oxfmt. oxfmt loads no Prettier plugin, so it cannot parse `.astro`.
40
- - Load `prettier-plugin-astro` first in `.prettierrc`'s plugins, then `prettier-plugin-tailwindcss` last (per its docs), with a parser override for `.astro` files.
63
+ - Format `.astro` files with Prettier and every other file with oxfmt, which loads no Prettier plugin and cannot parse `.astro`.
64
+ - Load `prettier-plugin-astro` first in `.prettierrc`'s plugins and `prettier-plugin-tailwindcss` last, with a parser override for `.astro` files.
41
65
  - Ignore `**/*.astro` in this stack's `.oxfmtrc.json`, so the two formatters never touch one file.
42
- - Copy every other option from base's `.oxfmtrc.json` into this one by hand. Scan takes the child config over the parent's whole file, so a change to base's options is lost here until it is copied.
66
+ - Copy every other option from base's `.oxfmtrc.json` by hand. Scan takes the child config over the parent's whole file, so a change to base's options is lost here until copied.
43
67
  - Override `check:format` and `format` to run oxfmt, then Prettier over `**/*.astro`.
44
68
 
45
69
  ## ESLint
46
70
 
47
71
  - Override `lint` and `lint:fix` back to `eslint . --max-warnings 0`, since the web layer sets them to oxlint.
48
- - Install the ESLint packages here (`eslint@^9`, `@eslint/js`, `typescript-eslint`, `eslint-config-prettier`, `eslint-plugin-react-hooks`, `eslint-plugin-vitest`, `globals`), since the web layer no longer ships them. Pin `eslint@^9`: ESLint 10 dropped the `LegacyESLint` shim `typescript-eslint@8` still imports.
72
+ - Install `eslint@^9`, `@eslint/js`, `typescript-eslint`, `eslint-config-prettier`, `eslint-plugin-react-hooks`, `eslint-plugin-vitest`, and `globals`, since the web layer no longer ships them.
73
+ - Pin `eslint@^9`, because ESLint 10 dropped the `LegacyESLint` shim `typescript-eslint@8` still imports.
49
74
  - Expect the web layer's `.oxlintrc.json` in the target. Nothing runs it here.
50
75
 
51
76
  ## Development docs (extend)
52
77
 
53
- Append to the `## Scripts` table:
78
+ Append these rows to the `## Scripts` table.
54
79
 
55
- | `bun run dev` | Start the Astro dev server on port 4321, plus this worktree's port offset. |
56
- | `bun run build` | Run `astro check` then build the static output. |
57
- | `bun run preview` | Serve the built site locally. |
58
- | `bun run astro` | Expose the Astro CLI. |
59
- | `bun run typecheck` | Run `astro check`. |
80
+ | Command | Purpose |
81
+ | ------------------- | --------------------------------------------------------------- |
82
+ | `bun run dev` | Start the Astro dev server on 4321 plus this worktree's offset. |
83
+ | `bun run build` | Run `astro check` then build the static output. |
84
+ | `bun run preview` | Serve the built site locally. |
85
+ | `bun run astro` | Expose the Astro CLI. |
86
+ | `bun run typecheck` | Run `astro check`. |
60
87
 
61
88
  ## CI docs (extend)
62
89
 
63
- In `canon/context/development/ci.md`, the Typecheck row's assertion reads: `` `astro check` passes ``. The Build row's assertion reads: `` `astro build` succeeds ``. <!-- audit-ignore-citations: canon/context/development/ci.md -->
90
+ In `canon/context/development/ci.md`, the Typecheck row asserts `` `astro check` passes `` and the Build row asserts `` `astro build` succeeds ``. <!-- audit-ignore-citations: canon/context/development/ci.md -->
64
91
 
65
92
  ## Social card route
66
93
 
67
- The social card is a route the project keeps rather than a page a render writes and deletes, so re-rendering it after a rename is an edit to one string and a re-capture instead of another pass through `draft-identity`'s pick loop with an operator in it. `canon:draft-identity` writes the picked composition into the route and captures it through the server below.
94
+ The social card is a route the project keeps rather than a page a render writes and deletes. Re-rendering it after a rename is an edit to one string and a re-capture, not another pass through the `draft-identity` pick loop.
95
+
96
+ `canon:draft-identity` writes the picked composition into the route and captures it through the server below.
68
97
 
69
98
  ### What ships
70
99
 
71
- - `card.config.mjs`: a second Astro config whose `srcDir` points at `./card-src`, which the main config's page router never reads. That is what excludes the card from the published build structurally rather than by a filename convention, the same mechanism `scenarios.astro` reaches for with `import.meta.env.DEV`. Its base port is `4421` plus `WORKTREE_PORT_OFFSET`, outside the band `astro.config.mjs` serves from, so a card server and a dev server of one worktree never contend.
72
- - `card-src/pages/og-card.astro`: the route, shipped as a working placeholder so it serves before any pick has been taken. It imports the project's own global stylesheet, which is what lets the composition read real tokens and real fonts instead of typed copies. It reads `--color-background` and `--color-text`, the names `canon design css` emits, and sets no `font-family` at all so the card inherits the project's body font rather than reading a font token the design system does not emit.
73
- - `scripts/check-card-exclusion.sh`: fails when the route's `<meta name="og-card-marker">` tag reaches `dist/`, and fails when the route dropped that tag, since the marker is what the leak test reads for.
74
- - `bun run card:dev` serves the route. `bun run card:check` runs the exclusion check, which base's verify runner, `scripts/verify.ts`, also runs after its build stage whenever the script is installed.
100
+ The route is a second config, an exclusion check, and two scripts. The page itself is covered below.
101
+
102
+ - `card.config.mjs` is a second Astro config whose `srcDir` points at `./card-src`, which the main config's page router never reads. That excludes the card from the published build structurally.
103
+ - `card.config.mjs` uses base port `4421` plus `WORKTREE_PORT_OFFSET`, outside the band `astro.config.mjs` serves from, so a worktree's card and dev servers never contend.
104
+ - `scripts/check-card-exclusion.sh` fails when the route's `<meta name="og-card-marker">` tag reaches `dist/`, and when the route dropped the tag, since the leak test reads it.
105
+ - `bun run card:dev` serves the route and `bun run card:check` runs the exclusion check, which base's `scripts/verify.ts` also runs after its build stage when the script is installed.
75
106
 
76
- ### Why each piece is shaped that way
107
+ ### Stylesheet and tokens
77
108
 
78
- The stylesheet import points at `@/styles/global.css`. A project keeping its stylesheet elsewhere retargets that one line, and an unresolved path then refuses the card server by name, which is the loud direction rather than the convenient one. The quiet failure it replaces is a card captured against no stylesheet at all: it renders as an unstyled box and reports success.
109
+ `card-src/pages/og-card.astro` ships as a working placeholder, so it serves before any pick. It imports the project's global stylesheet to read real tokens and fonts, uses `--color-background` and `--color-text` as `canon design css` emits them, and sets no `font-family`, so the card inherits the body font.
79
110
 
80
- A custom property the stylesheet does not define is the same silent failure in a second costume. It takes its fallback, captures cleanly and reports success, so the card ships on typed values while reading as though it read real ones. That is why the route names only properties `canon design css` emits, and why a project retargeting the stylesheet import checks the names against what its own stylesheet defines.
111
+ - The import points at `@/styles/global.css`. A project keeping its stylesheet elsewhere retargets that one line.
112
+ - An unresolved path refuses the card server by name. That is the loud direction, replacing a card captured against no stylesheet that renders unstyled and reports success.
81
113
 
82
- The exclusion is a check rather than a convention because the route has three consumers and only one of them can be enforced. The card server reads it, the capture reads it, and the published build must not carry it.
114
+ - A custom property the stylesheet does not define fails the same silent way. It takes its fallback, captures cleanly, and ships typed values.
115
+ - So the route names only properties `canon design css` emits, and a project retargeting the import checks the names against its own stylesheet.
83
116
 
84
- `.card` is declared at 600x315, half of the 1200x630 target. `canon capture` opens every page at a fixed 2x device scale factor with no flag to change it, so a half-size element is what lands the captured PNG on the literal size. Declaring it at 1200x630 captures at 2400x1260.
117
+ ### Exclusion and card size
118
+
119
+ The exclusion is a check rather than a convention because the route has three consumers and only one can be enforced. The card server reads it, the capture reads it, and the published build must not carry it.
120
+
121
+ `.card` is declared at 600x315, half of the 1200x630 target. `canon capture` opens every page at a fixed 2x device scale factor with no flag to change it, so a half-size element lands the PNG on the literal size. Declaring 1200x630 captures at 2400x1260.
85
122
 
86
123
  ### Why v1 is Astro-only
87
124
 
88
- Astro is the one web stack whose config gives a route a structural build exclusion. Next's App Router has no build-time route exclusion at all, and it needs none on the path it will take, since `ImageResponse` is that framework's own production mechanism and a card route there is meant to be served. `vite-react` builds a single-page app with no page router, so a card is a second Rollup entry rather than a route.
125
+ Astro is the one web stack whose config gives a route a structural build exclusion.
126
+
127
+ - Next's App Router has no build-time route exclusion, and needs none on its path, since `ImageResponse` is that framework's production mechanism and a card route there is meant to be served.
128
+ - `vite-react` builds a single-page app with no page router, so a card is a second Rollup entry rather than a route.
89
129
 
90
130
  ## Gitignore (extend)
91
131
 
92
- `[gitignore]` groups this stack edits, restated here per the manifest-to-reference symmetry:
132
+ These `[gitignore]` groups mirror the manifest.
93
133
 
94
134
  - `"# Astro" = [".astro/"]`
95
135
  - `"# Social card" = ["card-dist/"]`
96
136
 
97
137
  ## Scenario switcher
98
138
 
99
- - `src/components/dev/scenarios.astro` ships as a golden config, always overwritten on sync, since it is toolkit-authored infrastructure rather than a file a project hand-edits.
100
- - Import it into a page under test to drive candidate treatments of one decision by hand, selected by a query parameter, with a switcher for moving between them.
101
- - Guarded by `import.meta.env.DEV`, so it renders nothing and ships nothing in a production build.
102
- - Astro-only. It uses `is:inline`, `define:vars`, and `set:html`, which only the `.astro` file format parses. `vite-react` gets no equivalent until a real decision drives one.
139
+ `src/components/dev/scenarios.astro` ships as a golden config, always overwritten on sync, since it is toolkit-authored infrastructure rather than a file a project hand-edits.
140
+
141
+ - Import it into a page under test to drive candidate treatments of one decision by hand, selected by a query parameter, with a switcher between them.
142
+ - It is guarded by `import.meta.env.DEV`, so it renders nothing and ships nothing in a production build.
143
+ - It is Astro-only, using `is:inline`, `define:vars`, and `set:html`, which only the `.astro` format parses. `vite-react` gets no equivalent until a real decision drives one.
@@ -2,65 +2,86 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- The base layer covers every project the toolkit scaffolds, whatever language sits on top. It ships formatting, conventional commits, git hooks, CI, and the verify runner. Every other stack extends it, so a decision made here is one every stack inherits. Shell formatting and linting live in the `shell` add-on stack, which a project syncs once it writes shell.
5
+ The base layer covers every project the toolkit scaffolds, whatever language sits on top. It ships formatting, conventional commits, git hooks, CI, and the verify runner. Every other stack extends it, so a decision made here is one every stack inherits. Shell formatting and linting live in the `shell` add-on stack.
6
6
 
7
7
  A repository with several language roots layers this stack once at the root and skips it per subtree, since git honors only one `core.hooksPath` and a re-dropped husky silently breaks. `canon docs scenarios` carries the recipe.
8
8
 
9
9
  ## What ships as golden configs
10
10
 
11
- Golden config files live in `tooling/base/configs/` and are copied into the target on `canon tooling sync base . --write`, replacing whatever sits at those paths. They are the source of truth. The reference covers rationale and tradeoffs. Configs show the concrete setup.
11
+ Golden configs live in `tooling/base/configs/`. `canon tooling sync base . --write` copies them into the target and replaces whatever sits at those paths. They are the source of truth, so this reference covers rationale and the configs show the setup.
12
12
 
13
- - `.oxfmtrc.json`: `semi: false`, `singleQuote: true`, `printWidth: 80`, and `sortPackageJson: false`, which keep the output Prettier wrote. The `astro` stack repeats these options in its own copy, so copy a change here into it.
14
- - `commitlint.config.js`: ESM default export extending `@commitlint/config-conventional`. Rules are `header-max-length: 72`, `scope-case: lower-case`, `subject-full-stop: never`, `subject-case` disabled, and a local `no-claude-co-author` rule refusing a `Co-authored-by` trailer that names Claude or Anthropic.
15
- - `.husky/`: `pre-commit`, `commit-msg`, `pre-push`, `post-merge`, `post-rewrite`.
16
- - `scripts/verify.ts`: the one verify runner every stack shares, and the command behind `check`. It runs typecheck, lint, format check, shell, unit tests, build, markdown bans, then card exclusion, always in that order, and skips any phase the folder's `scripts/verify.json` leaves out. Node modules only, since a target's own lint and typecheck read it.
17
- - `scripts/verify.json`: the phases a stack requires, plus any tools and preconditions it probes first. The nearest stack's copy wins. A required phase whose script `package.json` lacks fails the run. A base phase the list leaves out runs only where the folder declares its script. Base requires `check:format`, so a base-only target still fails on a missing one. The shell phase runs only where `package.json` declares `check:shell`, which the `shell` stack adds, and probes for `shfmt` and `shellcheck` first. These two files are base's `[sync] per_root` set, which a `--skip base` subfolder still receives.
13
+ ### Formatting and commit configs
14
+
15
+ Two configs fix how output looks and how commits read.
16
+
17
+ - `.oxfmtrc.json` sets `semi: false`, `singleQuote: true`, `printWidth: 80`, and `sortPackageJson: false`, keeping the output Prettier wrote.
18
+ - The `astro` stack repeats those options in its own copy, so copy a change here into it.
19
+ - `commitlint.config.js` is an ESM default export extending `@commitlint/config-conventional`. It sets `header-max-length: 72`, `scope-case: lower-case`, and `subject-full-stop: never`.
20
+ - Its `subject-case` rule is off, and a local `no-claude-co-author` rule refuses a `Co-authored-by` trailer that names Claude or Anthropic.
21
+ - `.husky/` holds `pre-commit`, `commit-msg`, `pre-push`, `post-merge`, and `post-rewrite`.
22
+
23
+ ### Verify runner
24
+
25
+ `scripts/verify.ts` is the one verify runner every stack shares, and the command behind `check`. It uses Node modules only, since a target's own lint and typecheck read it.
26
+
27
+ - It runs typecheck, lint, format check, shell, unit tests, build, markdown bans, then card exclusion, always in that order, and skips any phase the folder's `scripts/verify.json` leaves out.
28
+ - `scripts/verify.json` lists the phases a stack requires, plus any tools and preconditions it probes first. The nearest stack's copy wins.
29
+ - A required phase whose script `package.json` lacks fails the run. A base phase the list leaves out runs only where the folder declares its script.
30
+ - Base requires `check:format`, so a base-only target still fails on a missing one.
31
+ - The shell phase runs only where `package.json` declares `check:shell`, which the `shell` stack adds, and probes for `shfmt` and `shellcheck` first.
32
+
33
+ These two files are base's `[sync] per_root` set, which a `--skip base` subfolder still receives.
18
34
 
19
35
  ## What ships as user-owned seeds
20
36
 
21
37
  Seeds live in `tooling/base/seeds/`. Sync drops each once on first install and never overwrites it, so a project extends them freely.
22
38
 
23
- - `.lintstagedrc`: the glob map below.
24
- - `.github/workflows/verify.yml`: runs on pull requests targeting `main` and on `workflow_dispatch`. A project with more than one language root adds its own jobs here.
25
- - `.github/pull_request_template.md`: `## Summary`, `## Key changes`, `## Technical context`, `## Testing`. A project adds the checklist lines its own review needs.
26
- - `canon/context/`: extend the `ci` and `development` entries with project-specific commands, workflows, or deploy steps. Canonical rationale stays in this reference.
39
+ - `.lintstagedrc` holds the glob map below.
40
+ - `.github/workflows/verify.yml` runs on pull requests targeting `main` and on `workflow_dispatch`. A multi-root project adds its own jobs here.
41
+ - `.github/pull_request_template.md` has `## Summary`, `## Key changes`, `## Technical context`, and `## Testing`. A project adds its own checklist lines.
42
+ - `canon/context/` holds the `ci` and `development` entries. Extend them with project-specific commands, workflows, or deploy steps, and keep canonical rationale in this reference.
27
43
 
28
44
  ## Tool pairing
29
45
 
30
- - Runtime: `bun` as package manager and script runner, `bunx` over `npx` for a one-off executable.
31
- - Formatting: oxfmt for what it parses, pinned to an exact version so an upgrade arrives as a reviewed diff. oxfmt has no shell parser, so shell formatting is the `shell` stack's.
32
- - Commits: commitlint against conventional commits, wired through the husky `commit-msg` hook. Format is `<type>(<scope>): <subject>` in imperative mood with no trailing period.
33
- - Dev dependencies: `oxfmt`, `husky`, `@commitlint/cli`, `@commitlint/config-conventional`. Install via `bun add -D`.
46
+ - Runtime is `bun` as package manager and script runner, with `bunx` over `npx` for a one-off executable.
47
+ - Formatting is oxfmt for what it parses, pinned to an exact version so an upgrade arrives as a reviewed diff. Shell formatting is the `shell` stack's, since oxfmt has no shell parser.
48
+ - Commits go through commitlint against conventional commits, wired through the husky `commit-msg` hook. The format is `<type>(<scope>): <subject>` in imperative mood with no trailing period.
49
+ - Dev dependencies are `oxfmt`, `husky`, `@commitlint/cli`, and `@commitlint/config-conventional`, installed via `bun add -D`.
34
50
 
35
51
  ## oxfmt
36
52
 
37
53
  - Add a project's own ignores to `.prettierignore`, which oxfmt reads beside `.gitignore` by default. Sync overwrites `.oxfmtrc.json`, so an entry in its `ignorePatterns` is lost on the next sync.
38
- - Delete a leftover `.prettierrc` and run `bun remove prettier` by hand after the first sync onto oxfmt. Sync deletes nothing a stack stopped shipping, and the script overrides already stop Prettier from running.
54
+ - After the first sync onto oxfmt, delete a leftover `.prettierrc` and run `bun remove prettier`. Sync deletes nothing a stack stopped shipping.
39
55
 
40
56
  ## File layout
41
57
 
42
- - Hooks live in `.husky/`, seeded context docs in `canon/context/`.
43
- - The `canon/context/` location matches the three-tier context model: project-wide invariants in `CLAUDE.md` and `canon/REQUIREMENTS.md`, path-scoped rules in `.claude/rules/`, and on-demand domain narrative in `canon/context/`. Indexes stay opt-in.
58
+ Hooks live in `.husky/` and seeded context docs in `canon/context/`. That location matches the three-tier context model.
59
+
60
+ - Project-wide invariants sit in `CLAUDE.md` and `canon/REQUIREMENTS.md`.
61
+ - Path-scoped rules sit in `.claude/rules/`.
62
+ - On-demand domain narrative sits in `canon/context/`, with indexes opt-in.
44
63
 
45
64
  ## Hooks
46
65
 
47
66
  - `pre-commit` runs `bunx lint-staged`.
48
67
  - `commit-msg` runs `bunx commitlint --edit "$1"`.
49
68
  - `pre-push` runs `bun run check`.
50
- - `post-merge` runs `canon hooks post-merge`: archive the task a merged pull request closed, push the records, reclaim merged worktrees, reinstall the CLI, and update the plugin. It does nothing when the board is absent. `CANON_SKIP_RECLAIM`, `CANON_SKIP_UPGRADE`, and `CANON_SKIP_PLUGIN_UPDATE` turn off the three steps that act on the machine. Run `canon hooks post-merge --help` for the contract.
51
69
  - `post-rewrite` delegates to `post-merge` on `rebase`, so a `pull.rebase=true` machine still gets the check.
70
+ - `post-merge` runs `canon hooks post-merge`, which archives the task a merged pull request closed, pushes the records, reclaims merged worktrees, reinstalls the CLI, and updates the plugin.
71
+
72
+ The hook does nothing when the board is absent. `CANON_SKIP_RECLAIM`, `CANON_SKIP_UPGRADE`, and `CANON_SKIP_PLUGIN_UPDATE` turn off the three steps that act on the machine, and `canon hooks post-merge --help` states the contract.
52
73
 
53
74
  ## lint-staged
54
75
 
55
- Seeded baseline globs:
76
+ The seeded baseline globs.
56
77
 
57
78
  - `**/*.{json,md}` runs `oxfmt --no-error-on-unmatched-pattern`. Without the flag, a staged file every pattern ignores, such as `CHANGELOG.md`, exits 2 and fails the commit.
58
- - `**/*.md` runs `canon indexes regen`
79
+ - `**/*.md` runs `canon indexes regen`.
59
80
 
60
81
  ## CI
61
82
 
62
- - Steps: checkout, setup Bun at latest, `bun install --frozen-lockfile`, then `check:format`.
63
- - CI asserts and never writes. Format must be clean before push.
83
+ - Steps are checkout, setup Bun at latest, `bun install --frozen-lockfile`, then `check:format`.
84
+ - CI asserts and never writes, so format must be clean before push.
64
85
 
65
86
  ## Gitignore
66
87
 
@@ -71,22 +92,32 @@ Seeded baseline globs:
71
92
 
72
93
  ## Anti-patterns
73
94
 
74
- Sticky negative knowledge. Do not relearn.
95
+ Negative knowledge that costs a failed run to relearn.
75
96
 
76
97
  - Do NOT pass `--ignore-path` to oxfmt. It replaces the default `.gitignore` and `.prettierignore` pair rather than adding to it.
77
98
  - Do NOT put logic in a husky hook and expect its shebang to hold. Husky runs hooks as `sh -e`, so a hook carrying logic is POSIX sh under errexit whatever the first line says.
78
99
 
79
100
  ## CLI
80
101
 
81
- | Script | What it does |
82
- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
83
- | `bun run check` | Gates a checkout through `scripts/verify.ts` and writes nothing. Honors `VERIFY_NESTED=true` to suppress timeline boundaries when another script calls it. |
84
- | `bun run check:format` | Asserts oxfmt formatting without writing |
85
- | `bun run format` | Writes oxfmt formatting in place |
86
- | `bun run prepare` | Initializes husky hooks, run automatically on `bun install` |
102
+ | Script | What it does |
103
+ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
104
+ | `bun run check` | Gates a checkout through `scripts/verify.ts`, writing nothing. `VERIFY_NESTED=true` suppresses timeline boundaries in a call. |
105
+ | `bun run check:format` | Asserts oxfmt formatting without writing |
106
+ | `bun run format` | Writes oxfmt formatting in place |
107
+ | `bun run prepare` | Initializes husky hooks, run automatically on `bun install` |
108
+
109
+ ### The check script
87
110
 
88
111
  Base writes `check` as a `[scripts.override]`, so a sync replaces whatever `check` a target declared, including one that chained further checks after the runner. Re-add a chained check as its own script after a sync.
89
112
 
90
- `bun run check` gates a checkout and writes nothing. Formatting drift fails it at `check:format`, and the failure line names `bun run format`, the one command that writes. It once ran `format` first to auto-fix drift, which rewrote files in a checkout several sessions share on every push and left a gate that could not fail. That was rejected, so the writing pass stays in `bun run format` and a push fails until it runs. The pull request workflow calls `check:format` directly as well.
113
+ - `bun run check` writes nothing. Formatting drift fails it at `check:format`, and the failure line names `bun run format`, the one command that writes.
114
+ - Auto-fixing drift inside `check` rewrote files in a shared checkout on every push and left a gate that could not fail, so the writing pass stays in `bun run format`.
115
+ - The pull request workflow calls `check:format` directly as well.
116
+
117
+ ### The markdown bans stage
118
+
119
+ The `Markdown bans` stage of `scripts/verify.ts` runs `canon markdown audit` over every tracked markdown file except `CHANGELOG.md`, since a generated changelog carries commit subjects nobody wrote against the ban set.
91
120
 
92
- `scripts/verify.ts`'s `Markdown bans` stage gates on banned characters. It runs `canon markdown audit` over every tracked markdown file except `CHANGELOG.md`, since a generated changelog carries commit subjects nobody wrote against the ban set, and exits `2` on a finding or `3` on a shipped-empty ban set, failing `bun run check` and the `pre-push` hook that calls it. A refusal to measure exits `1` and is logged as a skip rather than a failure, as does a corpus with no file left to check once `CHANGELOG.md` is set aside. The stage needs `canon` on PATH. `canon docs projects` names the install command. Without it, the stage is skipped and logged, and coverage falls back to the format check above.
121
+ - It exits `2` on a finding or `3` on a shipped-empty ban set, failing `bun run check` and the `pre-push` hook that calls it.
122
+ - A refusal to measure exits `1` and is logged as a skip, as does a corpus with no file left once `CHANGELOG.md` is set aside.
123
+ - The stage needs `canon` on PATH, and `canon docs projects` names the install command. Without it the stage is skipped and logged.
@@ -7,25 +7,17 @@ description: GitHub Actions workflow triggers and checks
7
7
 
8
8
  ## Overview
9
9
 
10
- Owns the GitHub Actions workflow that gates a merge: which events start a run, and which checks have to pass before the branch can land. The checks call package scripts rather than defining commands of their own, so what each one runs is the development entry's subject.
10
+ Owns the GitHub Actions workflow that gates a merge, being which events start a run and which checks have to pass before the branch can land. The checks call package scripts rather than defining commands of their own, so what each one runs is the development entry's subject.
11
11
 
12
12
  ## Layout
13
13
 
14
- - `.github/workflows/` owns the workflow definitions a trigger below starts
14
+ ### `.github/workflows/`
15
15
 
16
- ### Triggers
16
+ Holds `verify.yml`, the workflow that gates a merge. A pull request targeting `main` starts it, and so does a manual `workflow_dispatch` run from the Actions tab.
17
17
 
18
- - Pull requests targeting `main`
19
- - `workflow_dispatch` (manual run from the Actions tab)
18
+ - Every job must pass before merge
19
+ - The Format check runs `bun run check:format` and asserts oxfmt is clean
20
20
 
21
- ### Checks
21
+ ## Gotchas
22
22
 
23
- Defined in `.github/workflows/verify.yml`. All jobs must pass before merge.
24
-
25
- | Check | Command | What it asserts |
26
- | ------ | ---------------------- | --------------- |
27
- | Format | `bun run check:format` | oxfmt is clean |
28
-
29
- ### Running CI locally
30
-
31
- `bun run check` runs the same assert and writes nothing. If CI fails on format, run `bun run format` locally and commit the diff.
23
+ - `bun run check` runs the same assert as CI and writes nothing, so a CI format failure is fixed by running `bun run format` locally and committing the diff
@@ -7,31 +7,31 @@ description: Local dev workflow, scripts, and husky hooks
7
7
 
8
8
  ## Overview
9
9
 
10
- Owns how the project runs on a developer machine: installing the toolchain, the scripts that verify a change, and the git hooks that run them before a commit or a push leaves. CI calls the same scripts from a workflow, which is the CI entry's subject.
10
+ Owns how the project runs on a developer machine, being the toolchain install, the scripts that verify a change, and the git hooks that run them before a commit or a push leaves. CI calls the same scripts from a workflow, which is the CI entry's subject.
11
11
 
12
12
  ## Layout
13
13
 
14
- - `scripts/` owns the shell scripts the package scripts below call
15
- - `.husky/` owns the git hooks
14
+ ### `scripts/`
16
15
 
17
- ### Setup
16
+ Holds every shell script, since none belongs outside this folder. The package scripts below call them.
18
17
 
19
- - Install [Bun](https://bun.sh): `curl -fsSL https://bun.sh/install | bash`
20
- - Install dependencies: `bun install`
18
+ ### `.husky/`
19
+
20
+ Holds the git hooks that run the checks before a commit or a push leaves.
21
+
22
+ - `pre-commit` runs `lint-staged` (oxfmt on staged files)
23
+ - `commit-msg` runs `commitlint` against the conventional commit format
24
+ - `pre-push` runs `bun run check`, and also gates on banned characters across every tracked markdown file except `CHANGELOG.md` when a markdown-bans audit tool is on PATH
21
25
 
22
26
  ### Scripts
23
27
 
28
+ Install [Bun](https://bun.sh) with `curl -fsSL https://bun.sh/install | bash` and run `bun install`, then use these commands.
29
+
24
30
  | Command | Purpose |
25
31
  | ---------------- | ---------------------------------------------------------------------------------------- |
26
32
  | `bun run check` | Gates locally and writes nothing. Fails on formatting drift until `bun run format` runs. |
27
33
  | `bun run format` | Auto-fix oxfmt formatting. |
28
34
 
29
- ### Shell scripts
30
-
31
- All `.sh` files live under `scripts/`. Do not place shell scripts outside `scripts/`.
32
-
33
- ### Husky hooks
35
+ ## Gotchas
34
36
 
35
- - `pre-commit` runs `lint-staged` (oxfmt on staged files).
36
- - `commit-msg` runs `commitlint` against the conventional commit format.
37
- - `pre-push` runs `bun run check`. When a markdown-bans audit tool is on PATH, it also gates on banned characters across every tracked markdown file except `CHANGELOG.md`. A push fails on formatting drift until `bun run format` runs, since the hook writes nothing.
37
+ - A push fails on formatting drift until `bun run format` runs, since the `pre-push` hook writes nothing