@erclx/canon 5.7.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.
- package/README.md +4 -2
- package/claude/.claude-plugin/plugin.json +2 -2
- package/claude/skills/auto-ship/SKILL.md +2 -0
- package/claude/skills/draft-and-pick/REQUIREMENT.md +2 -1
- package/claude/skills/draft-and-pick/SKILL.md +9 -6
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-pr/references/evidence.md +1 -1
- package/claude/skills/git-ship/REQUIREMENT.md +3 -2
- package/claude/skills/git-ship/SKILL.md +25 -2
- package/claude/skills/review-address/SKILL.md +6 -4
- package/claude/skills/review-craft/REQUIREMENT.md +8 -2
- package/claude/skills/review-craft/SKILL.md +63 -7
- package/claude/skills/review-craft/references/adopted.md +49 -0
- package/docs/agents/commands.md +2 -0
- package/docs/agents/hooks.md +56 -0
- package/docs/agents/index.md +1 -0
- package/docs/agents/install-and-sync.md +1 -1
- package/docs/agents/markdown-audit.md +1 -1
- package/docs/agents/pr-evidence.md +33 -17
- package/docs/agents/records-push.md +1 -1
- package/docs/agents/records-validate.md +1 -1
- package/docs/agents/scripting.md +6 -0
- package/docs/agents/tasks-archive.md +4 -2
- package/docs/agents/tasks.md +1 -1
- package/docs/agents/worktrees.md +2 -2
- package/docs/target/scenarios.md +2 -2
- package/package.json +2 -2
- package/src/claude/skills-audit.ts +1 -0
- package/src/cli.ts +2 -0
- package/src/commands/claude.ts +1 -1
- package/src/commands/docs.ts +51 -11
- package/src/commands/hooks.ts +67 -0
- package/src/commands/pr.ts +146 -76
- package/src/commands/standards.ts +33 -2
- package/src/commands/tasks.ts +45 -0
- package/src/commands/tooling.ts +3 -3
- package/src/commands/upgrade.ts +3 -2
- package/src/commands/worktrees.ts +4 -3
- package/src/docs/list.ts +103 -0
- package/src/docs/read.ts +63 -19
- package/src/gov/stacks.ts +1 -1
- package/src/help.ts +2 -0
- package/src/hooks/post-merge.ts +331 -0
- package/src/pr/evidence.ts +21 -0
- package/src/records/validate.ts +1 -38
- package/src/standards/list.ts +33 -0
- package/src/standards/read.ts +13 -0
- package/src/standards/scope.ts +56 -0
- package/src/sync/check.ts +38 -4
- package/src/tasks/archive.ts +168 -3
- package/src/tooling/inject.ts +4 -3
- package/src/tooling/manifest.ts +62 -8
- package/src/tooling/scan.ts +4 -3
- package/src/tooling/stamp.ts +11 -2
- package/src/tooling/subfolder.ts +7 -3
- package/tooling/astro/reference.md +1 -1
- package/tooling/base/configs/scripts/verify.json +3 -0
- package/tooling/base/configs/scripts/verify.sh +1 -110
- package/tooling/base/configs/scripts/verify.ts +378 -0
- package/tooling/base/manifest.toml +1 -0
- package/tooling/base/reference.md +5 -3
- package/tooling/go/configs/scripts/verify.json +10 -0
- package/tooling/go/reference.md +5 -4
- package/tooling/php/configs/scripts/verify.json +7 -0
- package/tooling/php/reference.md +5 -4
- package/tooling/python/configs/scripts/verify.json +4 -0
- package/tooling/python/reference.md +3 -3
- package/tooling/web/configs/scripts/verify.json +3 -0
- package/tooling/web/reference.md +3 -3
- package/scripts/docs/list.sh +0 -184
- package/scripts/lib/frontmatter.sh +0 -30
- package/scripts/standards/list.sh +0 -115
- package/src/commands/pass-through.ts +0 -39
- package/tooling/go/configs/scripts/verify.sh +0 -142
- package/tooling/php/configs/scripts/verify.sh +0 -147
- package/tooling/python/configs/scripts/verify.sh +0 -141
- package/tooling/web/configs/scripts/verify.sh +0 -153
|
@@ -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
|
+
}
|
|
@@ -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.
|
|
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.
|
|
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.
|
package/tooling/go/reference.md
CHANGED
|
@@ -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
|
|
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.
|
|
29
|
+
## verify.json
|
|
30
30
|
|
|
31
|
-
-
|
|
32
|
-
-
|
|
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
|
package/tooling/php/reference.md
CHANGED
|
@@ -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
|
|
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.
|
|
42
|
+
## verify.json
|
|
43
43
|
|
|
44
|
-
-
|
|
45
|
-
-
|
|
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
|
|
|
@@ -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
|
|
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.
|
|
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.
|
|
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
|
|
package/tooling/web/reference.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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.
|