@erclx/canon 5.8.0 → 5.9.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 (70) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/git-followup/SKILL.md +1 -1
  3. package/claude/skills/git-pr/references/evidence.md +1 -1
  4. package/claude/skills/review-craft/REQUIREMENT.md +8 -2
  5. package/claude/skills/review-craft/SKILL.md +63 -7
  6. package/claude/skills/review-craft/references/adopted.md +49 -0
  7. package/docs/agents/commands.md +2 -0
  8. package/docs/agents/hooks.md +56 -0
  9. package/docs/agents/index.md +1 -0
  10. package/docs/agents/install-and-sync.md +1 -1
  11. package/docs/agents/markdown-audit.md +1 -1
  12. package/docs/agents/pr-evidence.md +33 -17
  13. package/docs/agents/records-push.md +1 -1
  14. package/docs/agents/records-validate.md +1 -1
  15. package/docs/agents/scripting.md +6 -0
  16. package/docs/agents/tasks-archive.md +4 -2
  17. package/docs/agents/tasks.md +1 -1
  18. package/docs/agents/worktrees.md +2 -2
  19. package/docs/target/scenarios.md +2 -2
  20. package/package.json +1 -1
  21. package/src/claude/skills-audit.ts +1 -0
  22. package/src/cli.ts +2 -0
  23. package/src/commands/claude.ts +1 -1
  24. package/src/commands/docs.ts +51 -11
  25. package/src/commands/hooks.ts +67 -0
  26. package/src/commands/pr.ts +146 -76
  27. package/src/commands/standards.ts +33 -2
  28. package/src/commands/tasks.ts +45 -0
  29. package/src/commands/tooling.ts +3 -3
  30. package/src/commands/upgrade.ts +3 -2
  31. package/src/commands/worktrees.ts +4 -3
  32. package/src/docs/list.ts +103 -0
  33. package/src/docs/read.ts +63 -19
  34. package/src/gov/stacks.ts +1 -1
  35. package/src/help.ts +2 -0
  36. package/src/hooks/post-merge.ts +331 -0
  37. package/src/pr/evidence.ts +21 -0
  38. package/src/records/validate.ts +1 -38
  39. package/src/standards/list.ts +33 -0
  40. package/src/standards/read.ts +13 -0
  41. package/src/standards/scope.ts +56 -0
  42. package/src/sync/check.ts +38 -4
  43. package/src/tasks/archive.ts +168 -3
  44. package/src/tooling/inject.ts +4 -3
  45. package/src/tooling/manifest.ts +62 -8
  46. package/src/tooling/scan.ts +4 -3
  47. package/src/tooling/stamp.ts +11 -2
  48. package/src/tooling/subfolder.ts +7 -3
  49. package/tooling/astro/reference.md +1 -1
  50. package/tooling/base/configs/scripts/verify.json +3 -0
  51. package/tooling/base/configs/scripts/verify.sh +1 -110
  52. package/tooling/base/configs/scripts/verify.ts +378 -0
  53. package/tooling/base/manifest.toml +1 -0
  54. package/tooling/base/reference.md +5 -3
  55. package/tooling/go/configs/scripts/verify.json +10 -0
  56. package/tooling/go/reference.md +5 -4
  57. package/tooling/php/configs/scripts/verify.json +7 -0
  58. package/tooling/php/reference.md +5 -4
  59. package/tooling/python/configs/scripts/verify.json +4 -0
  60. package/tooling/python/reference.md +3 -3
  61. package/tooling/web/configs/scripts/verify.json +3 -0
  62. package/tooling/web/reference.md +3 -3
  63. package/scripts/docs/list.sh +0 -184
  64. package/scripts/lib/frontmatter.sh +0 -30
  65. package/scripts/standards/list.sh +0 -115
  66. package/src/commands/pass-through.ts +0 -39
  67. package/tooling/go/configs/scripts/verify.sh +0 -142
  68. package/tooling/php/configs/scripts/verify.sh +0 -147
  69. package/tooling/python/configs/scripts/verify.sh +0 -141
  70. package/tooling/web/configs/scripts/verify.sh +0 -153
@@ -1,111 +1,2 @@
1
1
  #!/bin/bash
