@erclx/canon 5.37.0 → 5.39.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 (197) hide show
  1. package/README.md +1 -1
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/SKILL.md +12 -9
  4. package/claude/skills/auto-ship/SKILL.md +43 -23
  5. package/claude/skills/backlog-triage/SKILL.md +62 -53
  6. package/claude/skills/backlog-triage/references/apply.md +69 -0
  7. package/claude/skills/build-in-slices/SKILL.md +8 -4
  8. package/claude/skills/canon-cli/SKILL.md +5 -1
  9. package/claude/skills/canon-feedback/SKILL.md +18 -8
  10. package/claude/skills/canon-feedback-triage/SKILL.md +10 -3
  11. package/claude/skills/canvas-board/SKILL.md +36 -17
  12. package/claude/skills/ci-workflow/SKILL.md +5 -4
  13. package/claude/skills/code-craft/SKILL.md +10 -6
  14. package/claude/skills/codebase-layout/SKILL.md +3 -5
  15. package/claude/skills/context-fold/REQUIREMENT.md +4 -4
  16. package/claude/skills/context-fold/SKILL.md +42 -23
  17. package/claude/skills/context-fold/references/context-refresh.md +4 -3
  18. package/claude/skills/context-fold/references/context.md +79 -64
  19. package/claude/skills/create-rule/SKILL.md +7 -3
  20. package/claude/skills/create-skill/SKILL.md +27 -7
  21. package/claude/skills/create-skill/references/skill-reference.md +41 -17
  22. package/claude/skills/decision-escalate/SKILL.md +8 -5
  23. package/claude/skills/deploy-app/SKILL.md +1 -1
  24. package/claude/skills/deprecation-migration/SKILL.md +12 -4
  25. package/claude/skills/design-taste/SKILL.md +17 -11
  26. package/claude/skills/docs-sync/REQUIREMENT.md +1 -0
  27. package/claude/skills/docs-sync/SKILL.md +17 -6
  28. package/claude/skills/docs-sync/references/docs.md +54 -21
  29. package/claude/skills/docs-sync/references/readme.md +32 -8
  30. package/claude/skills/document-health/SKILL.md +3 -4
  31. package/claude/skills/draft-and-pick/SKILL.md +73 -47
  32. package/claude/skills/draft-doc/SKILL.md +9 -4
  33. package/claude/skills/draft-doc/references/context.md +4 -4
  34. package/claude/skills/draft-doc/references/docs.md +1 -0
  35. package/claude/skills/draft-doc/references/wiki.md +3 -2
  36. package/claude/skills/draft-figure/SKILL.md +9 -7
  37. package/claude/skills/draft-identity/SKILL.md +24 -52
  38. package/claude/skills/draft-identity/references/card-route.md +46 -0
  39. package/claude/skills/draft-ready/SKILL.md +19 -7
  40. package/claude/skills/draft-screencast/SKILL.md +6 -112
  41. package/claude/skills/draft-screencast/references/beats-template.md +119 -0
  42. package/claude/skills/draft-slides/SKILL.md +7 -4
  43. package/claude/skills/git-branch/SKILL.md +3 -3
  44. package/claude/skills/git-commit/SKILL.md +5 -3
  45. package/claude/skills/git-followup/SKILL.md +16 -9
  46. package/claude/skills/git-issue/SKILL.md +6 -5
  47. package/claude/skills/git-issue/references/issue.md +11 -10
  48. package/claude/skills/git-pr/SKILL.md +56 -44
  49. package/claude/skills/git-ship/SKILL.md +43 -34
  50. package/claude/skills/git-ship/references/ci-watch.md +31 -0
  51. package/claude/skills/git-split/SKILL.md +28 -100
  52. package/claude/skills/git-split/references/independent.md +40 -0
  53. package/claude/skills/git-split/references/stacked.md +73 -0
  54. package/claude/skills/git-stage/SKILL.md +9 -7
  55. package/claude/skills/git-worktree/SKILL.md +27 -49
  56. package/claude/skills/git-worktree/references/cleanup.md +55 -0
  57. package/claude/skills/markdown-craft/SKILL.md +2 -2
  58. package/claude/skills/markdown-craft/references/markdown.md +82 -52
  59. package/claude/skills/plan-feature/SKILL.md +52 -46
  60. package/claude/skills/plan-feature/references/batches.md +46 -0
  61. package/claude/skills/plan-groundwork/SKILL.md +60 -74
  62. package/claude/skills/plan-groundwork/references/groundwork.md +53 -24
  63. package/claude/skills/plan-groundwork/references/output.md +48 -0
  64. package/claude/skills/plan-intake/SKILL.md +28 -21
  65. package/claude/skills/plan-intake-answer/SKILL.md +14 -13
  66. package/claude/skills/read-frames/SKILL.md +2 -0
  67. package/claude/skills/record-screencast/SKILL.md +31 -11
  68. package/claude/skills/review-address/SKILL.md +76 -158
  69. package/claude/skills/review-address/references/rebase-only.md +34 -0
  70. package/claude/skills/review-craft/SKILL.md +27 -11
  71. package/claude/skills/review-pr/SKILL.md +91 -40
  72. package/claude/skills/review-pr/references/post.md +1 -1
  73. package/claude/skills/review-ui/SKILL.md +62 -37
  74. package/claude/skills/review-ui/references/hosted-preview.md +30 -0
  75. package/claude/skills/role-orchestrator/SKILL.md +105 -74
  76. package/claude/skills/role-orchestrator/references/orchestrator-filing.md +26 -0
  77. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +1 -1
  78. package/claude/skills/role-planner/SKILL.md +66 -64
  79. package/claude/skills/role-reviewer/SKILL.md +57 -46
  80. package/claude/skills/role-worker/SKILL.md +68 -63
  81. package/claude/skills/search-craft/SKILL.md +7 -5
  82. package/claude/skills/session-compact/SKILL.md +16 -11
  83. package/claude/skills/session-map/SKILL.md +31 -10
  84. package/claude/skills/session-relay/SKILL.md +32 -65
  85. package/claude/skills/session-resume/SKILL.md +18 -13
  86. package/claude/skills/session-worktree/SKILL.md +70 -42
  87. package/claude/skills/systematic-debugging/SKILL.md +6 -1
  88. package/claude/skills/target-check/SKILL.md +13 -6
  89. package/claude/skills/target-setup/SKILL.md +41 -30
  90. package/claude/skills/target-setup/references/verify.md +6 -5
  91. package/claude/skills/task-board/SKILL.md +68 -60
  92. package/claude/skills/task-board/references/create-report.md +36 -0
  93. package/claude/skills/task-board/references/tasks.md +3 -1
  94. package/claude/skills/task-complete/SKILL.md +20 -18
  95. package/claude/skills/teach-workspace/SKILL.md +61 -33
  96. package/claude/skills/test-craft/SKILL.md +2 -1
  97. package/claude/skills/test-first/SKILL.md +15 -9
  98. package/claude/skills/ui-checklist/SKILL.md +18 -16
  99. package/claude/skills/ux-audit/SKILL.md +9 -10
  100. package/claude/skills/ux-measure/SKILL.md +22 -18
  101. package/claude/skills/video-craft/SKILL.md +15 -7
  102. package/claude/skills/write-human/SKILL.md +9 -1
  103. package/claude/skills/youtube-transcripts/SKILL.md +2 -1
  104. package/docs/agents/context-audit-checks.md +19 -1
  105. package/docs/agents/context-audit.md +2 -2
  106. package/docs/agents/driver.md +1 -1
  107. package/docs/agents/hooks.md +21 -1
  108. package/docs/agents/index.md +10 -14
  109. package/docs/agents/indexes.md +25 -29
  110. package/docs/agents/install-and-sync.md +124 -235
  111. package/docs/agents/markdown-audit-density.md +39 -0
  112. package/docs/agents/markdown-audit-weight.md +9 -7
  113. package/docs/agents/markdown-audit.md +18 -14
  114. package/docs/agents/migrate.md +41 -16
  115. package/docs/agents/output-shape.md +60 -45
  116. package/docs/agents/overview.md +36 -12
  117. package/docs/agents/routing.md +1 -1
  118. package/docs/agents/rule-citations.md +1 -1
  119. package/docs/agents/skills-reach.md +1 -1
  120. package/docs/agents/targets.md +25 -77
  121. package/docs/target/migrations.md +1 -1
  122. package/docs/target/projects.md +3 -3
  123. package/docs/target/scenarios.md +1 -1
  124. package/docs/workflow/ai-workflow.md +2 -2
  125. package/docs/workflow/parallel-features.md +2 -2
  126. package/docs/workflow/skill-map.md +1 -1
  127. package/docs/workflow/visual-design-workflow.md +1 -1
  128. package/governance/detect.toml +6 -1
  129. package/package.json +6 -3
  130. package/scripts/core/install-check.sh +1 -1
  131. package/src/commands/claude.ts +1 -2
  132. package/src/commands/context.ts +106 -7
  133. package/src/commands/hooks.ts +63 -0
  134. package/src/commands/markdown.ts +169 -32
  135. package/src/commands/pr.ts +1 -1
  136. package/src/commands/records.ts +0 -1
  137. package/src/context/audit.ts +256 -5
  138. package/src/context/classify/prompts.ts +1 -1
  139. package/src/context/gate.ts +23 -7
  140. package/src/context/shape-gated.ts +28 -0
  141. package/src/design/document.ts +1 -1
  142. package/src/design/tokens.ts +1 -1
  143. package/src/docs/read.ts +15 -21
  144. package/src/gate/measures.ts +15 -190
  145. package/src/gate/stages.ts +21 -23
  146. package/src/hooks/reply-links.ts +110 -0
  147. package/src/markdown/density.ts +258 -0
  148. package/src/markdown/gate.ts +57 -18
  149. package/src/markdown/reference-shape.ts +103 -0
  150. package/src/markdown/scope.ts +174 -0
  151. package/src/markdown/structure.ts +10 -60
  152. package/src/sandbox/equivalence.ts +7 -9
  153. package/src/shipped/references.ts +1 -1
  154. package/src/teach/browser/head.ts +2 -2
  155. package/src/teach/browser/sidebar.ts +3 -3
  156. package/src/teach/browser/theme.ts +2 -2
  157. package/src/tooling/inject.ts +2 -2
  158. package/src/tooling/package.ts +15 -18
  159. package/src/tooling/verify-stack.ts +6 -3
  160. package/tooling/astro/configs/.oxfmtrc.json +8 -0
  161. package/tooling/astro/configs/.prettierrc +13 -0
  162. package/tooling/astro/manifest.toml +13 -0
  163. package/tooling/astro/reference.md +15 -5
  164. package/tooling/base/configs/.oxfmtrc.json +7 -0
  165. package/tooling/base/configs/scripts/verify.ts +1 -1
  166. package/tooling/base/manifest.toml +6 -6
  167. package/tooling/base/reference.md +12 -9
  168. package/tooling/base/seeds/.lintstagedrc +1 -3
  169. package/tooling/base/seeds/canon/context/ci.md +3 -3
  170. package/tooling/base/seeds/canon/context/development.md +2 -2
  171. package/tooling/claude/manifest.toml +5 -1
  172. package/tooling/claude/seeds/.claude/hooks/reply-links.sh +72 -0
  173. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +22 -1
  174. package/tooling/claude/seeds/.claude/settings.json +4 -0
  175. package/tooling/go/reference.md +2 -2
  176. package/tooling/nextjs/configs/.oxlintrc.json +223 -0
  177. package/tooling/nextjs/manifest.toml +2 -5
  178. package/tooling/nextjs/reference.md +4 -4
  179. package/tooling/php/manifest.toml +6 -1
  180. package/tooling/php/reference.md +2 -2
  181. package/tooling/python/manifest.toml +6 -1
  182. package/tooling/python/reference.md +4 -4
  183. package/tooling/vite-react/manifest.toml +1 -4
  184. package/tooling/vite-react/reference.md +3 -3
  185. package/tooling/web/configs/.oxlintrc.json +213 -0
  186. package/tooling/web/manifest.toml +9 -13
  187. package/tooling/web/reference.md +14 -14
  188. package/docs/agents/commands.md +0 -154
  189. package/docs/agents/docs.md +0 -27
  190. package/docs/agents/drift-surfaces.md +0 -201
  191. package/docs/agents/gov-detect.md +0 -47
  192. package/docs/agents/scripting.md +0 -142
  193. package/src/markdown/growth.ts +0 -43
  194. package/tooling/base/configs/.prettierrc +0 -12
  195. package/tooling/base/seeds/.prettierignore +0 -0
  196. package/tooling/nextjs/configs/eslint.config.js +0 -91
  197. package/tooling/web/configs/eslint.config.js +0 -82
