@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.
- package/README.md +34 -17
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/references/adopted.md +34 -14
- package/claude/skills/auto-ship/REQUIREMENT.md +2 -0
- package/claude/skills/auto-ship/SKILL.md +28 -2
- package/claude/skills/auto-ship/references/draft-mark.md +32 -6
- package/claude/skills/auto-ship/references/failure-recovery.md +15 -13
- package/claude/skills/auto-ship/references/fan-in.md +59 -0
- package/claude/skills/auto-ship/references/tier-failures.md +31 -10
- package/claude/skills/auto-ship/references/ui-checklist.md +9 -4
- package/claude/skills/auto-ship/references/worktree-name.md +9 -3
- package/claude/skills/build-in-slices/references/adopted.md +35 -21
- package/claude/skills/canvas-board/references/canvas.md +17 -6
- package/claude/skills/canvas-board/references/design.md +26 -7
- package/claude/skills/code-craft/references/adopted.md +58 -20
- package/claude/skills/codebase-layout/references/adopted.md +49 -16
- package/claude/skills/codebase-layout/references/frontend.md +11 -4
- package/claude/skills/codebase-layout/references/tests.md +4 -2
- package/claude/skills/context-fold/references/classify.md +10 -15
- package/claude/skills/context-fold/references/completion-report.md +11 -9
- package/claude/skills/context-fold/references/context-refresh.md +26 -9
- package/claude/skills/context-fold/references/unusable-baseline.md +5 -3
- package/claude/skills/create-rule/REQUIREMENT.md +5 -0
- package/claude/skills/create-rule/SKILL.md +9 -1
- package/claude/skills/create-rule/references/checklist.md +26 -0
- package/claude/skills/create-rule/references/rule.md +98 -20
- package/claude/skills/create-skill/SKILL.md +1 -0
- package/claude/skills/create-skill/references/skill-disclosure.md +10 -2
- package/claude/skills/create-skill/references/skill-frontmatter.md +43 -0
- package/claude/skills/create-skill/references/skill-paths.md +7 -4
- package/claude/skills/create-skill/references/skill-practice.md +6 -3
- package/claude/skills/create-skill/references/skill-requirement.md +4 -2
- package/claude/skills/create-skill/references/skill.md +67 -44
- package/claude/skills/decision-escalate/REQUIREMENT.md +1 -1
- package/claude/skills/decision-escalate/SKILL.md +1 -2
- package/claude/skills/deploy-app/references/vercel.md +2 -1
- package/claude/skills/deprecation-migration/references/adopted.md +44 -25
- package/claude/skills/design-taste/references/adopted.md +9 -1
- package/claude/skills/design-taste/references/craft.md +3 -1
- package/claude/skills/design-taste/references/kinds.md +8 -7
- package/claude/skills/design-taste/references/preflight.md +12 -0
- package/claude/skills/design-taste/references/systems.md +7 -6
- package/claude/skills/design-taste/references/tells.md +24 -25
- package/claude/skills/design-taste/references/vocabulary.md +7 -5
- package/claude/skills/document-health/references/axes.md +54 -30
- package/claude/skills/draft-and-pick/references/live-arms.md +2 -2
- package/claude/skills/draft-doc/references/context.md +4 -3
- package/claude/skills/draft-doc/references/docs.md +6 -3
- package/claude/skills/draft-doc/references/readme/agent-facing.md +6 -0
- package/claude/skills/draft-doc/references/readme/application.md +8 -2
- package/claude/skills/draft-doc/references/readme/cli.md +5 -1
- package/claude/skills/draft-doc/references/readme/library.md +4 -0
- package/claude/skills/draft-doc/references/readme/plugin.md +9 -1
- package/claude/skills/draft-doc/references/readme.md +30 -13
- package/claude/skills/draft-doc/references/requirements.md +17 -4
- package/claude/skills/draft-doc/references/wiki-kind.md +20 -10
- package/claude/skills/draft-figure/references/architecture-set.md +7 -3
- package/claude/skills/draft-figure/references/figures.md +8 -6
- package/claude/skills/draft-figure/references/hand-drawn.md +3 -1
- package/claude/skills/draft-figure/references/mermaid-path.md +19 -9
- package/claude/skills/draft-figure/references/mermaid.md +10 -5
- package/claude/skills/draft-ready/references/ready.md +22 -15
- package/claude/skills/git-branch/REQUIREMENT.md +17 -6
- package/claude/skills/git-branch/SKILL.md +63 -7
- package/claude/skills/git-branch/references/branch.md +11 -1
- package/claude/skills/git-branch/references/slug.md +17 -5
- package/claude/skills/git-commit/references/commit.md +8 -0
- package/claude/skills/git-commit/references/versioning.md +23 -17
- package/claude/skills/git-followup/REQUIREMENT.md +1 -1
- package/claude/skills/git-followup/SKILL.md +5 -6
- package/claude/skills/git-issue/references/issue.md +29 -16
- package/claude/skills/git-pr/REQUIREMENT.md +1 -5
- package/claude/skills/git-pr/SKILL.md +11 -22
- package/claude/skills/git-pr/references/final-command.md +21 -8
- package/claude/skills/git-pr/references/labels.md +45 -28
- package/claude/skills/git-pr/references/pr.md +28 -9
- package/claude/skills/git-pr/references/publish.md +42 -28
- package/claude/skills/git-pr/references/task-number.md +20 -8
- package/claude/skills/git-ship/SKILL.md +1 -1
- package/claude/skills/plan-feature/references/batches.md +48 -1
- package/claude/skills/plan-feature/references/plan-lifecycle.md +10 -3
- package/claude/skills/plan-feature/references/plan.md +59 -20
- package/claude/skills/plan-groundwork/references/groundwork-spikes.md +4 -1
- package/claude/skills/plan-groundwork/references/groundwork.md +14 -16
- package/claude/skills/plan-intake/references/intake.md +29 -24
- package/claude/skills/plan-intake-answer/REQUIREMENT.md +1 -1
- package/claude/skills/plan-intake-answer/SKILL.md +1 -2
- package/claude/skills/pr-evidence/REQUIREMENT.md +40 -0
- package/claude/skills/pr-evidence/SKILL.md +53 -0
- package/claude/skills/pr-evidence/references/evidence.md +150 -0
- package/claude/skills/review-address/references/post-review-findings.md +10 -3
- package/claude/skills/review-address/references/rebase-conflicts.md +22 -7
- package/claude/skills/review-address/references/rest-reads.md +3 -1
- package/claude/skills/review-craft/references/adopted.md +35 -18
- package/claude/skills/review-craft/references/security.md +42 -7
- package/claude/skills/review-craft/references/ui.md +23 -8
- package/claude/skills/review-pr/SKILL.md +6 -6
- package/claude/skills/review-pr/references/body-variants.md +29 -9
- package/claude/skills/review-pr/references/close-out.md +20 -5
- package/claude/skills/review-pr/references/key-changes.md +27 -3
- package/claude/skills/review-pr/references/post.md +83 -31
- package/claude/skills/review-pr/references/scope.md +54 -19
- package/claude/skills/review-ui/SKILL.md +2 -0
- package/claude/skills/review-ui/references/close-out.md +49 -0
- package/claude/skills/review-ui/references/driving.md +24 -4
- package/claude/skills/review-ui/references/post.md +43 -13
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +67 -17
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +81 -40
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +52 -15
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +48 -26
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +12 -13
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +72 -36
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +12 -4
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +64 -24
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +80 -26
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +41 -18
- package/claude/skills/role-orchestrator/references/orchestrator-resume.md +19 -7
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +22 -12
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +15 -6
- package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +25 -7
- package/claude/skills/role-orchestrator/references/orchestrator-watch.md +34 -8
- package/claude/skills/role-orchestrator/scripts/poll.ts +16 -24
- package/claude/skills/role-worker/REQUIREMENT.md +2 -0
- package/claude/skills/role-worker/SKILL.md +17 -0
- package/claude/skills/search-craft/references/adopted.md +18 -6
- package/claude/skills/session-map/references/session.md +26 -7
- package/claude/skills/session-worktree/references/main-root-writes.md +25 -8
- package/claude/skills/systematic-debugging/references/adopted.md +30 -16
- package/claude/skills/target-check/references/domains.md +39 -11
- package/claude/skills/target-setup/references/detect.md +80 -65
- package/claude/skills/target-setup/references/indexes.md +8 -6
- package/claude/skills/task-board/references/board.md +86 -30
- package/claude/skills/task-board/references/decline.md +29 -13
- package/claude/skills/task-board/references/tasks-decline.md +12 -5
- package/claude/skills/task-board/references/tasks.md +65 -33
- package/claude/skills/task-complete/references/archive.md +41 -17
- package/claude/skills/task-complete/references/receipt-sweep.md +23 -9
- package/claude/skills/task-complete/references/unusable-baseline.md +10 -3
- package/claude/skills/teach-workspace/REQUIREMENT.md +1 -1
- package/claude/skills/teach-workspace/SKILL.md +1 -2
- package/claude/skills/teach-workspace/references/glossary.md +6 -2
- package/claude/skills/teach-workspace/references/lesson-build.md +44 -16
- package/claude/skills/teach-workspace/references/lesson-craft.md +12 -7
- package/claude/skills/teach-workspace/references/pedagogy.md +6 -3
- package/claude/skills/teach-workspace/references/promotion-fold.md +17 -11
- package/claude/skills/teach-workspace/references/promotion.md +31 -8
- package/claude/skills/teach-workspace/references/revisit.md +8 -2
- package/claude/skills/teach-workspace/references/teach.md +17 -9
- package/claude/skills/test-craft/references/adopted.md +48 -14
- package/claude/skills/test-craft/references/component.md +7 -4
- package/claude/skills/test-craft/references/e2e.md +25 -7
- package/claude/skills/test-craft/references/unit.md +3 -1
- package/claude/skills/test-first/references/adopted.md +12 -4
- package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/claude/skills/video-craft/references/adopted.md +62 -18
- package/claude/skills/write-human/references/adopted.md +21 -8
- package/docs/workflow/parallel-features.md +2 -1
- package/docs/workflow/skill-map.md +2 -1
- package/package.json +1 -1
- package/src/claude/cases/git.ts +4 -0
- package/src/claude/cases/workflow.ts +5 -0
- package/src/commands/pr.ts +174 -37
- package/src/commands/records.ts +11 -3
- package/src/context/shape-gated.ts +1 -0
- package/src/markdown/scope.ts +150 -2
- package/src/pr/frames.ts +53 -9
- package/src/pr/lift.ts +6 -3
- package/src/pr/review-state.ts +182 -0
- package/src/records/validate.ts +1 -0
- package/src/tasks/answers.ts +79 -2
- package/src/tasks/fan-in.ts +176 -0
- package/src/tasks/stack.ts +1 -1
- package/tooling/astro/reference.md +80 -39
- package/tooling/base/reference.md +63 -32
- package/tooling/base/seeds/canon/context/ci.md +7 -15
- package/tooling/base/seeds/canon/context/development.md +14 -14
- package/tooling/claude/reference.md +40 -22
- package/tooling/cloudflare/reference.md +31 -12
- package/tooling/go/reference.md +47 -23
- package/tooling/nextjs/reference.md +52 -19
- package/tooling/php/reference.md +35 -16
- package/tooling/python/reference.md +47 -30
- package/tooling/shell/reference.md +6 -5
- package/tooling/vite-react/reference.md +49 -21
- package/tooling/web/reference.md +130 -70
- package/claude/skills/git-pr/references/evidence.md +0 -124
package/src/tasks/answers.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
+
}
|
package/src/tasks/stack.ts
CHANGED
|
@@ -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
|
|
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
|
|
13
|
-
3. Install web tooling
|
|
14
|
-
4. Install astro adapter
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- `
|
|
25
|
-
-
|
|
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
|
|
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
|
|
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
|
|
40
|
-
- Load `prettier-plugin-astro` first in `.prettierrc`'s plugins
|
|
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`
|
|
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
|
|
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
|
-
|
|
|
56
|
-
|
|
|
57
|
-
| `bun run
|
|
58
|
-
| `bun run astro`
|
|
59
|
-
| `bun run
|
|
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
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
- `
|
|
74
|
-
- `
|
|
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
|
-
###
|
|
107
|
+
### Stylesheet and tokens
|
|
77
108
|
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
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.
|