2
- set -e
3
- set -o pipefail
4
-
5
- GREEN='\033[0;32m'
6
- RED='\033[0;31m'
7
- WHITE='\033[1;37m'
8
- GREY='\033[0;90m'
9
- NC='\033[0m'
10
-
11
- NESTED="${VERIFY_NESTED:-false}"
12
-
13
- log_info() { echo -e "${GREY}│${NC} ${GREEN}✓${NC} $1"; }
14
- log_error() {
15
- echo -e "${GREY}│${NC} ${RED}✗${NC} $1"
16
- exit 1
17
- }
18
- log_step() { echo -e "${GREY}│${NC}\n${GREY}├${NC} ${WHITE}$1${NC}"; }
19
-
20
- pipe_output() { while IFS= read -r line; do echo -e "${GREY}│${NC} $line"; done; }
21
-
22
- check_dependencies() {
23
- command -v bun >/dev/null 2>&1 || log_error "bun is not installed"
24
- }
25
-
26
- check_markdown_bans() {
27
- if ! command -v canon >/dev/null 2>&1; then
28
- log_info "Skipped: no canon binary on PATH. Install with \`bun install --global @erclx/canon\`."
29
- return 0
30
- fi
31
-
32
- local files=()
33
- while IFS= read -r file; do
34
- files+=("$file")
35
- done < <(git ls-files --cached --others --exclude-standard -- '*.md' ':(exclude)CHANGELOG.md' ':(exclude)**/CHANGELOG.md')
36
-
37
- if [ ${#files[@]} -eq 0 ]; then
38
- log_info "No markdown file to check outside the exclusion set."
39
- return 0
40
- fi
41
-
42
- local output code
43
- if output=$(canon markdown audit "${files[@]}" 2>&1); then
44
- code=0
45
- else
46
- code=$?
47
- fi
48
-
49
- case "$code" in
50
- 0)
51
- log_info "No banned character"
52
- ;;
53
- 1)
54
- log_info "Skipped: canon markdown audit refused and measured nothing."
55
- ;;
56
- 2)
57
- echo "$output" | pipe_output
58
- log_error "Markdown prose carries a banned character, or a relative link resolves to nothing on disk"
59
- ;;
60
- 3)
61
- echo "$output" | pipe_output
62
- log_error "The markdown audit shipped an empty ban set, so the corpus was walked and nothing was looked for"
63
- ;;
64
- *)
65
- log_error "canon markdown audit exited $code, which is neither a pass nor a finding"
66
- ;;
67
- esac
68
- }
69
-
70
- run_check() {
71
- local cmd=$1
72
- local err_msg=$2
73
- local output
74
- if ! output=$(eval "$cmd" 2>&1); then
75
- echo "$output" | pipe_output
76
- log_error "$err_msg"
77
- fi
78
- echo "$output" | pipe_output
79
- }
80
-
81
- main() {
82
- check_dependencies
83
-
84
- if [ "$NESTED" = false ]; then echo -e "${GREY}┌${NC}"; fi
85
-
86
- echo -e "${GREY}├${NC} ${WHITE}Formatting${NC}"
87
- run_check "bun run format" "Format failed"
88
- log_info "Format applied"
89
-
90
- log_step "Format check"
91
- run_check "bun run check:format" "Format check failed"
92
- log_info "Format check passed"
93
-
94
- log_step "Spelling"
95
- run_check "bun run check:spell" "Spell check failed"
96
- log_info "Spell check passed"
97
-
98
- log_step "Shell"
99
- run_check "bun run check:shell" "Shell check failed"
100
- log_info "Shell check passed"
101
-
102
- log_step "Markdown bans"
103
- check_markdown_bans
104
-
105
- if [ "$NESTED" = false ]; then
106
- echo -e "${GREY}└${NC}\n"
107
- echo -e "${GREEN}✓ Verification passed${NC}"
108
- fi
109
- }
110
-
111
- main "$@"
2
+ exec bun "$(dirname "$0")/verify.ts" "$@"
@@ -0,0 +1,378 @@
1
+ // Runs this folder's verify phases in a fixed order. The phases a stack
2
+ // requires come from scripts/verify.json, which each stack ships and the
3
+ // nearest one wins. Only node: modules, since a target's own eslint and
4
+ // tsc --noEmit read this file with no Bun types.
5
+ import { spawnSync } from 'node:child_process'
6
+ import { accessSync, constants, existsSync, readFileSync } from 'node:fs'
7
+ import { delimiter, join } from 'node:path'
8
+
9
+ interface Tool {
10
+ readonly name: string
11
+ readonly hint?: string
12
+ }
13
+
14
+ interface Precondition {
15
+ readonly path: string
16
+ readonly hint: string
17
+ }
18
+
19
+ interface PhaseList {
20
+ readonly require: readonly string[]
21
+ readonly tools: readonly Tool[]
22
+ readonly preconditions: readonly Precondition[]
23
+ }
24
+
25
+ interface Phase {
26
+ readonly script: string
27
+ readonly title: string
28
+ readonly failed: string
29
+ readonly passed: string
30
+ }
31
+
32
+ const PHASES: readonly Phase[] = [
33
+ {
34
+ script: 'typecheck',
35
+ title: 'Typecheck',
36
+ failed: 'Typecheck failed',
37
+ passed: 'Typecheck passed',
38
+ },
39
+ {
40
+ script: 'lint',
41
+ title: 'Lint',
42
+ failed: 'Lint failed',
43
+ passed: 'Lint passed',
44
+ },
45
+ {
46
+ script: 'format',
47
+ title: 'Formatting',
48
+ failed: 'Format failed',
49
+ passed: 'Format applied',
50
+ },
51
+ {
52
+ script: 'check:format',
53
+ title: 'Format check',
54
+ failed: 'Format check failed',
55
+ passed: 'Format check passed',
56
+ },
57
+ {
58
+ script: 'check:spell',
59
+ title: 'Spelling',
60
+ failed: 'Spell check failed',
61
+ passed: 'Spell check passed',
62
+ },
63
+ {
64
+ script: 'check:shell',
65
+ title: 'Shell',
66
+ failed: 'Shell check failed',
67
+ passed: 'Shell check passed',
68
+ },
69
+ {
70
+ script: 'test:run',
71
+ title: 'Unit tests',
72
+ failed: 'Unit tests failed',
73
+ passed: 'Unit tests passed',
74
+ },
75
+ {
76
+ script: 'build',
77
+ title: 'Build',
78
+ failed: 'Build failed',
79
+ passed: 'Build passed',
80
+ },
81
+ ]
82
+
83
+ // A subfolder synced with --skip base declares none of these, since the
84
+ // repository root owns them, so a list that does not require one runs it only
85
+ // where the folder declares it.
86
+ const BASE_PHASES = new Set([
87
+ 'format',
88
+ 'check:format',
89
+ 'check:spell',
90
+ 'check:shell',
91
+ ])
92
+
93
+ const PHASE_LIST = 'scripts/verify.json'
94
+ const CARD_EXCLUSION = 'scripts/check-card-exclusion.sh'
95
+
96
+ const GREEN = '\x1b[0;32m'
97
+ const RED = '\x1b[0;31m'
98
+ const WHITE = '\x1b[1;37m'
99
+ const GREY = '\x1b[0;90m'
100
+ const NC = '\x1b[0m'
101
+
102
+ const isNested = (process.env.VERIFY_NESTED || 'false') !== 'false'
103
+ let hasStepped = false
104
+
105
+ class VerifyFailure extends Error {}
106
+
107
+ function print(line: string): void {
108
+ process.stdout.write(`${line}\n`)
109
+ }
110
+
111
+ function logInfo(message: string): void {
112
+ print(`${GREY}│${NC} ${GREEN}✓${NC} ${message}`)
113
+ }
114
+
115
+ function logSkip(message: string): void {
116
+ print(`${GREY}│ - ${message}${NC}`)
117
+ }
118
+
119
+ function fail(message: string): never {
120
+ print(`${GREY}│${NC} ${RED}✗${NC} ${message}`)
121
+ throw new VerifyFailure(message)
122
+ }
123
+
124
+ function logStep(title: string): void {
125
+ if (hasStepped) print(`${GREY}│${NC}`)
126
+ print(`${GREY}├${NC} ${WHITE}${title}${NC}`)
127
+ hasStepped = true
128
+ }
129
+
130
+ function pipeOutput(output: string): void {
131
+ for (const line of output.replace(/\n+$/, '').split('\n')) {
132
+ print(`${GREY}│${NC} ${line}`)
133
+ }
134
+ }
135
+
136
+ function isExecutable(path: string): boolean {
137
+ try {
138
+ accessSync(path, constants.X_OK)
139
+ return true
140
+ } catch {
141
+ return false
142
+ }
143
+ }
144
+
145
+ function onPath(name: string): boolean {
146
+ return (process.env.PATH ?? '')
147
+ .split(delimiter)
148
+ .some((dir) => dir !== '' && isExecutable(join(dir, name)))
149
+ }
150
+
151
+ // Runs through sh so stderr interleaves with stdout the way 2>&1 does.
152
+ function capture(
153
+ command: string,
154
+ args: readonly string[] = [],
155
+ ): {
156
+ readonly code: number
157
+ readonly output: string
158
+ } {
159
+ const result = spawnSync(
160
+ '/bin/sh',
161
+ ['-c', `${command} 2>&1`, 'sh', ...args],
162
+ {
163
+ encoding: 'utf8',
164
+ maxBuffer: 256 * 1024 * 1024,
165
+ },
166
+ )
167
+ return { code: result.status ?? 1, output: result.stdout ?? '' }
168
+ }
169
+
170
+ function runCheck(command: string, failed: string): void {
171
+ const { code, output } = capture(command)
172
+ pipeOutput(output)
173
+ if (code !== 0) fail(failed)
174
+ }
175
+
176
+ function readJson(path: string): unknown {
177
+ try {
178
+ return JSON.parse(readFileSync(path, 'utf8'))
179
+ } catch {
180
+ return undefined
181
+ }
182
+ }
183
+
184
+ function asRecord(value: unknown): Record<string, unknown> {
185
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
186
+ ? (value as Record<string, unknown>)
187
+ : {}
188
+ }
189
+
190
+ function asArray(value: unknown): unknown[] {
191
+ return Array.isArray(value) ? value : []
192
+ }
193
+
194
+ function readPhaseList(): PhaseList {
195
+ if (!existsSync(PHASE_LIST)) {
196
+ fail(
197
+ `No ${PHASE_LIST} here. Run 'canon tooling sync <stack>' so the stack ships its phase list`,
198
+ )
199
+ }
200
+ const parsed = readJson(PHASE_LIST)
201
+ if (parsed === undefined) fail(`${PHASE_LIST} is not valid JSON`)
202
+
203
+ const table = asRecord(parsed)
204
+ return {
205
+ require: asArray(table.require).filter(
206
+ (entry): entry is string => typeof entry === 'string',
207
+ ),
208
+ tools: asArray(table.tools).flatMap((entry) => {
209
+ const tool = asRecord(entry)
210
+ if (typeof tool.name !== 'string') return []
211
+ return [
212
+ {
213
+ name: tool.name,
214
+ hint: typeof tool.hint === 'string' ? tool.hint : undefined,
215
+ },
216
+ ]
217
+ }),
218
+ preconditions: asArray(table.preconditions).flatMap((entry) => {
219
+ const precondition = asRecord(entry)
220
+ if (typeof precondition.path !== 'string') return []
221
+ return [
222
+ {
223
+ path: precondition.path,
224
+ hint: typeof precondition.hint === 'string' ? precondition.hint : '',
225
+ },
226
+ ]
227
+ }),
228
+ }
229
+ }
230
+
231
+ function readDeclaredScripts(): Set<string> {
232
+ if (!existsSync('package.json')) {
233
+ fail("No package.json here. Run 'bun init', then sync the stack again")
234
+ }
235
+ return new Set(
236
+ Object.keys(asRecord(asRecord(readJson('package.json')).scripts)),
237
+ )
238
+ }
239
+
240
+ function checkTools(list: PhaseList): void {
241
+ for (const tool of list.tools) {
242
+ if (onPath(tool.name)) continue
243
+ fail(
244
+ tool.hint === undefined
245
+ ? `${tool.name} is not installed`
246
+ : `${tool.name} is not installed. ${tool.hint}`,
247
+ )
248
+ }
249
+ }
250
+
251
+ function checkPreconditions(list: PhaseList): void {
252
+ for (const precondition of list.preconditions) {
253
+ if (isExecutable(precondition.path)) continue
254
+ fail(`No ${precondition.path} here. ${precondition.hint}`.trimEnd())
255
+ }
256
+ }
257
+
258
+ function runPhase(
259
+ phase: Phase,
260
+ required: boolean,
261
+ declared: Set<string>,
262
+ ): void {
263
+ const isBase = BASE_PHASES.has(phase.script)
264
+ if (!required && !isBase) return
265
+
266
+ logStep(phase.title)
267
+ if (!declared.has(phase.script)) {
268
+ if (required) {
269
+ fail(
270
+ `${phase.script} is required by ${PHASE_LIST}, but package.json declares no such script`,
271
+ )
272
+ }
273
+ logSkip(
274
+ `Skipped: ${phase.script} is not declared here, so the repository root runs it`,
275
+ )
276
+ return
277
+ }
278
+ runCheck(`bun run ${phase.script}`, phase.failed)
279
+ logInfo(phase.passed)
280
+ }
281
+
282
+ function listMarkdown(): string[] {
283
+ const result = spawnSync(
284
+ 'git',
285
+ [
286
+ 'ls-files',
287
+ '--cached',
288
+ '--others',
289
+ '--exclude-standard',
290
+ '--',
291
+ '*.md',
292
+ ':(exclude)CHANGELOG.md',
293
+ ':(exclude)**/CHANGELOG.md',
294
+ ],
295
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] },
296
+ )
297
+ return (result.stdout ?? '').split('\n').filter((line) => line !== '')
298
+ }
299
+
300
+ // canon markdown audit exits 0 on a pass, 1 on a refusal that measured
301
+ // nothing, 2 on a finding, and 3 when it shipped an empty ban set.
302
+ function checkMarkdownBans(): void {
303
+ if (!onPath('canon')) {
304
+ logInfo(
305
+ 'Skipped: no canon binary on PATH. Install with `bun install --global @erclx/canon`.',
306
+ )
307
+ return
308
+ }
309
+
310
+ const files = listMarkdown()
311
+ if (files.length === 0) {
312
+ logInfo('No markdown file to check outside the exclusion set.')
313
+ return
314
+ }
315
+
316
+ const { code, output } = capture('canon markdown audit "$@"', files)
317
+ switch (code) {
318
+ case 0:
319
+ logInfo('No banned character')
320
+ return
321
+ case 1:
322
+ logInfo('Skipped: canon markdown audit refused and measured nothing.')
323
+ return
324
+ case 2:
325
+ pipeOutput(output)
326
+ fail(
327
+ 'Markdown prose carries a banned character, or a relative link resolves to nothing on disk',
328
+ )
329
+ break
330
+ case 3:
331
+ pipeOutput(output)
332
+ fail(
333
+ 'The markdown audit shipped an empty ban set, so the corpus was walked and nothing was looked for',
334
+ )
335
+ break
336
+ default:
337
+ fail(
338
+ `canon markdown audit exited ${code}, which is neither a pass nor a finding`,
339
+ )
340
+ }
341
+ }
342
+
343
+ function main(): void {
344
+ const declared = readDeclaredScripts()
345
+ const list = readPhaseList()
346
+ checkTools(list)
347
+ checkPreconditions(list)
348
+
349
+ if (!isNested) print(`${GREY}┌${NC}`)
350
+
351
+ const required = new Set(list.require)
352
+ for (const phase of PHASES) {
353
+ runPhase(phase, required.has(phase.script), declared)
354
+ }
355
+
356
+ logStep('Markdown bans')
357
+ checkMarkdownBans()
358
+
359
+ // The card exclusion check reads the build, so it runs last, and only where
360
+ // the stack that owns the card route shipped it.
361
+ if (existsSync(CARD_EXCLUSION)) {
362
+ logStep('Card exclusion')
363
+ runCheck(`bash ${CARD_EXCLUSION}`, 'Card exclusion check failed')
364
+ logInfo('Card exclusion passed')
365
+ }
366
+
367
+ if (!isNested) {
368
+ print(`${GREY}└${NC}\n`)
369
+ print(`${GREEN}✓ Verification passed${NC}`)
370
+ }
371
+ }
372
+
373
+ try {
374
+ main()
375
+ } catch (error) {
376
+ if (!(error instanceof VerifyFailure)) throw error
377
+ process.exitCode = 1
378
+ }
@@ -6,6 +6,7 @@ scaffold = ""
6
6
 
