@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.
Files changed (165) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/api-design/SKILL.md +6 -0
  3. package/claude/skills/auto-ship/SKILL.md +1 -1
  4. package/claude/skills/backlog-triage/SKILL.md +8 -8
  5. package/claude/skills/build-in-slices/SKILL.md +7 -1
  6. package/claude/skills/build-in-slices/references/adopted.md +3 -0
  7. package/claude/skills/canon-cli/SKILL.md +3 -0
  8. package/claude/skills/canon-feedback/SKILL.md +5 -0
  9. package/claude/skills/canon-feedback-triage/SKILL.md +3 -0
  10. package/claude/skills/canvas-board/SKILL.md +3 -0
  11. package/claude/skills/canvas-board/references/canvas.md +12 -0
  12. package/claude/skills/ci-workflow/SKILL.md +5 -0
  13. package/claude/skills/code-craft/SKILL.md +6 -0
  14. package/claude/skills/context-fold/references/context.md +48 -34
  15. package/claude/skills/create-rule/SKILL.md +11 -9
  16. package/claude/skills/create-rule/references/rule.md +6 -0
  17. package/claude/skills/create-skill/SKILL.md +11 -2
  18. package/claude/skills/create-skill/references/skill-disclosure.md +26 -0
  19. package/claude/skills/create-skill/references/skill-paths.md +3 -0
  20. package/claude/skills/create-skill/references/skill-reference.md +12 -0
  21. package/claude/skills/create-skill/references/skill.md +25 -24
  22. package/claude/skills/deprecation-migration/SKILL.md +6 -0
  23. package/claude/skills/deprecation-migration/references/adopted.md +3 -0
  24. package/claude/skills/design-taste/SKILL.md +6 -0
  25. package/claude/skills/design-taste/references/craft.md +3 -0
  26. package/claude/skills/design-taste/references/preflight.md +16 -0
  27. package/claude/skills/docs-sync/references/docs.md +6 -0
  28. package/claude/skills/docs-sync/references/readme.md +15 -0
  29. package/claude/skills/draft-and-pick/SKILL.md +12 -0
  30. package/claude/skills/draft-doc/references/context.md +1 -1
  31. package/claude/skills/draft-doc/references/readme.md +3 -0
  32. package/claude/skills/draft-figure/references/mermaid.md +6 -0
  33. package/claude/skills/draft-slides/SKILL.md +3 -0
  34. package/claude/skills/git-branch/references/branch.md +16 -11
  35. package/claude/skills/git-commit/references/commit.md +19 -11
  36. package/claude/skills/git-commit/references/versioning.md +9 -0
  37. package/claude/skills/git-issue/references/issue.md +13 -9
  38. package/claude/skills/git-pr/SKILL.md +5 -0
  39. package/claude/skills/git-pr/references/pr.md +24 -6
  40. package/claude/skills/git-ship/SKILL.md +3 -0
  41. package/claude/skills/git-split/SKILL.md +6 -0
  42. package/claude/skills/git-stage/SKILL.md +3 -0
  43. package/claude/skills/git-worktree/SKILL.md +6 -6
  44. package/claude/skills/markdown-craft/references/markdown.md +25 -6
  45. package/claude/skills/plan-feature/SKILL.md +9 -0
  46. package/claude/skills/plan-feature/references/plan.md +20 -8
  47. package/claude/skills/plan-groundwork/SKILL.md +6 -0
  48. package/claude/skills/plan-groundwork/references/groundwork.md +61 -29
  49. package/claude/skills/plan-intake/references/intake.md +6 -6
  50. package/claude/skills/read-frames/SKILL.md +3 -0
  51. package/claude/skills/review-craft/SKILL.md +3 -0
  52. package/claude/skills/review-pr/references/post.md +1 -1
  53. package/claude/skills/review-ui/SKILL.md +6 -3
  54. package/claude/skills/role-orchestrator/SKILL.md +4 -1
  55. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +7 -4
  56. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +1 -1
  57. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +3 -0
  58. package/claude/skills/role-planner/SKILL.md +4 -1
  59. package/claude/skills/role-worker/SKILL.md +13 -1
  60. package/claude/skills/search-craft/SKILL.md +3 -0
  61. package/claude/skills/systematic-debugging/SKILL.md +6 -0
  62. package/claude/skills/target-check/references/domains.md +5 -0
  63. package/claude/skills/target-setup/SKILL.md +8 -4
  64. package/claude/skills/target-setup/references/verify.md +6 -5
  65. package/claude/skills/task-board/references/tasks.md +12 -7
  66. package/claude/skills/task-complete/references/archive.md +3 -0
  67. package/claude/skills/teach-workspace/SKILL.md +3 -0
  68. package/claude/skills/teach-workspace/references/glossary.md +3 -0
  69. package/claude/skills/teach-workspace/references/lesson-build.md +3 -0
  70. package/claude/skills/teach-workspace/references/lesson-craft.md +3 -0
  71. package/claude/skills/teach-workspace/references/teach.md +8 -8
  72. package/claude/skills/test-craft/SKILL.md +5 -0
  73. package/claude/skills/video-craft/SKILL.md +6 -0
  74. package/claude/skills/write-human/SKILL.md +9 -0
  75. package/docs/agents/capture.md +2 -0
  76. package/docs/agents/commands.md +54 -54
  77. package/docs/agents/context-audit-checks.md +6 -0
  78. package/docs/agents/context-audit.md +8 -8
  79. package/docs/agents/context-classify.md +2 -0
  80. package/docs/agents/drift-surfaces.md +5 -5
  81. package/docs/agents/gate.md +6 -7
  82. package/docs/agents/index.md +1 -1
  83. package/docs/agents/install-and-sync.md +2 -2
  84. package/docs/agents/markdown-audit-weight.md +11 -3
  85. package/docs/agents/markdown-audit.md +2 -2
  86. package/docs/agents/pr-frames.md +17 -3
  87. package/docs/agents/sessions-transfer.md +8 -6
  88. package/docs/agents/skills-audit.md +9 -1
  89. package/docs/target/projects.md +1 -1
  90. package/docs/target/scenarios.md +2 -2
  91. package/docs/target/standards-moved.md +20 -18
  92. package/docs/workflow/ai-workflow.md +3 -0
  93. package/docs/workflow/visual-design-workflow.md +5 -0
  94. package/governance/detect.toml +6 -1
  95. package/package.json +7 -4
  96. package/scripts/core/install-check.sh +1 -1
  97. package/src/cli/help.ts +1 -1
  98. package/src/commands/claude.ts +0 -1
  99. package/src/commands/context.ts +29 -4
  100. package/src/commands/gate.ts +1 -8
  101. package/src/commands/markdown.ts +52 -0
  102. package/src/commands/pr.ts +18 -11
  103. package/src/commands/records.ts +0 -1
  104. package/src/context/audit.ts +83 -0
  105. package/src/context/classify/prompts.ts +6 -1
  106. package/src/context/gate.ts +6 -3
  107. package/src/docs/read.ts +15 -21
  108. package/src/gate/measures.ts +186 -1
  109. package/src/gate/sequencer.ts +0 -4
  110. package/src/gate/stages.ts +46 -27
  111. package/src/git/files.ts +30 -0
  112. package/src/gov/adapter.ts +2 -2
  113. package/src/gov/install.ts +2 -2
  114. package/src/gov/list.ts +1 -1
  115. package/src/gov/payload.ts +1 -1
  116. package/src/gov/stacks.ts +1 -1
  117. package/src/markdown/gate.ts +10 -0
  118. package/src/markdown/growth.ts +43 -0
  119. package/src/markdown/stacks.ts +74 -0
  120. package/src/markdown/structure.ts +73 -5
  121. package/src/migrate/rule-layout.ts +12 -12
  122. package/src/pr/frames.ts +66 -1
  123. package/src/sandbox/equivalence.ts +7 -9
  124. package/src/sync/check.ts +23 -23
  125. package/src/teach/browser/head.ts +2 -2
  126. package/src/teach/browser/sidebar.ts +3 -3
  127. package/src/teach/browser/theme.ts +2 -2
  128. package/src/tooling/inject.ts +2 -2
  129. package/src/tooling/package.ts +15 -18
  130. package/src/tooling/verify-stack.ts +6 -3
  131. package/tooling/astro/configs/.oxfmtrc.json +8 -0
  132. package/tooling/astro/configs/.prettierrc +13 -0
  133. package/tooling/astro/configs/tsconfig.json +1 -1
  134. package/tooling/astro/manifest.toml +13 -0
  135. package/tooling/astro/reference.md +17 -7
  136. package/tooling/base/configs/.oxfmtrc.json +7 -0
  137. package/tooling/base/configs/scripts/verify.json +1 -1
  138. package/tooling/base/configs/scripts/verify.ts +3 -9
  139. package/tooling/base/manifest.toml +6 -6
  140. package/tooling/base/reference.md +20 -17
  141. package/tooling/base/seeds/.lintstagedrc +1 -3
  142. package/tooling/base/seeds/canon/context/ci.md +7 -7
  143. package/tooling/base/seeds/canon/context/development.md +10 -10
  144. package/tooling/claude/manifest.toml +5 -1
  145. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +8 -0
  146. package/tooling/go/reference.md +2 -2
  147. package/tooling/nextjs/configs/.oxlintrc.json +223 -0
  148. package/tooling/nextjs/manifest.toml +2 -5
  149. package/tooling/nextjs/reference.md +4 -4
  150. package/tooling/php/manifest.toml +6 -1
  151. package/tooling/php/reference.md +2 -2
  152. package/tooling/python/manifest.toml +6 -1
  153. package/tooling/python/reference.md +4 -4
  154. package/tooling/vite-react/configs/tsconfig.json +1 -1
  155. package/tooling/vite-react/configs/vitest.config.ts +1 -1
  156. package/tooling/vite-react/manifest.toml +1 -4
  157. package/tooling/vite-react/reference.md +5 -5
  158. package/tooling/web/configs/.oxlintrc.json +213 -0
  159. package/tooling/web/configs/src/test/setup.ts +1 -1
  160. package/tooling/web/manifest.toml +12 -16
  161. package/tooling/web/reference.md +15 -15
  162. package/tooling/base/configs/.prettierrc +0 -12
  163. package/tooling/base/seeds/.prettierignore +0 -0
  164. package/tooling/nextjs/configs/eslint.config.js +0 -91
  165. 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 `bands` every band folder those names sit in.
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 band
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
- bands: Set<string>
550
+ folders: Set<string>
551
551
  } {
552
552
  const dir = join(target, ...INSTALL_MARKERS.governance)
553
553
  const held = new Set<string>()
554
- const bands = new Set<string>()
555
- if (!isDirectory(dir)) return { held, bands }
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) bands.add(posix.slice(0, boundary))
569
+ if (boundary > 0) folders.add(posix.slice(0, boundary))
570
570
  }
