@erclx/canon 5.40.0 → 5.42.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 (187) hide show
  1. package/README.md +34 -17
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/references/adopted.md +34 -14
  4. package/claude/skills/auto-ship/REQUIREMENT.md +2 -0
  5. package/claude/skills/auto-ship/SKILL.md +28 -2
  6. package/claude/skills/auto-ship/references/draft-mark.md +32 -6
  7. package/claude/skills/auto-ship/references/failure-recovery.md +15 -13
  8. package/claude/skills/auto-ship/references/fan-in.md +59 -0
  9. package/claude/skills/auto-ship/references/tier-failures.md +31 -10
  10. package/claude/skills/auto-ship/references/ui-checklist.md +9 -4
  11. package/claude/skills/auto-ship/references/worktree-name.md +9 -3
  12. package/claude/skills/build-in-slices/references/adopted.md +35 -21
  13. package/claude/skills/canvas-board/references/canvas.md +17 -6
  14. package/claude/skills/canvas-board/references/design.md +26 -7
  15. package/claude/skills/code-craft/references/adopted.md +58 -20
  16. package/claude/skills/codebase-layout/references/adopted.md +49 -16
  17. package/claude/skills/codebase-layout/references/frontend.md +11 -4
  18. package/claude/skills/codebase-layout/references/tests.md +4 -2
  19. package/claude/skills/context-fold/references/classify.md +10 -15
  20. package/claude/skills/context-fold/references/completion-report.md +11 -9
  21. package/claude/skills/context-fold/references/context-refresh.md +26 -9
  22. package/claude/skills/context-fold/references/unusable-baseline.md +5 -3
  23. package/claude/skills/create-rule/REQUIREMENT.md +5 -0
  24. package/claude/skills/create-rule/SKILL.md +9 -1
  25. package/claude/skills/create-rule/references/checklist.md +26 -0
  26. package/claude/skills/create-rule/references/rule.md +98 -20
  27. package/claude/skills/create-skill/SKILL.md +1 -0
  28. package/claude/skills/create-skill/references/skill-disclosure.md +10 -2
  29. package/claude/skills/create-skill/references/skill-frontmatter.md +43 -0
  30. package/claude/skills/create-skill/references/skill-paths.md +7 -4
  31. package/claude/skills/create-skill/references/skill-practice.md +6 -3
  32. package/claude/skills/create-skill/references/skill-requirement.md +4 -2
  33. package/claude/skills/create-skill/references/skill.md +67 -44
  34. package/claude/skills/decision-escalate/REQUIREMENT.md +1 -1
  35. package/claude/skills/decision-escalate/SKILL.md +1 -2
  36. package/claude/skills/deploy-app/references/vercel.md +2 -1
  37. package/claude/skills/deprecation-migration/references/adopted.md +44 -25
  38. package/claude/skills/design-taste/references/adopted.md +9 -1
  39. package/claude/skills/design-taste/references/craft.md +3 -1
  40. package/claude/skills/design-taste/references/kinds.md +8 -7
  41. package/claude/skills/design-taste/references/preflight.md +12 -0
  42. package/claude/skills/design-taste/references/systems.md +7 -6
  43. package/claude/skills/design-taste/references/tells.md +24 -25
  44. package/claude/skills/design-taste/references/vocabulary.md +7 -5
  45. package/claude/skills/document-health/references/axes.md +54 -30
  46. package/claude/skills/draft-and-pick/references/live-arms.md +2 -2
  47. package/claude/skills/draft-doc/references/context.md +4 -3
  48. package/claude/skills/draft-doc/references/docs.md +6 -3
  49. package/claude/skills/draft-doc/references/readme/agent-facing.md +6 -0
  50. package/claude/skills/draft-doc/references/readme/application.md +8 -2
  51. package/claude/skills/draft-doc/references/readme/cli.md +5 -1
  52. package/claude/skills/draft-doc/references/readme/library.md +4 -0
  53. package/claude/skills/draft-doc/references/readme/plugin.md +9 -1
  54. package/claude/skills/draft-doc/references/readme.md +30 -13
  55. package/claude/skills/draft-doc/references/requirements.md +17 -4
  56. package/claude/skills/draft-doc/references/wiki-kind.md +20 -10
  57. package/claude/skills/draft-figure/references/architecture-set.md +7 -3
  58. package/claude/skills/draft-figure/references/figures.md +8 -6
  59. package/claude/skills/draft-figure/references/hand-drawn.md +3 -1
  60. package/claude/skills/draft-figure/references/mermaid-path.md +19 -9
  61. package/claude/skills/draft-figure/references/mermaid.md +10 -5
  62. package/claude/skills/draft-ready/references/ready.md +22 -15
  63. package/claude/skills/git-branch/REQUIREMENT.md +17 -6
  64. package/claude/skills/git-branch/SKILL.md +63 -7
  65. package/claude/skills/git-branch/references/branch.md +11 -1
  66. package/claude/skills/git-branch/references/slug.md +17 -5
  67. package/claude/skills/git-commit/references/commit.md +8 -0
  68. package/claude/skills/git-commit/references/versioning.md +23 -17
  69. package/claude/skills/git-followup/REQUIREMENT.md +1 -1
  70. package/claude/skills/git-followup/SKILL.md +5 -6
  71. package/claude/skills/git-issue/references/issue.md +29 -16
  72. package/claude/skills/git-pr/REQUIREMENT.md +1 -5
  73. package/claude/skills/git-pr/SKILL.md +11 -22
  74. package/claude/skills/git-pr/references/final-command.md +21 -8
  75. package/claude/skills/git-pr/references/labels.md +45 -28
  76. package/claude/skills/git-pr/references/pr.md +28 -9
  77. package/claude/skills/git-pr/references/publish.md +42 -28
  78. package/claude/skills/git-pr/references/task-number.md +20 -8
  79. package/claude/skills/git-ship/SKILL.md +1 -1
  80. package/claude/skills/plan-feature/references/batches.md +48 -1
  81. package/claude/skills/plan-feature/references/plan-lifecycle.md +10 -3
  82. package/claude/skills/plan-feature/references/plan.md +59 -20
  83. package/claude/skills/plan-groundwork/references/groundwork-spikes.md +4 -1
  84. package/claude/skills/plan-groundwork/references/groundwork.md +14 -16
  85. package/claude/skills/plan-intake/references/intake.md +29 -24
  86. package/claude/skills/plan-intake-answer/REQUIREMENT.md +1 -1
  87. package/claude/skills/plan-intake-answer/SKILL.md +1 -2
  88. package/claude/skills/pr-evidence/REQUIREMENT.md +40 -0
  89. package/claude/skills/pr-evidence/SKILL.md +53 -0
  90. package/claude/skills/pr-evidence/references/evidence.md +150 -0
  91. package/claude/skills/review-address/references/post-review-findings.md +10 -3
  92. package/claude/skills/review-address/references/rebase-conflicts.md +22 -7
  93. package/claude/skills/review-address/references/rest-reads.md +3 -1
  94. package/claude/skills/review-craft/references/adopted.md +35 -18
  95. package/claude/skills/review-craft/references/security.md +42 -7
  96. package/claude/skills/review-craft/references/ui.md +23 -8
  97. package/claude/skills/review-pr/SKILL.md +6 -6
  98. package/claude/skills/review-pr/references/body-variants.md +29 -9
  99. package/claude/skills/review-pr/references/close-out.md +20 -5
  100. package/claude/skills/review-pr/references/key-changes.md +27 -3
  101. package/claude/skills/review-pr/references/post.md +83 -31
  102. package/claude/skills/review-pr/references/scope.md +54 -19
  103. package/claude/skills/review-ui/SKILL.md +2 -0
  104. package/claude/skills/review-ui/references/close-out.md +49 -0
  105. package/claude/skills/review-ui/references/driving.md +24 -4
  106. package/claude/skills/review-ui/references/post.md +43 -13
  107. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +67 -17
  108. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +81 -40
  109. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +52 -15
  110. package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +48 -26
  111. package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +12 -13
  112. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +72 -36
  113. package/claude/skills/role-orchestrator/references/orchestrator-output.md +12 -4
  114. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +64 -24
  115. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +80 -26
  116. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +41 -18
  117. package/claude/skills/role-orchestrator/references/orchestrator-resume.md +19 -7
  118. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +22 -12
  119. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +15 -6
  120. package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +25 -7
  121. package/claude/skills/role-orchestrator/references/orchestrator-watch.md +34 -8
  122. package/claude/skills/role-orchestrator/scripts/poll.ts +16 -24
  123. package/claude/skills/role-worker/REQUIREMENT.md +2 -0
  124. package/claude/skills/role-worker/SKILL.md +17 -0
  125. package/claude/skills/search-craft/references/adopted.md +18 -6
  126. package/claude/skills/session-map/references/session.md +26 -7
  127. package/claude/skills/session-worktree/references/main-root-writes.md +25 -8
  128. package/claude/skills/systematic-debugging/references/adopted.md +30 -16
  129. package/claude/skills/target-check/references/domains.md +39 -11
  130. package/claude/skills/target-setup/references/detect.md +80 -65
  131. package/claude/skills/target-setup/references/indexes.md +8 -6
  132. package/claude/skills/task-board/references/board.md +86 -30
  133. package/claude/skills/task-board/references/decline.md +29 -13
  134. package/claude/skills/task-board/references/tasks-decline.md +12 -5
  135. package/claude/skills/task-board/references/tasks.md +65 -33
  136. package/claude/skills/task-complete/references/archive.md +41 -17
  137. package/claude/skills/task-complete/references/receipt-sweep.md +23 -9
  138. package/claude/skills/task-complete/references/unusable-baseline.md +10 -3
  139. package/claude/skills/teach-workspace/REQUIREMENT.md +1 -1
  140. package/claude/skills/teach-workspace/SKILL.md +1 -2
  141. package/claude/skills/teach-workspace/references/glossary.md +6 -2
  142. package/claude/skills/teach-workspace/references/lesson-build.md +44 -16
  143. package/claude/skills/teach-workspace/references/lesson-craft.md +12 -7
  144. package/claude/skills/teach-workspace/references/pedagogy.md +6 -3
  145. package/claude/skills/teach-workspace/references/promotion-fold.md +17 -11
  146. package/claude/skills/teach-workspace/references/promotion.md +31 -8
  147. package/claude/skills/teach-workspace/references/revisit.md +8 -2
  148. package/claude/skills/teach-workspace/references/teach.md +17 -9
  149. package/claude/skills/test-craft/references/adopted.md +48 -14
  150. package/claude/skills/test-craft/references/component.md +7 -4
  151. package/claude/skills/test-craft/references/e2e.md +25 -7
  152. package/claude/skills/test-craft/references/unit.md +3 -1
  153. package/claude/skills/test-first/references/adopted.md +12 -4
  154. package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
  155. package/claude/skills/ui-checklist/SKILL.md +1 -1
  156. package/claude/skills/video-craft/references/adopted.md +62 -18
  157. package/claude/skills/write-human/references/adopted.md +21 -8
  158. package/docs/workflow/parallel-features.md +2 -1
  159. package/docs/workflow/skill-map.md +2 -1
  160. package/package.json +1 -1
  161. package/src/claude/cases/git.ts +4 -0
  162. package/src/claude/cases/workflow.ts +5 -0
  163. package/src/commands/pr.ts +174 -37
  164. package/src/commands/records.ts +11 -3
  165. package/src/context/shape-gated.ts +1 -0
  166. package/src/markdown/scope.ts +150 -2
  167. package/src/pr/frames.ts +53 -9
  168. package/src/pr/lift.ts +6 -3
  169. package/src/pr/review-state.ts +182 -0
  170. package/src/records/validate.ts +1 -0
  171. package/src/tasks/answers.ts +79 -2
  172. package/src/tasks/fan-in.ts +176 -0
  173. package/src/tasks/stack.ts +1 -1
  174. package/tooling/astro/reference.md +80 -39
  175. package/tooling/base/reference.md +63 -32
  176. package/tooling/base/seeds/canon/context/ci.md +7 -15
  177. package/tooling/base/seeds/canon/context/development.md +14 -14
  178. package/tooling/claude/reference.md +40 -22
  179. package/tooling/cloudflare/reference.md +31 -12
  180. package/tooling/go/reference.md +47 -23
  181. package/tooling/nextjs/reference.md +52 -19
  182. package/tooling/php/reference.md +35 -16
  183. package/tooling/python/reference.md +47 -30
  184. package/tooling/shell/reference.md +6 -5
  185. package/tooling/vite-react/reference.md +49 -21
  186. package/tooling/web/reference.md +130 -70
  187. package/claude/skills/git-pr/references/evidence.md +0 -124
