@erclx/canon 5.36.0 → 5.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +6 -0
- package/claude/skills/auto-ship/SKILL.md +1 -1
- package/claude/skills/backlog-triage/SKILL.md +8 -8
- package/claude/skills/build-in-slices/SKILL.md +7 -1
- package/claude/skills/build-in-slices/references/adopted.md +3 -0
- package/claude/skills/canon-cli/SKILL.md +3 -0
- package/claude/skills/canon-feedback/SKILL.md +5 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +3 -0
- package/claude/skills/canvas-board/SKILL.md +3 -0
- package/claude/skills/canvas-board/references/canvas.md +12 -0
- package/claude/skills/ci-workflow/SKILL.md +5 -0
- package/claude/skills/code-craft/SKILL.md +6 -0
- package/claude/skills/context-fold/references/context.md +48 -34
- package/claude/skills/create-rule/SKILL.md +11 -9
- package/claude/skills/create-rule/references/rule.md +6 -0
- package/claude/skills/create-skill/SKILL.md +11 -2
- package/claude/skills/create-skill/references/skill-disclosure.md +26 -0
- package/claude/skills/create-skill/references/skill-paths.md +3 -0
- package/claude/skills/create-skill/references/skill-reference.md +12 -0
- package/claude/skills/create-skill/references/skill.md +25 -24
- package/claude/skills/deprecation-migration/SKILL.md +6 -0
- package/claude/skills/deprecation-migration/references/adopted.md +3 -0
- package/claude/skills/design-taste/SKILL.md +6 -0
- package/claude/skills/design-taste/references/craft.md +3 -0
- package/claude/skills/design-taste/references/preflight.md +16 -0
- package/claude/skills/docs-sync/references/docs.md +6 -0
- package/claude/skills/docs-sync/references/readme.md +15 -0
- package/claude/skills/draft-and-pick/SKILL.md +12 -0
- package/claude/skills/draft-doc/references/context.md +1 -1
- package/claude/skills/draft-doc/references/readme.md +3 -0
- package/claude/skills/draft-figure/references/mermaid.md +6 -0
- package/claude/skills/draft-slides/SKILL.md +3 -0
- package/claude/skills/git-branch/references/branch.md +16 -11
- package/claude/skills/git-commit/references/commit.md +19 -11
- package/claude/skills/git-commit/references/versioning.md +9 -0
- package/claude/skills/git-issue/references/issue.md +13 -9
- package/claude/skills/git-pr/SKILL.md +5 -0
- package/claude/skills/git-pr/references/pr.md +24 -6
- package/claude/skills/git-ship/SKILL.md +3 -0
- package/claude/skills/git-split/SKILL.md +6 -0
- package/claude/skills/git-stage/SKILL.md +3 -0
- package/claude/skills/git-worktree/SKILL.md +6 -6
- package/claude/skills/markdown-craft/references/markdown.md +25 -6
- package/claude/skills/plan-feature/SKILL.md +9 -0
- package/claude/skills/plan-feature/references/plan.md +20 -8
- package/claude/skills/plan-groundwork/SKILL.md +6 -0
- package/claude/skills/plan-groundwork/references/groundwork.md +61 -29
- package/claude/skills/plan-intake/references/intake.md +6 -6
- package/claude/skills/read-frames/SKILL.md +3 -0
- package/claude/skills/review-craft/SKILL.md +3 -0
- package/claude/skills/review-pr/references/post.md +1 -1
- package/claude/skills/review-ui/SKILL.md +6 -3
- package/claude/skills/role-orchestrator/SKILL.md +4 -1
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +7 -4
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +3 -0
- package/claude/skills/role-planner/SKILL.md +4 -1
- package/claude/skills/role-worker/SKILL.md +13 -1
- package/claude/skills/search-craft/SKILL.md +3 -0
- package/claude/skills/systematic-debugging/SKILL.md +6 -0
- package/claude/skills/target-check/references/domains.md +5 -0
- package/claude/skills/target-setup/SKILL.md +8 -4
- package/claude/skills/target-setup/references/verify.md +6 -5
- package/claude/skills/task-board/references/tasks.md +12 -7
- package/claude/skills/task-complete/references/archive.md +3 -0
- package/claude/skills/teach-workspace/SKILL.md +3 -0
- package/claude/skills/teach-workspace/references/glossary.md +3 -0
- package/claude/skills/teach-workspace/references/lesson-build.md +3 -0
- package/claude/skills/teach-workspace/references/lesson-craft.md +3 -0
- package/claude/skills/teach-workspace/references/teach.md +8 -8
- package/claude/skills/test-craft/SKILL.md +5 -0
- package/claude/skills/video-craft/SKILL.md +6 -0
- package/claude/skills/write-human/SKILL.md +9 -0
- package/docs/agents/capture.md +2 -0
- package/docs/agents/commands.md +54 -54
- package/docs/agents/context-audit-checks.md +6 -0
- package/docs/agents/context-audit.md +8 -8
- package/docs/agents/context-classify.md +2 -0
- package/docs/agents/drift-surfaces.md +5 -5
- package/docs/agents/gate.md +6 -7
- package/docs/agents/index.md +1 -1
- package/docs/agents/install-and-sync.md +2 -2
- package/docs/agents/markdown-audit-weight.md +11 -3
- package/docs/agents/markdown-audit.md +2 -2
- package/docs/agents/pr-frames.md +17 -3
- package/docs/agents/sessions-transfer.md +8 -6
- package/docs/agents/skills-audit.md +9 -1
- package/docs/target/projects.md +1 -1
- package/docs/target/scenarios.md +2 -2
- package/docs/target/standards-moved.md +20 -18
- package/docs/workflow/ai-workflow.md +3 -0
- package/docs/workflow/visual-design-workflow.md +5 -0
- package/governance/detect.toml +6 -1
- package/package.json +7 -4
- package/scripts/core/install-check.sh +1 -1
- package/src/cli/help.ts +1 -1
- package/src/commands/claude.ts +0 -1
- package/src/commands/context.ts +29 -4
- package/src/commands/gate.ts +1 -8
- package/src/commands/markdown.ts +52 -0
- package/src/commands/pr.ts +18 -11
- package/src/commands/records.ts +0 -1
- package/src/context/audit.ts +83 -0
- package/src/context/classify/prompts.ts +6 -1
- package/src/context/gate.ts +6 -3
- package/src/docs/read.ts +15 -21
- package/src/gate/measures.ts +186 -1
- package/src/gate/sequencer.ts +0 -4
- package/src/gate/stages.ts +46 -27
- package/src/git/files.ts +30 -0
- package/src/gov/adapter.ts +2 -2
- package/src/gov/install.ts +2 -2
- package/src/gov/list.ts +1 -1
- package/src/gov/payload.ts +1 -1
- package/src/gov/stacks.ts +1 -1
- package/src/markdown/gate.ts +10 -0
- package/src/markdown/growth.ts +43 -0
- package/src/markdown/stacks.ts +74 -0
- package/src/markdown/structure.ts +73 -5
- package/src/migrate/rule-layout.ts +12 -12
- package/src/pr/frames.ts +66 -1
- package/src/sandbox/equivalence.ts +7 -9
- package/src/sync/check.ts +23 -23
- package/src/teach/browser/head.ts +2 -2
- package/src/teach/browser/sidebar.ts +3 -3
- package/src/teach/browser/theme.ts +2 -2
- package/src/tooling/inject.ts +2 -2
- package/src/tooling/package.ts +15 -18
- package/src/tooling/verify-stack.ts +6 -3
- package/tooling/astro/configs/.oxfmtrc.json +8 -0
- package/tooling/astro/configs/.prettierrc +13 -0
- package/tooling/astro/configs/tsconfig.json +1 -1
- package/tooling/astro/manifest.toml +13 -0
- package/tooling/astro/reference.md +17 -7
- package/tooling/base/configs/.oxfmtrc.json +7 -0
- package/tooling/base/configs/scripts/verify.json +1 -1
- package/tooling/base/configs/scripts/verify.ts +3 -9
- package/tooling/base/manifest.toml +6 -6
- package/tooling/base/reference.md +20 -17
- package/tooling/base/seeds/.lintstagedrc +1 -3
- package/tooling/base/seeds/canon/context/ci.md +7 -7
- package/tooling/base/seeds/canon/context/development.md +10 -10
- package/tooling/claude/manifest.toml +5 -1
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +8 -0
- package/tooling/go/reference.md +2 -2
- package/tooling/nextjs/configs/.oxlintrc.json +223 -0
- package/tooling/nextjs/manifest.toml +2 -5
- package/tooling/nextjs/reference.md +4 -4
- package/tooling/php/manifest.toml +6 -1
- package/tooling/php/reference.md +2 -2
- package/tooling/python/manifest.toml +6 -1
- package/tooling/python/reference.md +4 -4
- package/tooling/vite-react/configs/tsconfig.json +1 -1
- package/tooling/vite-react/configs/vitest.config.ts +1 -1
- package/tooling/vite-react/manifest.toml +1 -4
- package/tooling/vite-react/reference.md +5 -5
- package/tooling/web/configs/.oxlintrc.json +213 -0
- package/tooling/web/configs/src/test/setup.ts +1 -1
- package/tooling/web/manifest.toml +12 -16
- package/tooling/web/reference.md +15 -15
- package/tooling/base/configs/.prettierrc +0 -12
- package/tooling/base/seeds/.prettierignore +0 -0
- package/tooling/nextjs/configs/eslint.config.js +0 -91
- package/tooling/web/configs/eslint.config.js +0 -82
package/src/sync/check.ts
CHANGED
|
@@ -531,12 +531,12 @@ export function parseNewSkills(paths: string): string[] {
|
|
|
531
531
|
|
|
532
532
|
/**
|
|
533
533
|
* What the target's `.claude/rules/` proves about its entitlement. `held` is
|
|
534
|
-
* every rule name it carries and `
|
|
534
|
+
* every rule name it carries and `folders` every rule folder those names sit in.
|
|
535
535
|
*
|
|
536
536
|
* Read off the installed tree rather than off a stack name, because
|
|
537
537
|
* `gov install` records file hashes and never the stack it resolved, so the
|
|
538
538
|
* chain a target consumed survives nowhere else. `installRules` copies each
|
|
539
|
-
* rule into the subdirectory it was authored in, which is what makes the
|
|
539
|
+
* rule into the subdirectory it was authored in, which is what makes the folder
|
|
540
540
|
* folders legible as evidence.
|
|
541
541
|
*
|
|
542
542
|
* `successorOf` adds the name a held rule was renamed to, which the next sync
|
|
@@ -547,12 +547,12 @@ export function readInstalledRules(
|
|
|
547
547
|
successorOf?: (name: string) => string | undefined,
|
|
548
548
|
): {
|
|
549
549
|
held: Set<string>
|
|
550
|
-
|
|
550
|
+
folders: Set<string>
|
|
551
551
|
} {
|
|
552
552
|
const dir = join(target, ...INSTALL_MARKERS.governance)
|
|
553
553
|
const held = new Set<string>()
|
|
554
|
-
const
|
|
555
|
-
if (!isDirectory(dir)) return { held,
|
|
554
|
+
const folders = new Set<string>()
|
|
555
|
+
if (!isDirectory(dir)) return { held, folders }
|
|
556
556
|
|
|
557
557
|
for (const rel of new Bun.Glob('**/*.md').scanSync({
|
|
558
558
|
cwd: dir,
|
|
@@ -566,34 +566,34 @@ export function readInstalledRules(
|
|
|
566
566
|
held.add(name)
|
|
567
567
|
const successor = successorOf?.(name)
|
|
568
568
|
if (successor !== undefined) held.add(successor)
|
|
569
|
-
if (boundary > 0)
|
|
569
|
+
if (boundary > 0) folders.add(posix.slice(0, boundary))
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
-
return { held,
|
|
572
|
+
return { held, folders }
|
|
573
573
|
}
|
|
574
574
|
|
|
575
575
|
/**
|
|
576
576
|
* Narrows rules added upstream to the ones this target could receive and does
|
|
577
577
|
* not already hold.
|
|
578
578
|
*
|
|
579
|
-
* The
|
|
579
|
+
* The folder test is the entitlement filter, and `folders` carries two sources: the
|
|
580
580
|
* folders the target already holds, and the folders holding a rule the base
|
|
581
581
|
* stack resolves to.
|
|
582
582
|
* A rule authored under `lang/` or `ui/` is named by an individual stack, so it
|
|
583
583
|
* belongs to some targets and not others, and listing every added file would
|
|
584
584
|
* tell a base consumer about rules it was never entitled to. The test
|
|
585
|
-
* over-reports inside a
|
|
585
|
+
* over-reports inside a folder a target already carries, since one folder can be
|
|
586
586
|
* reached by more than one stack, and that costs a line where under-reporting
|
|
587
587
|
* would cost the whole point of the section.
|
|
588
588
|
*
|
|
589
|
-
* The `held` test is what keeps a rule that moved
|
|
589
|
+
* The `held` test is what keeps a rule that moved folders upstream out. A rename
|
|
590
590
|
* reaches this diff as an addition, and the target already has the file under
|
|
591
591
|
* its old folder, so matching by name is what tells the two apart.
|
|
592
592
|
*/
|
|
593
593
|
export function selectNewRules(
|
|
594
594
|
paths: string,
|
|
595
595
|
held: ReadonlySet<string>,
|
|
596
|
-
|
|
596
|
+
folders: ReadonlySet<string>,
|
|
597
597
|
): string[] {
|
|
598
598
|
const prefix = 'governance/rules/'
|
|
599
599
|
const names = new Set<string>()
|
|
@@ -604,11 +604,11 @@ export function selectNewRules(
|
|
|
604
604
|
|
|
605
605
|
const rel = trimmed.slice(prefix.length)
|
|
606
606
|
const boundary = rel.indexOf('/')
|
|
607
|
-
const
|
|
607
|
+
const folder = boundary === -1 ? '' : rel.slice(0, boundary)
|
|
608
608
|
const name = basename(rel, '.md')
|
|
609
609
|
|
|
610
610
|
if (held.has(name)) continue
|
|
611
|
-
if (
|
|
611
|
+
if (folder !== '' && !folders.has(folder)) continue
|
|
612
612
|
|
|
613
613
|
names.add(name)
|
|
614
614
|
}
|
|
@@ -617,29 +617,29 @@ export function selectNewRules(
|
|
|
617
617
|
}
|
|
618
618
|
|
|
619
619
|
/**
|
|
620
|
-
*
|
|
620
|
+
* Rule folders holding a rule the base stack resolves to. Every governance
|
|
621
621
|
* stack extends base, so a new rule authored beside one of base's rules is
|
|
622
622
|
* reported to every target the fallback below reads.
|
|
623
623
|
*
|
|
624
|
-
* This is what covers a
|
|
625
|
-
* read off folders the target already holds, and a
|
|
624
|
+
* This is what covers a folder no target can carry yet. Entitlement is otherwise
|
|
625
|
+
* read off folders the target already holds, and a folder base reaches later
|
|
626
626
|
* exists in no installed tree, so without this the rules inside it would reach
|
|
627
627
|
* nobody. Read through `resolveRules` rather than off the stack's entries,
|
|
628
628
|
* since base names its rules one by one and an entry is a slug rather than a
|
|
629
629
|
* folder.
|
|
630
630
|
*
|
|
631
631
|
* A folder only a leaf stack reaches is deliberately absent. It is entitled to
|
|
632
|
-
* some targets and not others, which is the distinction the
|
|
632
|
+
* some targets and not others, which is the distinction the folder test makes and
|
|
633
633
|
* the installed tree is the only evidence of.
|
|
634
634
|
*/
|
|
635
|
-
export function
|
|
635
|
+
export function baseFolders(root: string): Set<string> {
|
|
636
636
|
const resolution = resolveRules(root, 'base')
|
|
637
637
|
if (!resolution.ok) return new Set()
|
|
638
638
|
|
|
639
639
|
return new Set(
|
|
640
640
|
lookupRules(root, resolution.rules)
|
|
641
641
|
.found.map((source) => source.subdir.split(sep)[0] ?? '')
|
|
642
|
-
.filter((
|
|
642
|
+
.filter((folder) => folder !== ''),
|
|
643
643
|
)
|
|
644
644
|
}
|
|
645
645
|
|
|
@@ -651,7 +651,7 @@ export function baseBands(root: string): Set<string> {
|
|
|
651
651
|
* `collectMissing` reports per file through the domain scan, so a rule the
|
|
652
652
|
* chain names and the target lacks reaches both surfaces the same way.
|
|
653
653
|
*
|
|
654
|
-
* The diff-and-
|
|
654
|
+
* The diff-and-folder path stays as the fallback for a target stamped before
|
|
655
655
|
* governance recorded a chain. Rules are domain-scoped there too, so it
|
|
656
656
|
* measures from governance's own anchor rather than from the oldest anchor
|
|
657
657
|
* across domains the way `readNewSkills` does. A shared anchor would let
|
|
@@ -670,7 +670,7 @@ export function baseBands(root: string): Set<string> {
|
|
|
670
670
|
* unmeasured result from a clean one.
|
|
671
671
|
*
|
|
672
672
|
* A recorded chain naming a stack the toolkit no longer ships falls through to
|
|
673
|
-
* the
|
|
673
|
+
* the folder-based path below rather than reporting the empty list an
|
|
674
674
|
* unresolved chain would otherwise produce. That empty list reads exactly
|
|
675
675
|
* like a target holding everything, which is the same failure this function
|
|
676
676
|
* exists to close, so a retired stack name is read the same as no chain at
|
|
@@ -702,8 +702,8 @@ export async function readNewRules(
|
|
|
702
702
|
'governance/rules/',
|
|
703
703
|
])
|
|
704
704
|
|
|
705
|
-
const { held,
|
|
706
|
-
return selectNewRules(paths, held,
|
|
705
|
+
const { held, folders } = readInstalledRules(target, successorResolver(root))
|
|
706
|
+
return selectNewRules(paths, held, folders.union(baseFolders(root)))
|
|
707
707
|
}
|
|
708
708
|
|
|
709
709
|
async function readUpstream(
|
|
@@ -19,12 +19,12 @@ export function head(page: 'index' | 'lesson', breakpoint: number): void {
|
|
|
19
19
|
}
|
|
20
20
|
try {
|
|
21
21
|
s = localStorage.getItem('teach-sb')
|
|
22
|
-
} catch
|
|
22
|
+
} catch {}
|
|
23
23
|
var idx = r.dataset.page === 'index'
|
|
24
24
|
if (s === 'shut' || (s === null && idx)) r.classList.add('sb-shut')
|
|
25
25
|
var w: string | null = null
|
|
26
26
|
try {
|
|
27
27
|
w = localStorage.getItem('teach-sb-w')
|
|
28
|
-
} catch
|
|
28
|
+
} catch {}
|
|
29
29
|
if (w) r.style.setProperty('--sb-w', w + 'px')
|
|
30
30
|
}
|
|
@@ -34,7 +34,7 @@ export function sidebar(breakpoint: number): void {
|
|
|
34
34
|
root.classList.add('sb-shut')
|
|
35
35
|
try {
|
|
36
36
|
localStorage.setItem('teach-sb', 'shut')
|
|
37
|
-
} catch
|
|
37
|
+
} catch {}
|
|
38
38
|
if (inside && fold) fold.focus()
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -46,7 +46,7 @@ export function sidebar(breakpoint: number): void {
|
|
|
46
46
|
'teach-sb',
|
|
47
47
|
root.classList.contains('sb-shut') ? 'shut' : 'open',
|
|
48
48
|
)
|
|
49
|
-
} catch
|
|
49
|
+
} catch {}
|
|
50
50
|
/* An overlay takes focus with it. Without this a keyboard reader opens the
|
|
51
51
|
panel and goes on tabbing through the lesson behind the scrim. */
|
|
52
52
|
if (!narrow.matches || root.classList.contains('sb-shut')) return
|
|
@@ -112,7 +112,7 @@ export function sidebar(breakpoint: number): void {
|
|
|
112
112
|
if (save) {
|
|
113
113
|
try {
|
|
114
114
|
localStorage.setItem('teach-sb-w', String(w))
|
|
115
|
-
} catch
|
|
115
|
+
} catch {}
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -3,7 +3,7 @@ export function theme(): void {
|
|
|
3
3
|
try {
|
|
4
4
|
var s = localStorage.getItem('course-theme')
|
|
5
5
|
if (s) r.dataset.theme = s
|
|
6
|
-
} catch
|
|
6
|
+
} catch {}
|
|
7
7
|
document.addEventListener('click', function (e) {
|
|
8
8
|
var b = (e.target as Element).closest('.theme')
|
|
9
9
|
if (!b) return
|
|
@@ -13,6 +13,6 @@ export function theme(): void {
|
|
|
13
13
|
r.dataset.theme = d ? 'light' : 'dark'
|
|
14
14
|
try {
|
|
15
15
|
localStorage.setItem('course-theme', r.dataset.theme)
|
|
16
|
-
} catch
|
|
16
|
+
} catch {}
|
|
17
17
|
})
|
|
18
18
|
}
|
package/src/tooling/inject.ts
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
applyScripts,
|
|
18
18
|
collectDeps,
|
|
19
|
-
collectScripts,
|
|
20
19
|
readPackage,
|
|
21
20
|
serializePackage,
|
|
22
21
|
} from '@/tooling/package'
|
|
@@ -198,7 +197,8 @@ export async function injectManifest(
|
|
|
198
197
|
|
|
199
198
|
function warnSkippedInstalls(chain: readonly Manifest[], target: string): void {
|
|
200
199
|
const deps = collectDeps(chain, {}).map((dep) => dep.spec)
|
|
201
|
-
const
|
|
200
|
+
const skipped = applyScripts({}, chain)
|
|
201
|
+
const scripts = [...new Set([...skipped.added, ...skipped.overridden])]
|
|
202
202
|
if (deps.length === 0 && scripts.length === 0) return
|
|
203
203
|
|
|
204
204
|
logStep('Skipped installs')
|
package/src/tooling/package.ts
CHANGED
|
@@ -101,34 +101,31 @@ export function collectDeps(
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
|
-
* Compares manifest scripts
|
|
105
|
-
*
|
|
104
|
+
* Compares manifest scripts and `[scripts.override]` keys against
|
|
105
|
+
* package.json. The expected value of each key comes from `applyScripts`, so
|
|
106
|
+
* the scan and the write path agree on which stack wins. Plain keys come
|
|
107
|
+
* first, nearest stack first, then override-only keys.
|
|
106
108
|
*/
|
|
107
109
|
export function collectScripts(
|
|
108
110
|
chain: readonly Manifest[],
|
|
109
111
|
pkg: PackageJson,
|
|
110
112
|
): ScriptState[] {
|
|
111
113
|
const scripts = pkg.scripts ?? {}
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
+
const expected = applyScripts({}, chain).pkg.scripts ?? {}
|
|
115
|
+
const keys = new Set<string>()
|
|
114
116
|
|
|
115
117
|
for (const manifest of chain) {
|
|
116
|
-
for (const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const current = scripts[key]
|
|
121
|
-
if (current === undefined) {
|
|
122
|
-
states.push({ key, state: 'missing' })
|
|
123
|
-
} else if (current !== value) {
|
|
124
|
-
states.push({ key, state: 'drifted' })
|
|
125
|
-
} else {
|
|
126
|
-
states.push({ key, state: 'matching' })
|
|
127
|
-
}
|
|
128
|
-
}
|
|
118
|
+
for (const key of Object.keys(manifest.scripts)) keys.add(key)
|
|
119
|
+
}
|
|
120
|
+
for (const manifest of chain) {
|
|
121
|
+
for (const key of Object.keys(manifest.scriptOverrides)) keys.add(key)
|
|
129
122
|
}
|
|
130
123
|
|
|
131
|
-
return
|
|
124
|
+
return [...keys].map((key): ScriptState => {
|
|
125
|
+
const current = scripts[key]
|
|
126
|
+
if (current === undefined) return { key, state: 'missing' }
|
|
127
|
+
return { key, state: current === expected[key] ? 'matching' : 'drifted' }
|
|
128
|
+
})
|
|
132
129
|
}
|
|
133
130
|
|
|
134
131
|
/**
|
|
@@ -32,7 +32,8 @@ interface Run {
|
|
|
32
32
|
/**
|
|
33
33
|
* Scaffolds a stack into `.canon/tmp/runs/verify-<stack>/`, syncs its chain
|
|
34
34
|
* from the checkout at `root`, and runs the scaffold's own `lint:fix`,
|
|
35
|
-
* `check`, `test:e2e`, and `screenshot` scripts. Every line goes
|
|
35
|
+
* `format`, `check`, `test:e2e`, and `screenshot` scripts. Every line goes
|
|
36
|
+
* to stderr.
|
|
36
37
|
*
|
|
37
38
|
* A failed phase is recorded and the run carries on, so the Results block
|
|
38
39
|
* reports every phase rather than the first failure. The tmp dir is removed
|
|
@@ -153,11 +154,13 @@ async function run({
|
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
/**
|
|
156
|
-
* `lint:fix`
|
|
157
|
-
*
|
|
157
|
+
* `lint:fix` and `format` run first and rewrite the scaffold, so `check`
|
|
158
|
+
* reads the tree a project would commit. `check` itself writes nothing, and a
|
|
159
|
+
* fresh scaffold ships files its own generator formatted another way.
|
|
158
160
|
*/
|
|
159
161
|
async function runScripts(state: Run, scripts: Set<string>): Promise<void> {
|
|
160
162
|
await phase(state, 'lint:fix', () => bunRun('lint:fix', state.dir))
|
|
163
|
+
await phase(state, 'format', () => bunRun('format', state.dir))
|
|
161
164
|
await phase(state, 'check', () => bunRun('check', state.dir))
|
|
162
165
|
|
|
163
166
|
if (scripts.has('test:e2e')) {
|
|
@@ -9,9 +9,18 @@ packages = [
|
|
|
9
9
|
"astro",
|
|
10
10
|
"@astrojs/react",
|
|
11
11
|
"@astrojs/check",
|
|
12
|
+
"eslint@^9",
|
|
13
|
+
"@eslint/js",
|
|
14
|
+
"typescript-eslint",
|
|
15
|
+
"eslint-config-prettier",
|
|
16
|
+
"eslint-plugin-react-hooks",
|
|
17
|
+
"eslint-plugin-vitest",
|
|
18
|
+
"globals",
|
|
12
19
|
"eslint-plugin-astro",
|
|
13
20
|
"astro-eslint-parser",
|
|
21
|
+
"prettier",
|
|
14
22
|
"prettier-plugin-astro",
|
|
23
|
+
"prettier-plugin-tailwindcss",
|
|
15
24
|
]
|
|
16
25
|
|
|
17
26
|
[scripts]
|
|
@@ -24,6 +33,10 @@ packages = [
|
|
|
24
33
|
"card:check" = "bash scripts/check-card-exclusion.sh"
|
|
25
34
|
|
|
26
35
|
[scripts.override]
|
|
36
|
+
"lint" = "eslint . --max-warnings 0"
|
|
37
|
+
"lint:fix" = "eslint . --fix --max-warnings 0"
|
|
38
|
+
"check:format" = "oxfmt --check . && prettier --check --log-level warn '**/*.astro'"
|
|
39
|
+
"format" = "oxfmt . && prettier --write --log-level warn '**/*.astro'"
|
|
27
40
|
"screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 4321) && export PREVIEW_PORT && bash scripts/screenshot.sh"
|
|
28
41
|
"readme-screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 4321) && export PREVIEW_PORT && bash scripts/readme-screenshot.sh"
|
|
29
42
|
"dev" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && astro dev"
|
|
@@ -4,7 +4,7 @@
|
|
|
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`,
|
|
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.
|
|
8
8
|
|
|
9
9
|
## Scaffold checklist
|
|
10
10
|
|
|
@@ -13,16 +13,16 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
|
|
|
13
13
|
3. Install web tooling: `canon tooling sync web . --write`
|
|
14
14
|
4. Install astro adapter: `canon tooling sync astro . --write`
|
|
15
15
|
5. Extend the `ci` and `development` context entries under `canon/context/` per the web reference's extend sections plus the astro rows below.
|
|
16
|
-
6. Run `bun run lint:fix` then `bun run check`.
|
|
16
|
+
6. Run `bun run lint:fix`, then `bun run format`, then `bun run check`.
|
|
17
17
|
|
|
18
18
|
## What ships as golden configs
|
|
19
19
|
|
|
20
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
21
|
- `vitest.config.ts`: uses `getViteConfig` from `astro/config` (not `mergeConfig`). jsdom, globals, setup file, `passWithNoTests: true`, v8 coverage, `**/*.astro` in coverage excludes.
|
|
22
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` in types, `@/` paths.
|
|
23
|
+
- `tsconfig.json`: extends `astro/tsconfigs/strict`, adds `skipLibCheck`, `vitest/globals` and `@testing-library/jest-dom/vitest` in types, `@/` paths.
|
|
24
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`:
|
|
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/tooling/stacks.md` for the measurement.
|
|
26
26
|
|
|
27
27
|
## Typecheck
|
|
28
28
|
|
|
@@ -32,11 +32,21 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
|
|
|
32
32
|
|
|
33
33
|
Unit-test React island components (`.tsx`) only. Do not test `.astro` files. Page-level behavior is verified by Playwright against rendered output.
|
|
34
34
|
|
|
35
|
-
Vitest is pinned to `^
|
|
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.
|
|
36
36
|
|
|
37
|
-
##
|
|
37
|
+
## Formatting (extend)
|
|
38
38
|
|
|
39
|
-
|
|
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.
|
|
41
|
+
- 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.
|
|
43
|
+
- Override `check:format` and `format` to run oxfmt, then Prettier over `**/*.astro`.
|
|
44
|
+
|
|
45
|
+
## ESLint
|
|
46
|
+
|
|
47
|
+
- 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.
|
|
49
|
+
- Expect the web layer's `.oxlintrc.json` in the target. Nothing runs it here.
|
|
40
50
|
|
|
41
51
|
## Development docs (extend)
|
|
42
52
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Runs this folder's verify phases in a fixed order. The phases a stack
|
|
2
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
|
|
3
|
+
// nearest one wins. Only node: modules, since a target's own linter and
|
|
4
4
|
// tsc --noEmit read this file with no Bun types.
|
|
5
5
|
import { spawnSync } from 'node:child_process'
|
|
6
6
|
import { accessSync, constants, existsSync, readFileSync } from 'node:fs'
|
|
@@ -46,16 +46,10 @@ const PHASES: readonly Phase[] = [
|
|
|
46
46
|
failed: 'Lint failed',
|
|
47
47
|
passed: 'Lint passed',
|
|
48
48
|
},
|
|
49
|
-
{
|
|
50
|
-
script: 'format',
|
|
51
|
-
title: 'Formatting',
|
|
52
|
-
failed: 'Format failed',
|
|
53
|
-
passed: 'Format applied',
|
|
54
|
-
},
|
|
55
49
|
{
|
|
56
50
|
script: 'check:format',
|
|
57
51
|
title: 'Format check',
|
|
58
|
-
failed:
|
|
52
|
+
failed: "Format check failed. Run 'bun run format' to fix it",
|
|
59
53
|
passed: 'Format check passed',
|
|
60
54
|
},
|
|
61
55
|
{
|
|
@@ -85,7 +79,7 @@ const PHASES: readonly Phase[] = [
|
|
|
85
79
|
// A subfolder synced with --skip base declares none of these, since the
|
|
86
80
|
// repository root owns them, so a list that does not require one runs it only
|
|
87
81
|
// where the folder declares it.
|
|
88
|
-
const BASE_PHASES = new Set(['
|
|
82
|
+
const BASE_PHASES = new Set(['check:format'])
|
|
89
83
|
|
|
90
84
|
// An add-on stack declares these, keyed to the stack that ships each, so a
|
|
91
85
|
// folder that never synced the add-on skips the phase rather than failing it.
|
|
@@ -10,19 +10,19 @@ per_root = ["scripts/verify.ts", "scripts/verify.json"]
|
|
|
10
10
|
|
|
11
11
|
[dependencies.dev]
|
|
12
12
|
packages = [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
"oxfmt@0.72.0",
|
|
14
|
+
"husky",
|
|
15
|
+
"@commitlint/cli",
|
|
16
|
+
"@commitlint/config-conventional",
|
|
17
17
|
]
|
|
18
18
|
|
|
19
19
|
[scripts]
|
|
20
|
-
"check:format" = "prettier --check --log-level warn --ignore-path .gitignore --ignore-path .prettierignore ."
|
|
21
|
-
"format" = "prettier --write --log-level warn --ignore-path .gitignore --ignore-path .prettierignore ."
|
|
22
20
|
"prepare" = "husky"
|
|
23
21
|
|
|
24
22
|
[scripts.override]
|
|
25
23
|
"check" = "bun scripts/verify.ts"
|
|
24
|
+
"check:format" = "oxfmt --check ."
|
|
25
|
+
"format" = "oxfmt ."
|
|
26
26
|
|
|
27
27
|
[gitignore]
|
|
28
28
|
"# System" = [".DS_Store"]
|
|
@@ -10,18 +10,17 @@ A repository with several language roots layers this stack once at the root and
|
|
|
10
10
|
|
|
11
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.
|
|
12
12
|
|
|
13
|
-
- `.
|
|
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
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
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
|
|
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
|
|
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.
|
|
18
18
|
|
|
19
19
|
## What ships as user-owned seeds
|
|
20
20
|
|
|
21
21
|
Seeds live in `tooling/base/seeds/`. Sync drops each once on first install and never overwrites it, so a project extends them freely.
|
|
22
22
|
|
|
23
23
|
- `.lintstagedrc`: the glob map below.
|
|
24
|
-
- `.prettierignore`: created empty. Projects add their own entries.
|
|
25
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.
|
|
26
25
|
- `.github/pull_request_template.md`: `## Summary`, `## Key changes`, `## Technical context`, `## Testing`. A project adds the checklist lines its own review needs.
|
|
27
26
|
- `canon/context/`: extend the `ci` and `development` entries with project-specific commands, workflows, or deploy steps. Canonical rationale stays in this reference.
|
|
@@ -29,9 +28,14 @@ Seeds live in `tooling/base/seeds/`. Sync drops each once on first install and n
|
|
|
29
28
|
## Tool pairing
|
|
30
29
|
|
|
31
30
|
- Runtime: `bun` as package manager and script runner, `bunx` over `npx` for a one-off executable.
|
|
32
|
-
- Formatting:
|
|
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.
|
|
33
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.
|
|
34
|
-
- Dev dependencies: `
|
|
33
|
+
- Dev dependencies: `oxfmt`, `husky`, `@commitlint/cli`, `@commitlint/config-conventional`. Install via `bun add -D`.
|
|
34
|
+
|
|
35
|
+
## oxfmt
|
|
36
|
+
|
|
37
|
+
- 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.
|
|
35
39
|
|
|
36
40
|
## File layout
|
|
37
41
|
|
|
@@ -50,7 +54,7 @@ Seeds live in `tooling/base/seeds/`. Sync drops each once on first install and n
|
|
|
50
54
|
|
|
51
55
|
Seeded baseline globs:
|
|
52
56
|
|
|
53
|
-
- `**/*.{json,md
|
|
57
|
+
- `**/*.{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.
|
|
54
58
|
- `**/*.md` runs `canon indexes regen`
|
|
55
59
|
|
|
56
60
|
## CI
|
|
@@ -69,21 +73,20 @@ Seeded baseline globs:
|
|
|
69
73
|
|
|
70
74
|
Sticky negative knowledge. Do not relearn.
|
|
71
75
|
|
|
72
|
-
- Do NOT
|
|
73
|
-
- Do NOT omit `--log-level warn` from a prettier invocation. The default prints a line per unchanged file.
|
|
76
|
+
- Do NOT pass `--ignore-path` to oxfmt. It replaces the default `.gitignore` and `.prettierignore` pair rather than adding to it.
|
|
74
77
|
- 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.
|
|
75
78
|
|
|
76
79
|
## CLI
|
|
77
80
|
|
|
78
|
-
| Script | What it does
|
|
79
|
-
| ---------------------- |
|
|
80
|
-
| `bun run check` |
|
|
81
|
-
| `bun run check:format` | Asserts
|
|
82
|
-
| `bun run format` | Writes
|
|
83
|
-
| `bun run prepare` | Initializes husky hooks, run automatically on `bun install`
|
|
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` |
|
|
84
87
|
|
|
85
88
|
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.
|
|
86
89
|
|
|
87
|
-
`bun run check`
|
|
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.
|
|
88
91
|
|
|
89
|
-
`scripts/verify.ts`'s `Markdown bans` stage
|
|
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.
|
|
@@ -13,19 +13,19 @@ Owns the GitHub Actions workflow that gates a merge: which events start a run, a
|
|
|
13
13
|
|
|
14
14
|
- `.github/workflows/` owns the workflow definitions a trigger below starts
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
### Triggers
|
|
17
17
|
|
|
18
18
|
- Pull requests targeting `main`
|
|
19
19
|
- `workflow_dispatch` (manual run from the Actions tab)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
### Checks
|
|
22
22
|
|
|
23
23
|
Defined in `.github/workflows/verify.yml`. All jobs must pass before merge.
|
|
24
24
|
|
|
25
|
-
| Check | Command | What it asserts
|
|
26
|
-
| ------ | ---------------------- |
|
|
27
|
-
| Format | `bun run check:format` |
|
|
25
|
+
| Check | Command | What it asserts |
|
|
26
|
+
| ------ | ---------------------- | --------------- |
|
|
27
|
+
| Format | `bun run check:format` | oxfmt is clean |
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
### Running CI locally
|
|
30
30
|
|
|
31
|
-
`bun run check` runs the same assert
|
|
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.
|