571
571
 
572
- return { held, bands }
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 band test is the entitlement filter, and `bands` carries two sources: 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 band a target already carries, since one folder can be
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 bands upstream out. A rename
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
- bands: ReadonlySet<string>,
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 band = boundary === -1 ? '' : rel.slice(0, boundary)
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 (band !== '' && !bands.has(band)) continue
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
- * Band folders holding a rule the base stack resolves to. Every governance
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 band no target can carry yet. Entitlement is otherwise
625
- * read off folders the target already holds, and a band base reaches later
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 band test makes and
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 baseBands(root: string): Set<string> {
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((band) => band !== ''),
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-bands path stays as the fallback for a target stamped before
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 band-based path below rather than reporting the empty list an
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, bands } = readInstalledRules(target, successorResolver(root))
706
- return selectNewRules(paths, held, bands.union(baseBands(root)))
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 (e) {}
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 (e) {}
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 (e) {}
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 (e) {}
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 (e) {}
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 (e) {}
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 (e) {}
16
+ } catch {}
17
17
  })
18
18
  }
@@ -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 scripts = collectScripts(chain, {}).map((script) => script.key)
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')
@@ -101,34 +101,31 @@ export function collectDeps(
101
101
  }
102
102
 
103
103
  /**
104
- * Compares manifest scripts against package.json, nearest stack first, so a
105
- * child definition shadows the parent it extends.
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 seen = new Set<string>()
113
- const states: ScriptState[] = []
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 [key, value] of Object.entries(manifest.scripts)) {
117
- if (seen.has(key)) continue
118
- seen.add(key)
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 states
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 to stderr.
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` runs first and reformats the scaffold, so `check` reads the
157
- * reformatted tree.
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')) {
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "semi": false,
4
+ "singleQuote": true,
5
+ "printWidth": 80,
6
+ "sortPackageJson": false,
7
+ "ignorePatterns": ["**/*.astro"]
8
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
5
+ "overrides": [
6
+ {
7
+ "files": "*.astro",
8
+ "options": {
9
+ "parser": "astro"
10
+ }
11
+ }
12
+ ]
13
+ }
@@ -2,7 +2,7 @@
2
2
  "extends": "astro/tsconfigs/strict",