7
7
  [sync]
8
8
  source = "configs"
9
+ per_root = ["scripts/verify.sh", "scripts/verify.ts", "scripts/verify.json"]
9
10
 
10
11
  [dependencies.dev]
11
12
  packages = [
@@ -16,7 +16,9 @@ Golden config files live in `tooling/base/configs/` and are copied into the targ
16
16
  - `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.
17
17
  - `.husky/`: `pre-commit`, `commit-msg`, `pre-push`, `post-merge`, `post-rewrite`.
18
18
  - `.vscode/extensions.json` and `.vscode/settings.json`: editor wiring for Prettier, cspell, shfmt, and shellcheck.
19
- - `scripts/verify.sh`: the maintenance entry point behind `check`.
19
+ - `scripts/verify.sh`: the maintenance entry point behind `check`, a wrapper that hands off to `scripts/verify.ts`.
20
+ - `scripts/verify.ts`: the one verify runner every stack shares. It runs typecheck, lint, format, format check, spelling, 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.
21
+ - `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 its four phases, so a base-only target still fails on a missing one. These three files are base's `[sync] per_root` set, which a `--skip base` subfolder still receives.
20
22
 
21
23
  ## What ships as user-owned seeds
22
24
 
@@ -88,7 +90,7 @@ Sticky negative knowledge. Do not relearn.
88
90
 
89
91
  | Script | What it does |
90
92
  | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
91
- | `bun run check` | Repairs a checkout via `scripts/verify.sh`. Honors `VERIFY_NESTED=true` to suppress timeline boundaries when another script calls it. |
93
+ | `bun run check` | Repairs a checkout via `scripts/verify.ts`. Honors `VERIFY_NESTED=true` to suppress timeline boundaries when another script calls it. |
92
94
  | `bun run check:format` | Asserts prettier and shfmt formatting without writing |
93
95
  | `bun run check:spell` | Runs cspell across every file, with context on failures |
94
96
  | `bun run check:shell` | Runs shellcheck at warning severity |
@@ -97,4 +99,4 @@ Sticky negative knowledge. Do not relearn.
97
99
 
98
100
  `bun run check` repairs a checkout rather than gating one. It runs `format` first to auto-fix drifted code, then asserts only what the formatters could not fix. The pull request workflow calls `check:format`, `check:spell`, and `check:shell` directly, and those three are the gate.
99
101
 
100
- `scripts/verify.sh`'s `Markdown bans` stage is the exception: it gates rather than repairs. 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 three checks above.
102
+ `scripts/verify.ts`'s `Markdown bans` stage is the exception: it gates rather than repairs. 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 three checks above.
@@ -0,0 +1,10 @@
1
+ {
2
+ "require": ["typecheck", "lint", "test:run"],
3
+ "tools": [
4
+ { "name": "go" },
5
+ {
6
+ "name": "golangci-lint",
7
+ "hint": "Run 'go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest'"
8
+ }
9
+ ]
10
+ }
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Overview
6
6
 
7
- The go stack covers Go modules linted and formatted by `golangci-lint` v2, type-checked by `go vet`, and tested by `go test`. It ships a golden `.golangci.yml`, a Go-aware `verify.sh`, and a copy-once smoke test.
7
+ The go stack covers Go modules linted and formatted by `golangci-lint` v2, type-checked by `go vet`, and tested by `go test`. It ships a golden `.golangci.yml`, a go phase list for base's verify runner, and a copy-once smoke test.
8
8
 
9
9
  ## Scaffold checklist
10
10
 
@@ -26,10 +26,11 @@ The go stack also ships `smoke_test.go` at the module root as a copy-once seed.
26
26
  - Enable formatters `gofmt` and `goimports`. `lint:fix` runs `golangci-lint fmt` ahead of `golangci-lint run --fix`.
27
27
  - Exclude `node_modules` from both linters and formatters. The `go.mod` `ignore` directive in the checklist covers every Go tool, but `go.mod` belongs to the project and sync cannot write it, so the config keeps `golangci-lint run` from reporting and `golangci-lint fmt` from rewriting a vendored `.go` file in a module that never added the directive.
28
28
 
29
- ## verify.sh
29
+ ## verify.json
30
30
 
31
- - Run Typecheck (`go vet`), Lint (`golangci-lint run`), and Unit tests (`go test -v`), plus the four base phases wherever the folder declares their scripts, then the markdown bans stage base ships.
32
- - Keep `build` out of `verify.sh`. `go build ./...` exits 1 with `no packages to build` on a module holding only a test file, and a seeded `main.go` would assume every module is a binary rather than a library.
31
+ - Require Typecheck (`go vet`), Lint (`golangci-lint run`), and Unit tests (`go test -v`). Base's runner adds the four base phases wherever the folder declares their scripts, then the markdown bans stage.
32
+ - Probe `go` and `golangci-lint` first, and name the `go install` command when `golangci-lint` is missing.
33
+ - Keep `build` out of `verify.json`, even though the stack declares the script. `go build ./...` exits 1 with `no packages to build` on a module holding only a test file, and a seeded `main.go` would assume every module is a binary rather than a library.
33
34
  - Keep `-race` out of `test:run`. The race detector needs cgo and a C toolchain, so it belongs in CI rather than in a local `bun run check`.
34
35
 
35
36
  ## CI
@@ -0,0 +1,7 @@
1
+ {
2
+ "require": ["typecheck", "lint", "test:run"],
3
+ "tools": [{ "name": "php" }, { "name": "composer" }],
4
+ "preconditions": [
5
+ { "path": "vendor/bin/phpunit", "hint": "Run 'composer install'" }
6
+ ]
7
+ }
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Overview
6
6
 
7
- The php stack covers Composer projects on PHP 8.3 or later, formatted by PHP CS Fixer, analysed by PHPStan, and tested by PHPUnit. It ships the three tools' configs, a PHP-aware `verify.sh`, and a copy-once smoke test.
7
+ The php stack covers Composer projects on PHP 8.3 or later, formatted by PHP CS Fixer, analysed by PHPStan, and tested by PHPUnit. It ships the three tools' configs, a php phase list for base's verify runner, and a copy-once smoke test.
8
8
 
9
9
  ## Scaffold checklist
10
10
 
@@ -39,10 +39,11 @@ The php stack also ships `tests/SmokeTest.php` as a copy-once seed, so `bun run
39
39
  - Set `colors`, `failOnWarning`, and `failOnRisky`.
40
40
  - Run one `unit` suite over `tests`, with `src` as the source.
41
41
 
42
- ## verify.sh
42
+ ## verify.json
43
43
 
44
- - Stop and name `composer install` when `vendor/bin/phpunit` is missing.
45
- - Run Typecheck (`phpstan analyse`), Lint (`php-cs-fixer check`), and Unit tests (`phpunit --testdox`), plus the four base phases wherever the folder declares their scripts, then the markdown bans stage base ships.
44
+ - Probe `php` and `composer` first.
45
+ - Hold a precondition on `vendor/bin/phpunit`, so the run stops and names `composer install` when it is missing.
46
+ - Require Typecheck (`phpstan analyse`), Lint (`php-cs-fixer check`), and Unit tests (`phpunit --testdox`). Base's runner adds the four base phases wherever the folder declares their scripts, then the markdown bans stage.
46
47
 
47
48
  ## Gitignore
48
49
 
@@ -0,0 +1,4 @@
1
+ {
2
+ "require": ["typecheck", "lint", "test:run"],
3
+ "tools": [{ "name": "uv" }]
4
+ }
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Overview
6
6
 
7
- The python stack covers Python 3.13+ projects managed with `uv`. It ships golden sidecar configs for `ruff`, `mypy`, `pytest`, and `coverage`, plus a `.python-version` pin and a python-aware `verify.sh`. Framework adapters layer on top with their own deps and configs.
7
+ The python stack covers Python 3.13+ projects managed with `uv`. It ships golden sidecar configs for `ruff`, `mypy`, `pytest`, and `coverage`, plus a `.python-version` pin and a python phase list for base's verify runner. Framework adapters layer on top with their own deps and configs.
8
8
 
9
9
  Configs ship as sidecar files (`ruff.toml`, `mypy.ini`, `pytest.ini`, `.coveragerc`) rather than `[tool.*]` sections in `pyproject.toml`. Sync overwrites configs on every run, so keeping them sidecar avoids stomping the user-owned `[project]` block in `pyproject.toml`.
10
10
 
@@ -27,7 +27,7 @@ The python stack also ships `tests/test_smoke.py` as a copy-once seed. `pytest`
27
27
  - `pytest.ini`: tests live under `tests/`, source under `src/`.
28
28
  - `.coveragerc`: branch coverage on `src/`, html report under `.coverage_cache/html`.
29
29
  - `.python-version`: pinned to `3.14` to match `uv init` defaults.
30
- - `scripts/verify.sh`: overrides base verify to add Typecheck (`mypy`), Lint (`ruff check && ruff format --check`), and Tests (`pytest -v`) phases, then the same markdown bans phase base ships.
30
+ - `scripts/verify.json`: requires Typecheck (`mypy`), Lint (`ruff check && ruff format --check`), and Tests (`pytest -v`) on top of base's runner, and stops naming `uv` when it is not on PATH. The markdown bans phase runs as it does everywhere.
31
31
 
32
32
  ## Hybrid project shape
33
33
 
@@ -39,7 +39,7 @@ The manifest declares no `[dependencies.dev]`. Manifest injection currently call
39
39
 
40
40
  ## Verify command
41
41
 
42
- `canon tooling verify <stack>` currently runs `bun run lint:fix` and `bun run check` against any stack with a `package.json`. Since base ships a `package.json`, those run for python too. Python's verify.sh wraps the package.json `lint`/`typecheck`/`test:run` scripts that delegate to `uv run`, so the verify path works end-to-end as long as `uv` is installed in the verify environment. The end-to-end test and screenshot phases are gated on `package.json` script keys that python does not declare, so they cleanly skip.
42
+ `canon tooling verify <stack>` currently runs `bun run lint:fix` and `bun run check` against any stack with a `package.json`. Since base ships a `package.json`, those run for python too. Python's `verify.json` requires the package.json `lint`/`typecheck`/`test:run` scripts that delegate to `uv run`, so the verify path works end-to-end as long as `uv` is installed in the verify environment. The end-to-end test and screenshot phases are gated on `package.json` script keys that python does not declare, so they cleanly skip.
43
43
 
44
44
  ## Anti-patterns
45
45
 
@@ -0,0 +1,3 @@
1
+ {
2
+ "require": ["typecheck", "lint", "test:run", "build"]
3
+ }
@@ -19,7 +19,7 @@ Golden config files live in `tooling/web/configs/` and are copied into the targe
19
19
  - `.github/workflows/readme-screenshot.yml`: refreshes that frame on a pull request and commits it back to the branch. The path list ships as `REPLACE_WITH_PROJECT_PATHS`, and the job's first step after checkout exits 1 while it stays, since a job that never fires leaves no run and no diff to notice. The workflow's own path is in the list so the installing pull request reaches that step. When a commit-back is right and when a report is, `governance/rules/tooling/700-ci-workflow.md` states the test.
20
20
  - `.vscode/extensions.json` and `.vscode/settings.json`: editor wiring for ESLint, Tailwind, Playwright, Vitest.
21
21
  - `.github/workflows/verify.yml`: `static-checks`, `unit-tests`, `build-verify`, and `e2e-tests` jobs.
22
- - `scripts/verify.sh`: extends base verify with typecheck, lint, unit tests, and build in the full order. Its last stage runs `scripts/check-card-exclusion.sh` whenever that file is installed, which puts the social card route's build exclusion on the default path rather than behind a flag nobody passes. The stage tests for the script rather than for a framework, so this layer owns when the check runs and the stack shipping a card route owns what it reads. The astro reference carries the route itself.
22
+ - `scripts/verify.json`: the phases base's verify runner requires here, being typecheck, lint, unit tests, and build. `## Verify script` covers the order and the card exclusion stage.
23
23
  - `scripts/worktree-port.sh`: prints a base port plus this working directory's offset. Called with no argument it prints the offset alone. It refuses a folder left under the worktrees directory after its worktree was removed, rather than printing a port for it.
24
24
 
25
25
  ## What stays in per-stack adapters
@@ -128,6 +128,6 @@ The sweep under `screenshots/` is ignored again, and only a flagged case's `evid
128
128
 
129
129
  ## Verify script
130
130
 
131
- The web layer's `scripts/verify.sh` replaces the base version. Order: typecheck, lint, format, spelling, shell, unit tests, build, markdown bans, then the card exclusion check where one is installed. Stack adapters may override if their typecheck or build differs.
131
+ The web layer's `scripts/verify.json` requires `typecheck`, `lint`, `test:run`, and `build`, and base's `scripts/verify.ts` runs them in its fixed order: typecheck, lint, format, spelling, shell, unit tests, build, markdown bans, then the card exclusion check where one is installed. astro, vite-react, and nextjs inherit the list, since the nearest stack's copy wins. An adapter whose phases differ ships its own `verify.json` rather than a script.
132
132
 
133
- The card stage is conditional on `scripts/check-card-exclusion.sh` existing rather than on the stack, so a project with no card route runs one less stage and needs no override to skip it. A stack that ships a card route ships that script, and the check reads the build the stage before it produced.
133
+ The card stage runs `scripts/check-card-exclusion.sh` whenever that file is installed, which keeps the social card route's build exclusion on the default path rather than behind a flag. It tests for the script rather than the stack, so a project with no card route runs one less stage with no override, and the stack shipping a card route owns what the check reads, being the build the stage before it produced. The astro reference carries the route itself.