@erclx/canon 5.38.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 (149) 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-pr/SKILL.md +56 -44
  48. package/claude/skills/git-ship/SKILL.md +43 -34
  49. package/claude/skills/git-ship/references/ci-watch.md +31 -0
  50. package/claude/skills/git-split/SKILL.md +28 -100
  51. package/claude/skills/git-split/references/independent.md +40 -0
  52. package/claude/skills/git-split/references/stacked.md +73 -0
  53. package/claude/skills/git-stage/SKILL.md +9 -7
  54. package/claude/skills/git-worktree/SKILL.md +27 -49
  55. package/claude/skills/git-worktree/references/cleanup.md +55 -0
  56. package/claude/skills/markdown-craft/SKILL.md +2 -2
  57. package/claude/skills/markdown-craft/references/markdown.md +82 -53
  58. package/claude/skills/plan-feature/SKILL.md +52 -46
  59. package/claude/skills/plan-feature/references/batches.md +46 -0
  60. package/claude/skills/plan-groundwork/SKILL.md +60 -74
  61. package/claude/skills/plan-groundwork/references/output.md +48 -0
  62. package/claude/skills/plan-intake/SKILL.md +28 -21
  63. package/claude/skills/plan-intake-answer/SKILL.md +14 -13
  64. package/claude/skills/read-frames/SKILL.md +2 -0
  65. package/claude/skills/record-screencast/SKILL.md +31 -11
  66. package/claude/skills/review-address/SKILL.md +76 -158
  67. package/claude/skills/review-address/references/rebase-only.md +34 -0
  68. package/claude/skills/review-craft/SKILL.md +27 -11
  69. package/claude/skills/review-pr/SKILL.md +91 -40
  70. package/claude/skills/review-ui/SKILL.md +62 -37
  71. package/claude/skills/review-ui/references/hosted-preview.md +30 -0
  72. package/claude/skills/role-orchestrator/SKILL.md +105 -74
  73. package/claude/skills/role-orchestrator/references/orchestrator-filing.md +26 -0
  74. package/claude/skills/role-planner/SKILL.md +66 -64
  75. package/claude/skills/role-reviewer/SKILL.md +57 -46
  76. package/claude/skills/role-worker/SKILL.md +68 -63
  77. package/claude/skills/search-craft/SKILL.md +7 -5
  78. package/claude/skills/session-compact/SKILL.md +16 -11
  79. package/claude/skills/session-map/SKILL.md +31 -10
  80. package/claude/skills/session-relay/SKILL.md +32 -65
  81. package/claude/skills/session-resume/SKILL.md +18 -13
  82. package/claude/skills/session-worktree/SKILL.md +70 -42
  83. package/claude/skills/systematic-debugging/SKILL.md +6 -1
  84. package/claude/skills/target-check/SKILL.md +13 -6
  85. package/claude/skills/target-setup/SKILL.md +39 -27
  86. package/claude/skills/task-board/SKILL.md +68 -60
  87. package/claude/skills/task-board/references/create-report.md +36 -0
  88. package/claude/skills/task-complete/SKILL.md +20 -18
  89. package/claude/skills/teach-workspace/SKILL.md +61 -33
  90. package/claude/skills/test-craft/SKILL.md +2 -1
  91. package/claude/skills/test-first/SKILL.md +15 -9
  92. package/claude/skills/ui-checklist/SKILL.md +18 -16
  93. package/claude/skills/ux-audit/SKILL.md +9 -10
  94. package/claude/skills/ux-measure/SKILL.md +22 -18
  95. package/claude/skills/video-craft/SKILL.md +15 -7
  96. package/claude/skills/write-human/SKILL.md +9 -1
  97. package/claude/skills/youtube-transcripts/SKILL.md +2 -1
  98. package/docs/agents/context-audit-checks.md +19 -1
  99. package/docs/agents/context-audit.md +2 -2
  100. package/docs/agents/driver.md +1 -1
  101. package/docs/agents/hooks.md +21 -1
  102. package/docs/agents/index.md +10 -14
  103. package/docs/agents/indexes.md +25 -29
  104. package/docs/agents/install-and-sync.md +124 -235
  105. package/docs/agents/markdown-audit-density.md +39 -0
  106. package/docs/agents/markdown-audit-weight.md +9 -7
  107. package/docs/agents/markdown-audit.md +18 -14
  108. package/docs/agents/migrate.md +41 -16
  109. package/docs/agents/output-shape.md +60 -45
  110. package/docs/agents/overview.md +36 -12
  111. package/docs/agents/routing.md +1 -1
  112. package/docs/agents/rule-citations.md +1 -1
  113. package/docs/agents/skills-reach.md +1 -1
  114. package/docs/agents/targets.md +25 -77
  115. package/docs/target/migrations.md +1 -1
  116. package/docs/target/projects.md +2 -2
  117. package/docs/workflow/ai-workflow.md +2 -2
  118. package/docs/workflow/parallel-features.md +2 -2
  119. package/docs/workflow/skill-map.md +1 -1
  120. package/docs/workflow/visual-design-workflow.md +1 -1
  121. package/package.json +1 -1
  122. package/src/commands/claude.ts +1 -1
  123. package/src/commands/context.ts +106 -7
  124. package/src/commands/hooks.ts +63 -0
  125. package/src/commands/markdown.ts +169 -32
  126. package/src/context/audit.ts +256 -5
  127. package/src/context/classify/prompts.ts +1 -1
  128. package/src/context/gate.ts +23 -7
  129. package/src/context/shape-gated.ts +28 -0
  130. package/src/design/document.ts +1 -1
  131. package/src/design/tokens.ts +1 -1
  132. package/src/gate/measures.ts +15 -190
  133. package/src/gate/stages.ts +7 -23
  134. package/src/hooks/reply-links.ts +110 -0
  135. package/src/markdown/density.ts +258 -0
  136. package/src/markdown/gate.ts +57 -18
  137. package/src/markdown/reference-shape.ts +103 -0
  138. package/src/markdown/scope.ts +174 -0
  139. package/src/markdown/structure.ts +9 -59
  140. package/src/shipped/references.ts +1 -1
  141. package/tooling/claude/seeds/.claude/hooks/reply-links.sh +72 -0
  142. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +22 -1
  143. package/tooling/claude/seeds/.claude/settings.json +4 -0
  144. package/docs/agents/commands.md +0 -154
  145. package/docs/agents/docs.md +0 -27
  146. package/docs/agents/drift-surfaces.md +0 -201
  147. package/docs/agents/gov-detect.md +0 -47
  148. package/docs/agents/scripting.md +0 -142
  149. package/src/markdown/growth.ts +0 -43