3
3
  "compilerOptions": {
4
4
  "skipLibCheck": true,
5
- "types": ["vitest/globals", "@testing-library/jest-dom"],
5
+ "types": ["vitest/globals", "@testing-library/jest-dom/vitest"],
6
6
  "paths": {
7
7
  "@/*": ["./src/*"]
8
8
  }
@@ -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`, and an `eslint.config.js` that overrides the web layer's config to include `eslint-plugin-astro` and exclude React-refresh rules.
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`: overrides the web layer. 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.
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 `^3` in the web manifest. Vitest `^4` bundles rolldown-vite (vite@8), which ignores the esbuild JSX config in `@vitejs/plugin-react` and breaks `.tsx` tests, plus emits deprecation warnings. Unpin once `@astrojs/react` ships a rolldown-aware plugin (or astro's bundled vite reaches 8).
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
- ## Prettier (extend)
37
+ ## Formatting (extend)
38
38
 
39
- Add `prettier-plugin-astro` first in plugins, then `prettier-plugin-tailwindcss` last (per its docs). Add a parser override for `.astro` files.
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
 
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "semi": false,
4
+ "singleQuote": true,
5
+ "printWidth": 80,
6
+ "sortPackageJson": false
7
+ }
@@ -1,3 +1,3 @@
1
1
  {
2
- "require": ["format", "check:format"]
2
+ "require": ["check:format"]
3
3
  }
@@ -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 eslint and
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: 'Format check 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(['format', 'check:format'])
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
- "prettier",
14
- "husky",
15
- "@commitlint/cli",
16
- "@commitlint/config-conventional",
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
- - `.prettierrc`: `semi: false`, `singleQuote: true`, plus a parser override per non-standard extension (`.mdx` to `markdown`).
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, 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 its two phases, 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.
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: Prettier for what it parses. Prettier has no shell parser, so shell formatting is the `shell` stack's.
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: `prettier`, `husky`, `@commitlint/cli`, `@commitlint/config-conventional`. Install via `bun add -D`.
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,mdc}` runs `prettier --write --ignore-path .gitignore --ignore-path .prettierignore`
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 invoke prettier without both `--ignore-path .gitignore --ignore-path .prettierignore`. Passing one drops the other, since the flag replaces the default rather than adding to it.
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` | Repairs a checkout through `scripts/verify.ts`. Honors `VERIFY_NESTED=true` to suppress timeline boundaries when another script calls it. |
81
- | `bun run check:format` | Asserts prettier formatting without writing |
82
- | `bun run format` | Writes prettier formatting in place |
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` repairs a checkout rather than gating one. It runs `format` first to auto-fix drifted code, then asserts only what the formatter could not fix. The pull request workflow calls `check:format` directly, and that is the gate.
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 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 format check above.
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.
@@ -1,6 +1,4 @@
1
1
  {
2
- "**/*.{json,md,mdc}": [
3
- "prettier --write --ignore-path .gitignore --ignore-path .prettierignore"
4
- ],
2
+ "**/*.{json,md}": ["oxfmt --no-error-on-unmatched-pattern"],
5
3
  "**/*.md": ["canon indexes regen"]
6
4
  }
@@ -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
- ## Triggers
16
+ ### Triggers
17
17
 
18
18
  - Pull requests targeting `main`
19
19
  - `workflow_dispatch` (manual run from the Actions tab)
20
20
 
21
- ## Checks
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` | prettier is clean |
25
+ | Check | Command | What it asserts |
26
+ | ------ | ---------------------- | --------------- |
27
+ | Format | `bun run check:format` | oxfmt is clean |
28
28
 
29
- ## Running CI locally
29
+ ### Running CI locally
30
30
 
31
- `bun run check` runs the same assert plus auto-formats first. If CI fails on format, run `bun run check` locally and commit the diff.
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.