@@ -1,6 +1,6 @@
1
1
  import { type BodyLine, visibleText } from '@/markdown/scan'
2
2
 
3
- const HEADING = /^#{1,6}\s/
3
+ export const HEADING = /^#{1,6}\s/
4
4
 
5
5
  /**
6
6
  * A section marker holding its line alone, which is the signpost a document
@@ -65,10 +65,10 @@ const BOLD_LINE = /^\*\*([^*]+)\*\*\s*$/
65
65
  const SPAN_LABEL = /^`[^`]+`$/
66
66
  const MARKER_WIDTH = 20
67
67
 
68
- const LIST_ITEM = /^(\s*)([-*+]|\d+\.)\s+/
69
- const TABLE_ROW = /^\s*\|/
68
+ export const LIST_ITEM = /^(\s*)([-*+]|\d+\.)\s+/
69
+ export const TABLE_ROW = /^\s*\|/
70
70
  const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/
71
- const BLOCKQUOTE = /^\s*>/
71
+ export const BLOCKQUOTE = /^\s*>/
72
72
 
73
73
  /**
74
74
  * A sentence ends on terminal punctuation followed by the start of another
@@ -86,7 +86,7 @@ const BLOCKQUOTE = /^\s*>/
86
86
  * follows, and the price is that a span opening a fragment mid-paragraph reads
87
87
  * as a sentence start.
88
88
  */