@@ -67,8 +67,7 @@ export const REQUIRED_SECTIONS_BY_FOLDER: Readonly<
67
67
  * The closed set of H2 sections per audited folder name, which a required
68
68
  * section belongs to. An entry carrying another H2 is a finding under
69
69
  * `--gate`, since the set is what makes a context entry cheap to find a fact
70
- * in: a domain's own topic names go one level down as H3 items under
71
- * `Decisions` and `Gotchas`.
70
+ * in: a domain's folders go one level down as H3 items under `Layout`.
72
71
  *
73
72
  * Held beside the required lists for the same reason they are held here, and
74
73
  * keyed on the folder whose standard states it.
@@ -76,9 +75,35 @@ export const REQUIRED_SECTIONS_BY_FOLDER: Readonly<
76
75
  export const ALLOWED_SECTIONS_BY_FOLDER: Readonly<
77
76
  Record<string, readonly string[]>
78
77
  > = {
79
- context: ['Overview', 'Layout', 'Decisions', 'Gotchas'],
78
+ context: ['Overview', 'Layout', 'Gotchas'],
80
79
  }
81
80
 
81
+ /**
82
+ * H2 sections the standard once allowed and now forbids, per audited folder.
83
+ *
84
+ * Kept apart from the extra sections because every unrebuilt entry still
85
+ * carries one, and an extra section gates under `--gate` everywhere. A retired
86
+ * section is a shape finding instead, which gates only where the folder was
87
+ * rebuilt, per `SHAPE_GATED_FOLDERS`.
88
+ */
89
+ export const RETIRED_SECTIONS_BY_FOLDER: Readonly<
90
+ Record<string, readonly string[]>
91
+ > = {
92
+ context: ['Decisions'],
93
+ }
94
+
95
+ /** Bullets a `Gotchas` section may hold, counted at any indent. */
96
+ export const GOTCHA_CAP = 5
97
+
98
+ /** Bullets a folder H3 under `Layout` may hold, counted at any indent. */
99
+ export const LAYOUT_BULLET_CAP = 5
100
+
101
+ /** Paragraphs a folder H3 under `Layout` may hold, being its one lead. */
102
+ export const LAYOUT_PARAGRAPH_CAP = 1
103
+
104
+ /** The one Layout H3 not titled with a path, holding the trivial folders. */
105
+ export const OTHER_FOLDERS = 'Other folders'
106
+
82
107
  /**
83
108
  * The context standard's list, kept as its own export because the audit record
84
109
  * has published it flat under `checkpoints.requiredSections` since before any
@@ -210,6 +235,34 @@ export interface BareReferenceFinding {
210
235
  readonly name: string
211
236
  }
212
237
 
238
+ /**
239
+ * Each way an entry breaks the section shape the context standard states.
240
+ *
241
+ * `retired-section` is a `Decisions` H2. The three `gotcha-*` kinds are a
242
+ * `Gotchas` section past its bullet cap, holding a heading, or holding a
243
+ * paragraph. The three `layout-*` kinds are a Layout H3 titled neither a path
244
+ * nor `Other folders`, a folder H3 past its bullet or paragraph cap, and a
245
+ * heading below H3 under `Layout`.
246
+ */
247
+ export type ShapeKind =
248
+ | 'retired-section'
249
+ | 'gotcha-count'
250
+ | 'gotcha-heading'
251
+ | 'gotcha-paragraph'
252
+ | 'layout-heading'
253
+ | 'layout-bullets'
254
+ | 'layout-paragraphs'
255
+ | 'layout-depth'
256
+
257
+ export interface ShapeIssue {
258
+ readonly line: number
259
+ readonly kind: ShapeKind
260
+ /** The heading or line as written, so a report names where to look. */
261
+ readonly text: string
262
+ /** Bullets or paragraphs counted, on the two cap kinds alone. */
263
+ readonly count?: number
264
+ }
265
+
213
266
  export interface EntryReport {
214
267
  readonly rel: string
215
268
  /**
@@ -247,6 +300,12 @@ export interface EntryReport {
247
300
  * all is `extraSections`.
248
301
  */
249
302
  readonly otherSections: readonly string[]
303
+ /**
304
+ * Breaks of the section shape, in document order, and empty outside the
305
+ * governed folder. Whether one gates depends on where the entry sits, which
306
+ * `shapeFindings` decides rather than the measure.
307
+ */
308
+ readonly shape: readonly ShapeIssue[]
250
309
  /**
251
310
  * Whether the file declares itself a skeleton, which excludes it from the
252
311
  * section check alone. Every other measure still reads it, since a stub is
@@ -483,6 +542,7 @@ const H2_TEXT = /^##\s+(.+?)\s*$/
483
542
  function otherSections(
484
543
  lines: readonly BodyLine[],
485
544
  allowed: readonly string[],
545
+ retired: readonly string[],
486
546
  ): string[] {
487
547
  if (allowed.length === 0) return []
488
548
 
@@ -492,12 +552,161 @@ function otherSections(
492
552
  if (line.fenced) continue
493
553
 
494
554
  const match = line.text.match(H2_TEXT)
495
- if (match && !allowed.includes(match[1])) found.push(match[1])
555
+ if (match && !allowed.includes(match[1]) && !retired.includes(match[1])) {
556
+ found.push(match[1])
557
+ }
496
558
  }
497
559
 
498
560
  return found
499
561
  }
500
562
 
563
+ const HEADING_LEVEL = /^(#{1,6})\s+(.+?)\s*$/
564
+ const LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s+/
565
+ const INDENTED = /^\s+\S/
566
+ const FOLDER_TITLE = /^`[^`]+`$/
567
+
568
+ interface FolderHeading {
569
+ readonly line: number
570
+ readonly text: string
571
+ bullets: number
572
+ paragraphs: number
573
+ }
574
+
575
+ /**
576
+ * Finds where the entry breaks the section shape.
577
+ *
578
+ * Bullets are counted at any indent and across any paragraph, so a list split
579
+ * by a bold label or nested one level down still counts against the cap it
580
+ * would otherwise slip under. A line continuing a list item, lazily or
581
+ * indented after a blank, belongs to the item rather than opening a paragraph,
582
+ * which is what keeps a wrapped bullet from reading as prose.
583
+ *
584
+ * A cap reports once per section or folder heading however far past it the
585
+ * count runs, since the fix is one cut either way. `Other folders` is exempt
586
+ * from both folder caps, its one-line bullets being held by the bullet run
587
+ * limit `canon markdown audit` already gates.
588
+ */
589
+ function shapeIssues(
590
+ lines: readonly BodyLine[],
591
+ retired: readonly string[],
592
+ ): ShapeIssue[] {
593
+ const issues: ShapeIssue[] = []
594
+ let section: string | undefined
595
+ let gotchas: { line: number; bullets: number } | undefined
596
+ let folder: FolderHeading | undefined
597
+ let inItem = false
598
+ let inParagraph = false
599
+ let afterBlank = false
600
+
601
+ const closeFolder = (): void => {
602
+ if (folder && folder.text !== OTHER_FOLDERS) {
603
+ if (folder.paragraphs > LAYOUT_PARAGRAPH_CAP) {
604
+ issues.push({
605
+ line: folder.line,
606
+ kind: 'layout-paragraphs',
607
+ text: folder.text,
608
+ count: folder.paragraphs,
609
+ })
610
+ }
611
+ if (folder.bullets > LAYOUT_BULLET_CAP) {
612
+ issues.push({
613
+ line: folder.line,
614
+ kind: 'layout-bullets',
615
+ text: folder.text,
616
+ count: folder.bullets,
617
+ })
618
+ }
619
+ }
620
+ folder = undefined
621
+ }
622
+
623
+ const closeGotchas = (): void => {
624
+ if (gotchas && gotchas.bullets > GOTCHA_CAP) {
625
+ issues.push({
626
+ line: gotchas.line,
627
+ kind: 'gotcha-count',
628
+ text: 'Gotchas',
629
+ count: gotchas.bullets,
630
+ })
631
+ }
632
+ gotchas = undefined
633
+ }
634
+
635
+ for (const line of lines) {
636
+ if (line.fenced) continue
637
+
638
+ const heading = line.text.match(HEADING_LEVEL)
639
+ if (heading) {
640
+ const level = heading[1].length
641
+ const text = heading[2]
642
+ inItem = false
643
+ inParagraph = false
644
+ afterBlank = false
645
+
646
+ if (level <= 2) {
647
+ closeFolder()
648
+ closeGotchas()
649
+ section = level === 2 ? text : undefined
650
+ if (level === 2 && retired.includes(text)) {
651
+ issues.push({ line: line.number, kind: 'retired-section', text })
652
+ }
653
+ if (section === 'Gotchas') gotchas = { line: line.number, bullets: 0 }
654
+ continue
655
+ }
656
+
657
+ if (section === 'Gotchas') {
658
+ issues.push({ line: line.number, kind: 'gotcha-heading', text })
659
+ } else if (section === 'Layout' && level === 3) {
660
+ closeFolder()
661
+ folder = { line: line.number, text, bullets: 0, paragraphs: 0 }
662
+ if (!FOLDER_TITLE.test(text) && text !== OTHER_FOLDERS) {
663
+ issues.push({ line: line.number, kind: 'layout-heading', text })
664
+ }
665
+ } else if (section === 'Layout') {
666
+ issues.push({ line: line.number, kind: 'layout-depth', text })
667
+ }
668
+ continue
669
+ }
670
+
671
+ if (line.text.trim() === '') {
672
+ inParagraph = false
673
+ afterBlank = true
674
+ continue
675
+ }
676
+
677
+ if (LIST_ITEM.test(line.text)) {
678
+ inItem = true
679
+ inParagraph = false
680
+ afterBlank = false
681
+ if (gotchas) gotchas.bullets++
682
+ if (folder) folder.bullets++
683
+ continue
684
+ }
685
+
686
+ const continues = inItem && (!afterBlank || INDENTED.test(line.text))
687
+ afterBlank = false
688
+ if (continues) continue
689
+
690
+ inItem = false
691
+ if (inParagraph) continue
692
+ inParagraph = true
693
+
694
+ if (gotchas) {
695
+ issues.push({
696
+ line: line.number,
697
+ kind: 'gotcha-paragraph',
698
+ text: line.text.trim(),
699
+ })
700
+ }
701
+ if (folder) folder.paragraphs++
702
+ }
703
+
704
+ closeFolder()
705
+ closeGotchas()
706
+
707
+ return issues.sort((a, b) => a.line - b.line)
708
+ }
709
+
501
710
  /**
502
711
  * Finds the references naming a sibling entry by bare filename.
503
712
  *
@@ -557,6 +766,9 @@ export function measureEntry(
557
766
  siblings: readonly string[] = [],
558
767
  required: readonly string[] = governsContent ? REQUIRED_SECTIONS : [],
559
768
  allowed: readonly string[] = [],
769
+ retired: readonly string[] = governsContent
770
+ ? RETIRED_SECTIONS_BY_FOLDER.context
771
+ : [],
560
772
  ): EntryReport {
561
773
  const lines = bodyLines(source)
562
774
 
@@ -567,7 +779,8 @@ export function measureEntry(
567
779
  provenance: governsContent ? provenance(lines) : [],
568
780
  bareReferences: bareReferences(lines, siblings),
569
781
  sections: declaredSections(lines, required),
570
- otherSections: otherSections(lines, allowed),
782
+ otherSections: otherSections(lines, allowed, retired),
783
+ shape: governsContent ? shapeIssues(lines, retired) : [],
571
784
  stub: isStubSeed(source),
572
785
  governed: governsContent,
573
786
  }
@@ -648,6 +861,7 @@ export async function measureFolders(
648
861
  names.filter((name) => name !== self),
649
862
  requiredSections(folder),
650
863
  allowedSections(folder),
864
+ retiredSections(folder),
651
865
  ),
652
866
  )
653
867
  }
@@ -675,6 +889,13 @@ export function allowedSections(folder: AuditedFolder): readonly string[] {
675
889
  : []
676
890
  }
677
891
 
892
+ /** Names the H2 sections the folder's standard retired, empty where it retired none. */
893
+ export function retiredSections(folder: AuditedFolder): readonly string[] {
894
+ return Object.hasOwn(RETIRED_SECTIONS_BY_FOLDER, folder.name)
895
+ ? RETIRED_SECTIONS_BY_FOLDER[folder.name]
896
+ : []
897
+ }
898
+
678
899
  /**
679
900
  * Reports whether a bare sibling name here is a reference by construction.
680
901
  *
@@ -771,3 +992,33 @@ export function extraSections(
771
992
  .filter((entry) => !entry.stub && entry.otherSections.length > 0)
772
993
  .map((entry) => ({ rel: entry.rel, headings: entry.otherSections }))
773
994
  }
995
+
996
+ export interface ShapeFinding {
997
+ /** Repo-relative path of the entry breaking the shape. */
998
+ readonly rel: string
999
+ /** Whether the entry sits where the shape gates under `--gate`. */
1000
+ readonly gated: boolean
1001
+ /** Every break in the entry, never empty. */
1002
+ readonly issues: readonly ShapeIssue[]
1003
+ }
1004
+
1005
+ /**
1006
+ * Names each entry breaking the section shape, and whether it gates.
1007
+ *
1008
+ * Where it gates is the caller's predicate rather than a list read here, since
1009
+ * the folders rebuilt to the shape are named by path from the repository root
1010
+ * and `measureFolders` holds only paths from the audit root. A stub seed is
1011
+ * dropped for the reason `extraSections` drops one.
1012
+ */
1013
+ export function shapeFindings(
1014
+ entries: readonly EntryReport[],
1015
+ isGated: (rel: string) => boolean,
1016
+ ): ShapeFinding[] {
1017
+ return entries
1018
+ .filter((entry) => !entry.stub && entry.shape.length > 0)
1019
+ .map((entry) => ({
1020
+ rel: entry.rel,
1021
+ gated: isGated(entry.rel),
1022
+ issues: entry.shape,
1023
+ }))
1024
+ }
@@ -5,7 +5,7 @@
5
5
  * mode) and `sweep-v1.md` (sweep mode). The diff prompt is verbatim. The sweep