@@ -1,8 +1,15 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
3
  import { basename, isAbsolute, relative, resolve } from 'node:path'
4
+ import { branchRefs } from '@/git/worktree'
5
+ import { fanInBranch, fanInFindings, readFanInMembers } from '@/tasks/fan-in'
4
6
  import { isUnder } from '@/tasks/paths'
5
- import { JUDGED_APART_REMEDY, readLivePlans, stackPairs } from '@/tasks/stack'
7
+ import {
8
+ JUDGED_APART_REMEDY,
9
+ readLivePlans,
10
+ type StackPlan,
11
+ stackPairs,
12
+ } from '@/tasks/stack'
6
13
  import { recordDir, recordDirs } from '@/roots/record'
7
14
  import {
8
15
  hasStagedBatches,
@@ -49,6 +56,28 @@ export interface PlanAnswers {
49
56
 
50
57
  export type AnswersOutcome = PlanAnswers | AnswersRefused
51
58
 
59
+ /**
60
+ * Whether `refs/remotes/origin/<branch>` exists, or `null` when the read
61
+ * itself failed. Injected so the gate is testable without a repository.
62
+ */
63
+ export type RemoteBranchReader = (
64
+ root: string,
65
+ branch: string,
66
+ ) => Promise<boolean | null>
67
+
68
+ /**
69
+ * Reads the remote-tracking ref as the last fetch left it, so a member pushed
70
+ * since then reads as absent and holds its integrator, which fails safe. The
71
+ * local head is not read, since it exists mid-build and a pushed branch is the
72
+ * only signal a member writes when it is done.
73
+ */
74
+ export const originBranchPushed: RemoteBranchReader = async (root, branch) => {
75
+ const report = await branchRefs(branch, root)
76
+ if (!report.readable) return null
77
+
78
+ return report.refs.includes(`refs/remotes/origin/${branch}`)
79
+ }
80
+
52
81
  /**
53
82
  * A reference that named a live plan, carrying the absolute path a reader opens
54
83
  * and the root-relative spelling a record reports.
@@ -217,6 +246,7 @@ export function resolvePlanReference(
217
246
  export async function planAnswers(
218
247
  root: string,
219
248
  reference: string,
249
+ readRemoteBranch: RemoteBranchReader = originBranchPushed,
220
250
  ): Promise<AnswersOutcome> {
221
251
  const resolved = resolvePlanReference(root, reference)
222
252
  if (!resolved.ok) return resolved
@@ -233,7 +263,8 @@ export async function planAnswers(
233
263
  }
234
264
 
235
265
  const stem = basename(resolved.path, '.md')
236
- const pairs = stackPairs(await readLivePlans(root))
266
+ const live = await readLivePlans(root)
267
+ const pairs = stackPairs(live)
237
268
 
238
269
  for (const { child, parent, shared } of pairs) {
239
270
  if (child !== stem) continue
@@ -244,6 +275,8 @@ export async function planAnswers(
244
275
  })
245
276
  }
246
277
 
278
+ open.push(...(await fanInEntries(root, stem, text, live, readRemoteBranch)))
279
+
247
280
  return {
248
281
  ok: true,
249
282
  plan: resolved.plan,
@@ -252,6 +285,50 @@ export async function planAnswers(
252
285
  }
253
286
  }
254
287
 
288
+ /**
289
+ * A plan whose fan-in set disagrees holds on `Fan-in set`, on either side,
290
+ * since an integrator listing an archived member would otherwise read that
291
+ * member's leftover branch as done. An integrator whose set agrees holds on
292
+ * `Fan-in pending` until every member branch is on origin.
293
+ */
294
+ async function fanInEntries(
295
+ root: string,
296
+ stem: string,
297
+ text: string,
298
+ live: readonly StackPlan[],
299
+ readRemoteBranch: RemoteBranchReader,
300
+ ): Promise<OpenQuestion[]> {
301
+ const entries: OpenQuestion[] = fanInFindings(live)
302
+ .filter(({ record }) => record === `${stem}.md`)
303
+ .map(({ message }) => ({ label: 'Fan-in set', why: message }))
304
+
305
+ const branches = readFanInMembers(text).map(fanInBranch)
306
+ if (branches.length === 0 || entries.length > 0) return entries
307
+
308
+ const pushed = await Promise.all(
309
+ branches.map((branch) => readRemoteBranch(root, branch)),
310
+ )
311
+
312
+ if (pushed.includes(null)) {
313
+ return [
314
+ {
315
+ label: 'Fan-in pending',
316
+ why: 'git could not read the member branches on origin, so the set is unread rather than done.',
317
+ },
318
+ ]
319
+ }
320
+
321
+ const absent = branches.filter((_, index) => pushed[index] !== true)
322
+ if (absent.length === 0) return []
323
+
324
+ return [
325
+ {
326
+ label: 'Fan-in pending',
327
+ why: `waits on ${absent.map((branch) => `\`${branch}\``).join(', ')} reaching origin. Fetch, then read again.`,
328
+ },
329
+ ]
330
+ }
331
+
255
332
  function refuse(
256
333
  reason: AnswerRefusal,
257
334
  message: string,
@@ -0,0 +1,176 @@
1
+ import type { Finding } from '@/records/validate'
2
+ import { PLAN_BRANCH_TYPE } from '@/tasks/branch'
3
+ import { constraintBullets, type StackPlan } from '@/tasks/stack'
4
+
5
+ const PLAN_PREFIX = 'feature-'
6
+
7
+ const MEMBER_OF = /\bfan[- ]in member of\s+/gi
8
+
9
+ const FANS_IN = /\bfans in\s+/gi
10
+
11
+ /**
12
+ * A backticked slug may drop the `feature-` prefix, and a bare one must carry
13
+ * it. Without the prefix a bare word is indistinguishable from the prose that
14
+ * follows the list, so `Fans in feature-a and the rest` would read `the` as a
15
+ * member.
16
+ */
17
+ const TICKED_SLUG = /^`((?:feature-)?[a-z0-9][a-z0-9-]*)`/i
18
+ const BARE_SLUG = /^(feature-[a-z0-9][a-z0-9-]*)/i
19
+
20
+ const LIST_SEPARATOR = /^\s*(?:,\s*(?:and\s+)?|and\s+)/i
21
+
22
+ function stemOf(slug: string): string {
23
+ const lower = slug.toLowerCase()
24
+
25
+ return lower.startsWith(PLAN_PREFIX) ? lower : `${PLAN_PREFIX}${lower}`
26
+ }
27
+
28
+ function leadingSlug(text: string): { stem: string; rest: string } | null {
29
+ const match = TICKED_SLUG.exec(text) ?? BARE_SLUG.exec(text)
30
+ const slug = match?.[1]
31
+ if (match === null || slug === undefined) return null
32
+
33
+ return { stem: stemOf(slug), rest: text.slice(match[0].length) }
34
+ }
35
+
36
+ /** Every slug in the list standing after each match of `phrase`, in order. */
37
+ function readList(text: string, phrase: RegExp, many: boolean): string[] {
38
+ const stems: string[] = []
39
+
40
+ for (const bullet of constraintBullets(text)) {
41
+ for (const match of bullet.matchAll(phrase)) {
42
+ let rest = bullet.slice((match.index ?? 0) + match[0].length)
43
+
44
+ for (;;) {
45
+ const slug = leadingSlug(rest)
46
+ if (slug === null) break
47
+ stems.push(slug.stem)
48
+ if (!many) break
49
+
50
+ const separator = LIST_SEPARATOR.exec(slug.rest)
51
+ if (separator === null) break
52
+ rest = slug.rest.slice(separator[0].length)
53
+ }
54
+ }
55
+ }
56
+
57
+ return [...new Set(stems)]
58
+ }
59
+
60
+ /** The integrators a member plan names, which a conforming member holds one of. */
61
+ export function readFanInIntegrators(text: string): string[] {
62
+ return readList(text, MEMBER_OF, false)
63
+ }
64
+
65
+ /** The members an integrator plan fans in, in the order its merges run. */
66
+ export function readFanInMembers(text: string): string[] {
67
+ return readList(text, FANS_IN, true)
68
+ }
69
+
70
+ /**
71
+ * The branch a member builds on, derived the way `planBranch` derives it, since
72
+ * the integrator merges and the dispatch gate reads the branch the worker took.
73
+ */
74
+ export function fanInBranch(stem: string): string {
75
+ const slug = stem.startsWith(PLAN_PREFIX)
76
+ ? stem.slice(PLAN_PREFIX.length)
77
+ : stem
78
+
79
+ return `${PLAN_BRANCH_TYPE}/${slug}`
80
+ }
81
+
82
+ function unmatched(stem: string, subject: string, message: string): Finding {
83
+ return { kind: 'fan-in-unmatched', record: `${stem}.md`, subject, message }
84
+ }
85
+
86
+ /**
87
+ * Every place a fan-in set's two sides disagree across the live plans.
88
+ *
89
+ * An absent plan is reported rather than skipped, unlike `stackFindings`. The
90
+ * caller hands over the live folder only, so a member archived after it merged
91
+ * reads as absent, and an integrator still listing it would otherwise take its
92
+ * leftover branch on origin as a member that finished.
93
+ *
94
+ * A member two integrators list is reported on each of them rather than paired
95
+ * with whichever sorts first, since either merge would carry it.
96
+ */
97
+ export function fanInFindings(plans: readonly StackPlan[]): Finding[] {
98
+ const integratorsOf = new Map(
99
+ plans.map((plan) => [plan.stem, readFanInIntegrators(plan.text)]),
100
+ )
101
+ const membersOf = new Map(
102
+ plans.map((plan) => [plan.stem, readFanInMembers(plan.text)]),
103
+ )
104
+ const listers = new Map<string, string[]>()
105
+ for (const [integrator, members] of membersOf) {
106
+ for (const member of members) {
107
+ listers.set(member, [...(listers.get(member) ?? []), integrator])
108
+ }
109
+ }
110
+
111
+ const findings: Finding[] = []
112
+
113
+ for (const { stem } of plans) {
114
+ const integrators = integratorsOf.get(stem) ?? []
115
+
116
+ for (const integrator of integrators) {
117
+ if (integrators.length > 1) {
118
+ findings.push(
119
+ unmatched(
120
+ stem,
121
+ integrator,
122
+ `is a fan-in member of ${integrators.map((name) => `\`${name}\``).join(' and ')}. A member fans in to one integrator.`,
123
+ ),
124
+ )
125
+ } else if (!membersOf.has(integrator)) {
126
+ findings.push(
127
+ unmatched(
128
+ stem,
129
+ integrator,
130
+ `is a fan-in member of \`${integrator}\`, which is not a live plan. Write the integrator's plan, or drop the member bullet.`,
131
+ ),
132
+ )
133
+ } else if (!(membersOf.get(integrator) ?? []).includes(stem)) {
134
+ findings.push(
135
+ unmatched(
136
+ stem,
137
+ integrator,
138
+ `is a fan-in member of \`${integrator}\`, whose \`Fans in\` bullet does not list it.`,
139
+ ),
140
+ )
141
+ }
142
+ }
143
+
144
+ for (const member of membersOf.get(stem) ?? []) {
145
+ const others = (listers.get(member) ?? []).filter((name) => name !== stem)
146
+
147
+ if (others.length > 0) {
148
+ findings.push(
149
+ unmatched(
150
+ stem,
151
+ member,
152
+ `fans in \`${member}\`, which ${others.map((name) => `\`${name}\``).join(' and ')} also fans in. A member fans in to one integrator.`,
153
+ ),
154
+ )
155
+ } else if (!integratorsOf.has(member)) {
156
+ findings.push(
157
+ unmatched(
158
+ stem,
159
+ member,
160
+ `fans in \`${member}\`, which is not a live plan. An archived member is not a finished one, so drop it from the list or restore its plan.`,
161
+ ),
162
+ )
163
+ } else if (!(integratorsOf.get(member) ?? []).includes(stem)) {
164
+ findings.push(
165
+ unmatched(
166
+ stem,
167
+ member,
168
+ `fans in \`${member}\`, which does not name it as its integrator.`,
169
+ ),
170
+ )
171
+ }
172
+ }
173
+ }
174
+
175
+ return findings
176
+ }
@@ -41,7 +41,7 @@ export interface StackPair {
41
41
  * One string per `**Constraints:**` bullet, with a wrapped continuation line
42
42
  * joined to the bullet it belongs to.
43
43
  */
44
- function constraintBullets(text: string): string[] {
44
+ export function constraintBullets(text: string): string[] {
45
45
  const bullets: string[] = []
46
46
 
47
47
  for (const line of splitPlanSections(text).get('Constraints') ?? []) {
@@ -4,25 +4,49 @@
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`, an `eslint.config.js` that keeps ESLint for this stack alone, and a `.prettierrc` that formats `.astro` files alone.
7
+ The astro stack covers Astro + TypeScript projects, being content sites, marketing sites, blogs, and docs. Interactive islands use React only and output is static.
8
+
9
+ It ships golden configs for `astro.config.mjs`, `vitest.config.ts`, `playwright.config.ts` (preview-server backed), and a `tsconfig.json` extending `astro/tsconfigs/strict`. An `eslint.config.js` keeps ESLint for this stack alone, and a `.prettierrc` formats `.astro` files alone.
8
10
 
9
11
  ## Scaffold checklist
10
12
 
11
13
  1. Scaffold with `bunx create-astro@latest`. Choose `TypeScript: Strict`. Skip git init and install.
12
- 2. Add React integration: `bunx astro add react`. Do not use `astro add tailwind`. That command installs the v3 integration. Tailwind v4 arrives via the web manifest.
13
- 3. Install web tooling: `canon tooling sync web . --write`
14
- 4. Install astro adapter: `canon tooling sync astro . --write`
14
+ 2. Add React with `bunx astro add react`. Never use `astro add tailwind`, which installs the v3 integration. Tailwind v4 arrives via the web manifest.
15
+ 3. Install web tooling with `canon tooling sync web . --write`.
16
+ 4. Install the astro adapter with `canon tooling sync astro . --write`.
15
17
  5. Extend the `ci` and `development` context entries under `canon/context/` per the web reference's extend sections plus the astro rows below.
16
18
  6. Run `bun run lint:fix`, then `bun run format`, then `bun run check`.
17
19
 
18
20
  ## What ships as golden configs
19
21
 
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
- - `vitest.config.ts`: uses `getViteConfig` from `astro/config` (not `mergeConfig`). jsdom, globals, setup file, `passWithNoTests: true`, v8 coverage, `**/*.astro` in coverage excludes.
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/vitest` in types, `@/` paths.
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`: 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/surfaces/tooling.md` for the measurement.
22
+ ### Astro and test configs
23
+
24
+ The configs below wire Astro into the web layer's test setup.
25
+
26
+ - `astro.config.mjs` adds `@astrojs/react`, `@tailwindcss/vite` in `vite.plugins`, the `@/` alias via `vite.resolve.alias`, and `ASTRO_SITE` for the `site` field.
27
+ - It serves port `4321` plus `WORKTREE_PORT_OFFSET` at `server.port`, with `strictPort` under `vite.server` and `vite.preview`.
28
+ - Astro feeds `server.port` through as the preview port but merges the `vite` block into the config backing both servers, so the port sits at the top level and the bind guarantee under `vite`.
29
+ - `vitest.config.ts` uses `getViteConfig` from `astro/config`, not `mergeConfig`, with jsdom, globals, the setup file, `passWithNoTests: true`, v8 coverage, and `**/*.astro` in coverage excludes.
30
+ - `tsconfig.json` adds `skipLibCheck`, `vitest/globals` and `@testing-library/jest-dom/vitest` in types, and `@/` paths.
31
+
32
+ ### Playwright config
33
+
34
+ `playwright.config.ts` runs all browsers, with `webServer` running `bun run build && bun run preview` on port `4321` plus `WORKTREE_PORT_OFFSET` and `reuseExistingServer: false`.
35
+
36
+ - Astro's dev/prod gap is wide (MDX, island hydration, asset optimization), so E2E always tests the built `dist/`.
37
+ - `DIST_PREBUILT` in the environment drops the `build` half, running `bun run preview` alone against a `dist/` a prior CI job produced.
38
+
39
+ ### ESLint config
40
+
41
+ `eslint.config.js` is this stack's own flat config, since the web layer lints with oxlint, which parses no `.astro` file. It adds `eslint-plugin-astro` with `astro-eslint-parser`.
42
+
43
+ - React hooks apply to `.jsx` and `.tsx` only, since `.astro` is not React.
44
+ - `check-file/filename-naming-convention` reaches `.ts`, `.tsx`, and `.astro` under `KEBAB_CASE`, overriding Astro's PascalCase on purpose, since markup names a component by its import binding.
45
+ - `.js` and `.jsx` stay out of that rule, matching `web` and `nextjs`.
46
+ - `check-file/folder-naming-convention` reaches every `src/**` folder except `__tests__` and the `pages/` subtree, which has its own off-block for bracket-named dynamic routes and slug folders.
47
+ - `canon/context/surfaces/tooling.md` carries the measurement behind both naming rules.
48
+
49
+ The social card files also ship as golden configs, being `card.config.mjs`, `card-src/pages/og-card.astro`, and `scripts/check-card-exclusion.sh`. See `## Social card route` below.
26
50
 
27
51
  ## Typecheck
28
52
 
@@ -30,73 +54,90 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
30
54
 
31
55
  ## Vitest scope
32
56
 
33
- Unit-test React island components (`.tsx`) only. Do not test `.astro` files. Page-level behavior is verified by Playwright against rendered output.
57
+ Unit-test React island components (`.tsx`) only. Do not test `.astro` files, since Playwright verifies page-level behavior against rendered output.
34
58
 
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.
59
+ Vitest is pinned to `^5` in the web manifest, which supports the Vite 8 that Astro 7 bundles. The earlier `^3` pin dodged `^4` bundling rolldown-vite, which ignored the esbuild JSX config in `@vitejs/plugin-react`. Astro bundling Vite 8 itself ended that conflict.
36
60
 
37
61
  ## Formatting (extend)
38
62
 
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.
63
+ - Format `.astro` files with Prettier and every other file with oxfmt, which loads no Prettier plugin and cannot parse `.astro`.
64
+ - Load `prettier-plugin-astro` first in `.prettierrc`'s plugins and `prettier-plugin-tailwindcss` last, with a parser override for `.astro` files.
41
65
  - 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.
66
+ - Copy every other option from base's `.oxfmtrc.json` by hand. Scan takes the child config over the parent's whole file, so a change to base's options is lost here until copied.
43
67
  - Override `check:format` and `format` to run oxfmt, then Prettier over `**/*.astro`.
44
68
 
45
69
  ## ESLint
46
70
 
47
71
  - 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.
72
+ - Install `eslint@^9`, `@eslint/js`, `typescript-eslint`, `eslint-config-prettier`, `eslint-plugin-react-hooks`, `eslint-plugin-vitest`, and `globals`, since the web layer no longer ships them.
73
+ - Pin `eslint@^9`, because ESLint 10 dropped the `LegacyESLint` shim `typescript-eslint@8` still imports.
49
74
  - Expect the web layer's `.oxlintrc.json` in the target. Nothing runs it here.
50
75
 
51
76
  ## Development docs (extend)
52
77
 
53
- Append to the `## Scripts` table:
78
+ Append these rows to the `## Scripts` table.
54
79
 
55
- | `bun run dev` | Start the Astro dev server on port 4321, plus this worktree's port offset. |
56
- | `bun run build` | Run `astro check` then build the static output. |
57
- | `bun run preview` | Serve the built site locally. |
58
- | `bun run astro` | Expose the Astro CLI. |
59
- | `bun run typecheck` | Run `astro check`. |
80
+ | Command | Purpose |
81
+ | ------------------- | --------------------------------------------------------------- |
82
+ | `bun run dev` | Start the Astro dev server on 4321 plus this worktree's offset. |
83
+ | `bun run build` | Run `astro check` then build the static output. |
84
+ | `bun run preview` | Serve the built site locally. |
85
+ | `bun run astro` | Expose the Astro CLI. |
86
+ | `bun run typecheck` | Run `astro check`. |
60
87
 
61
88
  ## CI docs (extend)
62
89
 
63
- In `canon/context/development/ci.md`, the Typecheck row's assertion reads: `` `astro check` passes ``. The Build row's assertion reads: `` `astro build` succeeds ``. <!-- audit-ignore-citations: canon/context/development/ci.md -->
90
+ In `canon/context/development/ci.md`, the Typecheck row asserts `` `astro check` passes `` and the Build row asserts `` `astro build` succeeds ``. <!-- audit-ignore-citations: canon/context/development/ci.md -->
64
91
 
65
92
  ## Social card route
66
93
 
67
- The social card is a route the project keeps rather than a page a render writes and deletes, so re-rendering it after a rename is an edit to one string and a re-capture instead of another pass through `draft-identity`'s pick loop with an operator in it. `canon:draft-identity` writes the picked composition into the route and captures it through the server below.
94
+ The social card is a route the project keeps rather than a page a render writes and deletes. Re-rendering it after a rename is an edit to one string and a re-capture, not another pass through the `draft-identity` pick loop.
95
+
96
+ `canon:draft-identity` writes the picked composition into the route and captures it through the server below.
68
97
 
69
98
  ### What ships
70
99
 
71
- - `card.config.mjs`: a second Astro config whose `srcDir` points at `./card-src`, which the main config's page router never reads. That is what excludes the card from the published build structurally rather than by a filename convention, the same mechanism `scenarios.astro` reaches for with `import.meta.env.DEV`. Its base port is `4421` plus `WORKTREE_PORT_OFFSET`, outside the band `astro.config.mjs` serves from, so a card server and a dev server of one worktree never contend.
72
- - `card-src/pages/og-card.astro`: the route, shipped as a working placeholder so it serves before any pick has been taken. It imports the project's own global stylesheet, which is what lets the composition read real tokens and real fonts instead of typed copies. It reads `--color-background` and `--color-text`, the names `canon design css` emits, and sets no `font-family` at all so the card inherits the project's body font rather than reading a font token the design system does not emit.
73
- - `scripts/check-card-exclusion.sh`: fails when the route's `<meta name="og-card-marker">` tag reaches `dist/`, and fails when the route dropped that tag, since the marker is what the leak test reads for.
74
- - `bun run card:dev` serves the route. `bun run card:check` runs the exclusion check, which base's verify runner, `scripts/verify.ts`, also runs after its build stage whenever the script is installed.
100
+ The route is a second config, an exclusion check, and two scripts. The page itself is covered below.
101
+
102
+ - `card.config.mjs` is a second Astro config whose `srcDir` points at `./card-src`, which the main config's page router never reads. That excludes the card from the published build structurally.
103
+ - `card.config.mjs` uses base port `4421` plus `WORKTREE_PORT_OFFSET`, outside the band `astro.config.mjs` serves from, so a worktree's card and dev servers never contend.
104
+ - `scripts/check-card-exclusion.sh` fails when the route's `<meta name="og-card-marker">` tag reaches `dist/`, and when the route dropped the tag, since the leak test reads it.
105
+ - `bun run card:dev` serves the route and `bun run card:check` runs the exclusion check, which base's `scripts/verify.ts` also runs after its build stage when the script is installed.
75
106
 
76
- ### Why each piece is shaped that way
107
+ ### Stylesheet and tokens
77
108
 
78
- The stylesheet import points at `@/styles/global.css`. A project keeping its stylesheet elsewhere retargets that one line, and an unresolved path then refuses the card server by name, which is the loud direction rather than the convenient one. The quiet failure it replaces is a card captured against no stylesheet at all: it renders as an unstyled box and reports success.
109
+ `card-src/pages/og-card.astro` ships as a working placeholder, so it serves before any pick. It imports the project's global stylesheet to read real tokens and fonts, uses `--color-background` and `--color-text` as `canon design css` emits them, and sets no `font-family`, so the card inherits the body font.
79
110
 
80
- A custom property the stylesheet does not define is the same silent failure in a second costume. It takes its fallback, captures cleanly and reports success, so the card ships on typed values while reading as though it read real ones. That is why the route names only properties `canon design css` emits, and why a project retargeting the stylesheet import checks the names against what its own stylesheet defines.
111
+ - The import points at `@/styles/global.css`. A project keeping its stylesheet elsewhere retargets that one line.
112
+ - An unresolved path refuses the card server by name. That is the loud direction, replacing a card captured against no stylesheet that renders unstyled and reports success.
81
113
 
82
- The exclusion is a check rather than a convention because the route has three consumers and only one of them can be enforced. The card server reads it, the capture reads it, and the published build must not carry it.
114
+ - A custom property the stylesheet does not define fails the same silent way. It takes its fallback, captures cleanly, and ships typed values.
115
+ - So the route names only properties `canon design css` emits, and a project retargeting the import checks the names against its own stylesheet.
83
116
 
84
- `.card` is declared at 600x315, half of the 1200x630 target. `canon capture` opens every page at a fixed 2x device scale factor with no flag to change it, so a half-size element is what lands the captured PNG on the literal size. Declaring it at 1200x630 captures at 2400x1260.
117
+ ### Exclusion and card size
118
+
119
+ The exclusion is a check rather than a convention because the route has three consumers and only one can be enforced. The card server reads it, the capture reads it, and the published build must not carry it.
120
+
121
+ `.card` is declared at 600x315, half of the 1200x630 target. `canon capture` opens every page at a fixed 2x device scale factor with no flag to change it, so a half-size element lands the PNG on the literal size. Declaring 1200x630 captures at 2400x1260.
85
122
 
86
123
  ### Why v1 is Astro-only
87
124
 
88
- Astro is the one web stack whose config gives a route a structural build exclusion. Next's App Router has no build-time route exclusion at all, and it needs none on the path it will take, since `ImageResponse` is that framework's own production mechanism and a card route there is meant to be served. `vite-react` builds a single-page app with no page router, so a card is a second Rollup entry rather than a route.
125
+ Astro is the one web stack whose config gives a route a structural build exclusion.
126
+
127
+ - Next's App Router has no build-time route exclusion, and needs none on its path, since `ImageResponse` is that framework's production mechanism and a card route there is meant to be served.
128
+ - `vite-react` builds a single-page app with no page router, so a card is a second Rollup entry rather than a route.
89
129
 
90
130
  ## Gitignore (extend)
91
131
 
92
- `[gitignore]` groups this stack edits, restated here per the manifest-to-reference symmetry:
132
+ These `[gitignore]` groups mirror the manifest.
93
133
 
94
134
  - `"# Astro" = [".astro/"]`
95
135
  - `"# Social card" = ["card-dist/"]`
96
136
 
97
137
  ## Scenario switcher
98
138
 
99
- - `src/components/dev/scenarios.astro` ships as a golden config, always overwritten on sync, since it is toolkit-authored infrastructure rather than a file a project hand-edits.
100
- - Import it into a page under test to drive candidate treatments of one decision by hand, selected by a query parameter, with a switcher for moving between them.
101
- - Guarded by `import.meta.env.DEV`, so it renders nothing and ships nothing in a production build.
102
- - Astro-only. It uses `is:inline`, `define:vars`, and `set:html`, which only the `.astro` file format parses. `vite-react` gets no equivalent until a real decision drives one.
139
+ `src/components/dev/scenarios.astro` ships as a golden config, always overwritten on sync, since it is toolkit-authored infrastructure rather than a file a project hand-edits.
140
+
141
+ - Import it into a page under test to drive candidate treatments of one decision by hand, selected by a query parameter, with a switcher between them.
142
+ - It is guarded by `import.meta.env.DEV`, so it renders nothing and ships nothing in a production build.
143
+ - It is Astro-only, using `is:inline`, `define:vars`, and `set:html`, which only the `.astro` format parses. `vite-react` gets no equivalent until a real decision drives one.