89
- const SENTENCE_END = /[.!?]["'’”)\]]*(?=\s+(?:["'“(\[]*[A-Z]|`)|\s*$)/g
89
+ const SENTENCE_END = /[.!?]["'’”)\]]*(?=\s+(?:["'“([]*[A-Z]|`)|\s*$)/g
90
90
 
91
91
  /**
92
92
  * Checkpoints the structural measures run against.
@@ -109,8 +109,8 @@ const SENTENCE_END = /[.!?]["'’”)\]]*(?=\s+(?:["'“(\[]*[A-Z]|`)|\s*$)/g
109
109
  export const CHECKPOINTS = {
110
110
  run: 40,
111
111
  peerBullet: 130,
112
- bullet: 250,
113
- paragraph: 700,
112
+ bullet: 200,
113
+ paragraph: 350,
114
114
  sentences: 4,
115
115
  renderWidth: 80,
116
116
  cadence: 3,
@@ -336,7 +336,7 @@ function isTableRun(run: readonly BodyLine[]): boolean {
336
336
  * what lets a linked path reach the span test at all, since the markup around
337
337
  * it would fail the pattern the destination is still attached.
338
338
  */
339
- function isSectionMarker(text: string): boolean {
339
+ export function isSectionMarker(text: string): boolean {
340
340
  const marker = text.match(BOLD_LINE)
341
341
  if (!marker) return false
342
342
 
@@ -438,11 +438,7 @@ interface FoldedBullet extends BulletFinding {
438
438
  readonly text: string
439
439
  }
440
440
 
441
- /**
442
- * The walk `heavyBullets` reports from, keeping each past-checkpoint bullet's
443
- * text so `longBlocks` can ask whether a base copy of it exists. One walk
444
- * rather than two, for the reason `paragraphBlocks` is shared.
445
- */
441
+ /** The walk `heavyBullets` reports from. */
446
442
  function foldBullets(
447
443
  lines: readonly BodyLine[],
448
444
  checkpoints: Checkpoints,
@@ -579,10 +575,7 @@ function paragraphText(block: readonly BodyLine[]): string {
579
575
  * paragraph rule whenever the bullet rule was changed, and an author reading
580
576
  * the sentence cap would find no weight rule to read at all.
581
577
  *
582
- * The two numbers coincide today because paragraph and bullet weight measure
583
- * one population, sharing a median near 170 characters with no gap behind
584
- * either candidate. They are separate checkpoints regardless, so either moves
585
- * without dragging the other.
578
+ * They are separate checkpoints, so either moves without dragging the other.
586
579
  */
587
580
  export function heavyParagraphs(
588
581
  lines: readonly BodyLine[],
@@ -606,49 +599,6 @@ export function heavyParagraphs(
606
599
  return findings
607
600
  }
608
601
 
609
- export interface LongBlock {
610
- readonly kind: 'paragraph' | 'bullet'
611
- readonly line: number
612
- readonly characters: number
613
- /** The block's source lines folded into one, as the paragraph and bullet folds read it. */
614
- readonly text: string
615
- }
616
-
617
- /**
618
- * The paragraphs past the paragraph checkpoint and the top-level bullets past
619
- * the bullet checkpoint, each with its text.
620
- *
621
- * Weight alone, never the sentence cap, since the growth stage asks about the
622
- * two numbers the standard states in characters. The definitions are the
623
- * audit's own, so a block this names and a block the weight report names are
624
- * one set.
625
- */
626
- export function longBlocks(
627
- lines: readonly BodyLine[],
628
- checkpoints: Checkpoints,
629
- ): LongBlock[] {
630
- const blocks: LongBlock[] = []
631
-
632
- for (const block of paragraphBlocks(lines)) {
633
- const text = paragraphText(block)
634
- const characters = visibleText(text).length
635
- if (characters > checkpoints.paragraph) {
636
- blocks.push({
637
- kind: 'paragraph',
638
- line: block[0].number,
639
- characters,
640
- text,
641
- })
642
- }
643
- }
644
-
645
- for (const bullet of foldBullets(lines, checkpoints)) {
646
- blocks.push({ kind: 'bullet', ...bullet })
647
- }
648
-
649
- return blocks.sort((a, b) => a.line - b.line)
650
- }
651
-
652
602
  /** Punctuation either side of a word, so an opener is compared on its letters. */
653
603
  const WORD_EDGE = /^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu
654
604
 
@@ -674,7 +674,7 @@ async function pool<T, R>(
674
674
  size: number,
675
675
  work: (item: T, index: number) => Promise<R>,
676
676
  ): Promise<R[]> {
677
- const results: R[] = new Array(items.length)
677
+ const results: R[] = Array.from({ length: items.length })
678
678
  let next = 0
679
679
 
680
680
  async function lane(): Promise<void> {
@@ -827,14 +827,12 @@ export async function runEquivalence(
827
827
  const records: ArmRecord[] = [
828
828
  ...offline,
829
829
  ...anchored,
830
- ...skipped.map(
831
- (arm): ArmRecord => ({
832
- arm: armKey(arm),
833
- state: 'skipped-anchor',
834
- differs: [],
835
- masked: [],
836
- }),
837
- ),
830
+ ...skipped.map((arm): ArmRecord => ({
831
+ arm: armKey(arm),
832
+ state: 'skipped-anchor',
833
+ differs: [],
834
+ masked: [],
835
+ })),
838
836
  ].sort((x, y) => x.arm.localeCompare(y.arm))
839
837
 
840
838
  const counts: Record<ArmState, number> = {
@@ -132,7 +132,7 @@ const SAME_REPOSITORY =
132
132
  * `canon/context/` carries no equivalent pattern. The same resolution test
133
133
  * cannot separate a path every scaffolded project holds, such as
134
134
  * `canon/context/index.md`, from this repository's own domain entry, such
135
- * as `canon/context/context-model/indexes.md`, since both resolve here and only the
135
+ * as `canon/context/context-model.md`, since both resolve here and only the
136
136
  * second is a defect. That is a semantic read no pattern makes, so it stays
137
137
  * with the rule and the review checklist rather than joining this reader.
138
138
  */
@@ -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
+ }
@@ -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,7 +13,7 @@ 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
 
@@ -22,7 +22,7 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
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
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
 
@@ -34,9 +34,19 @@ Unit-test React island components (`.tsx`) only. Do not test `.astro` files. Pag
34
34
 
35
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,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'
@@ -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,7 +10,7 @@ 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
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.
@@ -21,7 +21,6 @@ Golden config files live in `tooling/base/configs/` and are copied into the targ
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,8 +73,7 @@ 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
@@ -78,8 +81,8 @@ Sticky negative knowledge. Do not relearn.
78
81
  | Script | What it does |
79
82
  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
80
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. |
81
- | `bun run check:format` | Asserts prettier formatting without writing |
82
- | `bun run format` | Writes prettier formatting in place |
84
+ | `bun run check:format` | Asserts oxfmt formatting without writing |
85
+ | `bun run format` | Writes oxfmt formatting in place |
83
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.
@@ -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
  }
@@ -22,9 +22,9 @@ Owns the GitHub Actions workflow that gates a merge: which events start a run, a
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
29
  ### Running CI locally
30
30
 
@@ -24,7 +24,7 @@ Owns how the project runs on a developer machine: installing the toolchain, the
24
24
  | Command | Purpose |
25
25
  | ---------------- | ---------------------------------------------------------------------------------------- |
26
26
  | `bun run check` | Gates locally and writes nothing. Fails on formatting drift until `bun run format` runs. |
27
- | `bun run format` | Auto-fix prettier formatting. |
27
+ | `bun run format` | Auto-fix oxfmt formatting. |
28
28
 
29
29
  ### Shell scripts
30
30
 
@@ -32,6 +32,6 @@ All `.sh` files live under `scripts/`. Do not place shell scripts outside `scrip
32
32
 
33
33
  ### Husky hooks
34
34
 
35
- - `pre-commit` runs `lint-staged` (prettier on staged files).
35
+ - `pre-commit` runs `lint-staged` (oxfmt on staged files).
36
36
  - `commit-msg` runs `commitlint` against the conventional commit format.
37
37
  - `pre-push` runs `bun run check`. When a markdown-bans audit tool is on PATH, it also gates on banned characters across every tracked markdown file except `CHANGELOG.md`. A push fails on formatting drift until `bun run format` runs, since the hook writes nothing.
@@ -21,4 +21,8 @@ scaffold = ""
21
21
  # array no longer tells it to ignore them, so the first sign is a memory file or
22
22
  # a task board appearing in someone's commit.
23
23
  [gitignore]
24
- "# Claude" = [".canon/", ".claude/worktrees/", ".claude/.tmp/"] # canon-keep-record-root
24
+ "# Claude" = [
25
+ ".canon/",
26
+ ".claude/worktrees/",
27
+ ".claude/.tmp/", # canon-keep-record-root
28
+ ]