6
6
  * prompt was tuned past `sweep-v1.md` with one decision check, measured
7
7
  * against the groundwork's labelled set in
8
- * `canon/context/context-model/overview.md`.
8
+ * `canon/context/context-model.md`.
9
9
  *
10
10
  * Each prompt already states its own rules per canonical doc type inside one
11
11
  * string, which is the shape the spike measured, so there is one prompt per
@@ -1,4 +1,8 @@
1
- import type { ExtraSectionFinding, SectionFinding } from '@/context/audit'
1
+ import type {
2
+ ExtraSectionFinding,
3
+ SectionFinding,
4
+ ShapeFinding,
5
+ } from '@/context/audit'
2
6
  import type { FolderDrift } from '@/context/index-drift'
3
7
 
4
8
  export interface GateInput {
@@ -15,6 +19,11 @@ export interface GateInput {
15
19
  readonly sections: readonly SectionFinding[]
16
20
  /** H2 headings outside a folder's closed set, gating under `--gate` alone. */
17
21
  readonly extraSections: readonly ExtraSectionFinding[]
22
+ /**
23
+ * Section shape breaks, gating under `--gate` only where the finding is
24
+ * marked gated, so an unrebuilt entry reports and never fails a push.
25
+ */
26
+ readonly shape: readonly ShapeFinding[]
18
27
  readonly drift: readonly FolderDrift[]
19
28
  /**
20
29
  * Whether the caller asked for the widened gate. False leaves a missing
@@ -36,17 +45,19 @@ export function hasDrift(drift: readonly FolderDrift[]): boolean {
36
45
  *
37
46
  * An unresolved citation is a broken pointer and gates unconditionally, and so
38
47
  * does a requirements record past its own word cap: the record states its rule
39
- * for itself, which makes it a fact rather than a threshold a reader weighs. The findings `--gate` adds are
40
- * the ones answerable from the file itself: a required section it does not
41
- * declare, an H2 outside the closed set, and an index disagreeing with its folder. Entry length, depth, bullet,
42
- * table, provenance, and the record's claim coverage are judgments, so they
43
- * stay out under both modes.
48
+ * for itself, which makes it a fact rather than a threshold a reader weighs.
49
+ * The findings `--gate` adds are the ones answerable from the file itself: a
50
+ * required section it does not declare, an H2 outside the closed set, a shape
51
+ * break in a folder rebuilt to the shape, and an index disagreeing with its
52
+ * folder. Entry length, depth, bullet, table, provenance, and the record's
53
+ * claim coverage are judgments, so they stay out under both modes.
44
54
  */
45
55
  export function isGating({
46
56
  unresolvedCitations,
47
57
  requirementsOverWords,
48
58
  sections,
49
59
  extraSections,
60
+ shape,
50
61
  drift,
51
62
  widened,
52
63
  }: GateInput): boolean {
@@ -54,5 +65,10 @@ export function isGating({
54
65
  if (requirementsOverWords) return true
55
66
  if (!widened) return false
56
67
 
57
- return sections.length > 0 || extraSections.length > 0 || hasDrift(drift)
68
+ return (
69
+ sections.length > 0 ||
70
+ extraSections.length > 0 ||
71
+ shape.some((finding) => finding.gated) ||
72
+ hasDrift(drift)
73
+ )
58
74
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Path prefixes, from the repository root, whose context entries gate on the
3
+ * section shape under `--gate`.
4
+ *
5
+ * Every other entry still reports its shape findings and never fails a push,
6
+ * since the entries are rebuilt to the shape a batch at a time and an unrebuilt
7
+ * one carries `## Decisions` by construction. A batch rebuilding a folder adds
8
+ * its prefix here in the same change, which is what holds the folder to the
9
+ * shape from then on.
10
+ *
11
+ * Read against the repository root rather than the audit root, so a seed tree
12
+ * audited at its own root is listed by its path in the toolkit, such as
13
+ * `tooling/base/seeds/`, and never collides with the entry a target installs
14
+ * from it.
15
+ */
16
+ export const SHAPE_GATED_FOLDERS: readonly string[] = [
17
+ 'canon/context/context-model.md',
18
+ 'canon/context/design.md',
19
+ 'canon/context/web.md',
20
+ ]
21
+
22
+ /** Whether a repository-relative entry path sits under a gated prefix. */
23
+ export function isShapeGated(
24
+ rel: string,
25
+ prefixes: readonly string[] = SHAPE_GATED_FOLDERS,
26
+ ): boolean {
27
+ return prefixes.some((prefix) => rel.startsWith(prefix))
28
+ }
@@ -95,7 +95,7 @@ function borderRows(tokens: readonly BorderToken[]): string[][] {
95
95
  */
96
96
  const LEAD = [
97
97
  'Authoring guidance: `claude/skills/canvas-board/references/design.md`. Rendered from `src/design/tokens.ts` by `canon design regen`, so edit the module and never this file.',
98
- 'Why each value was chosen lives in `canon/context/design/tokens.md`.',
98
+ 'Why each value was chosen lives in `canon/context/design.md`.',
99
99
  ].join(' ')
100
100
 
101
101
  function bullets(rules: readonly string[]): string {
@@ -59,7 +59,7 @@ export interface BorderToken {
59
59
  /**
60
60
  * Every prose slot is a list of one-line rules rather than a string, so a
61
61
  * bullet is the unit the renderer emits and the prose budget counts, and a
62
- * value needing a paragraph of why goes to `canon/context/design/tokens.md`.
62
+ * value needing a paragraph of why goes to `canon/context/design.md`.
63
63
  */
64
64
  export interface DesignTokens {
65
65
  readonly personality: string