@erclx/canon 5.38.0 → 5.40.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 +5 -5
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +12 -9
- package/claude/skills/auto-ship/SKILL.md +43 -23
- package/claude/skills/backlog-triage/SKILL.md +62 -53
- package/claude/skills/backlog-triage/references/apply.md +69 -0
- package/claude/skills/build-in-slices/SKILL.md +8 -4
- package/claude/skills/canon-cli/SKILL.md +5 -1
- package/claude/skills/canon-feedback/SKILL.md +18 -8
- package/claude/skills/canon-feedback-triage/SKILL.md +10 -3
- package/claude/skills/canvas-board/SKILL.md +37 -18
- package/claude/skills/ci-workflow/SKILL.md +5 -4
- package/claude/skills/code-craft/SKILL.md +10 -6
- package/claude/skills/codebase-layout/SKILL.md +3 -5
- package/claude/skills/context-fold/REQUIREMENT.md +4 -4
- package/claude/skills/context-fold/SKILL.md +42 -23
- package/claude/skills/context-fold/references/context-refresh.md +4 -3
- package/claude/skills/context-fold/references/context.md +79 -64
- package/claude/skills/create-rule/SKILL.md +7 -3
- package/claude/skills/create-skill/SKILL.md +27 -7
- package/claude/skills/create-skill/references/skill-reference.md +41 -17
- package/claude/skills/decision-escalate/SKILL.md +8 -5
- package/claude/skills/deploy-app/SKILL.md +1 -1
- package/claude/skills/deprecation-migration/SKILL.md +12 -4
- package/claude/skills/design-taste/SKILL.md +17 -11
- package/claude/skills/docs-sync/REQUIREMENT.md +1 -0
- package/claude/skills/docs-sync/SKILL.md +17 -6
- package/claude/skills/docs-sync/references/docs.md +54 -21
- package/claude/skills/docs-sync/references/readme.md +32 -8
- package/claude/skills/document-health/SKILL.md +3 -4
- package/claude/skills/draft-and-pick/SKILL.md +73 -47
- package/claude/skills/draft-doc/SKILL.md +9 -4
- package/claude/skills/draft-doc/references/context.md +4 -4
- package/claude/skills/draft-doc/references/docs.md +1 -0
- package/claude/skills/draft-doc/references/wiki.md +3 -2
- package/claude/skills/draft-figure/SKILL.md +9 -7
- package/claude/skills/draft-identity/SKILL.md +24 -52
- package/claude/skills/draft-identity/references/card-route.md +46 -0
- package/claude/skills/draft-ready/SKILL.md +19 -7
- package/claude/skills/draft-screencast/SKILL.md +6 -112
- package/claude/skills/draft-screencast/references/beats-template.md +119 -0
- package/claude/skills/draft-slides/SKILL.md +9 -7
- package/claude/skills/draft-slides/references/deck.md +183 -0
- package/claude/skills/git-branch/SKILL.md +3 -3
- package/claude/skills/git-commit/SKILL.md +5 -3
- package/claude/skills/git-followup/SKILL.md +16 -9
- package/claude/skills/git-issue/SKILL.md +6 -5
- package/claude/skills/git-pr/SKILL.md +56 -44
- package/claude/skills/git-pr/references/evidence.md +2 -2
- package/claude/skills/git-pr/references/final-command.md +1 -1
- package/claude/skills/git-pr/references/labels.md +1 -1
- package/claude/skills/git-pr/references/publish.md +1 -1
- package/claude/skills/git-ship/SKILL.md +43 -34
- package/claude/skills/git-ship/references/ci-watch.md +31 -0
- package/claude/skills/git-split/SKILL.md +28 -100
- package/claude/skills/git-split/references/independent.md +40 -0
- package/claude/skills/git-split/references/stacked.md +73 -0
- package/claude/skills/git-stage/SKILL.md +9 -7
- package/claude/skills/git-worktree/SKILL.md +27 -49
- package/claude/skills/git-worktree/references/cleanup.md +55 -0
- package/claude/skills/markdown-craft/SKILL.md +2 -2
- package/claude/skills/markdown-craft/references/markdown.md +82 -53
- package/claude/skills/plan-feature/SKILL.md +52 -46
- package/claude/skills/plan-feature/references/batches.md +46 -0
- package/claude/skills/plan-groundwork/SKILL.md +60 -74
- package/claude/skills/plan-groundwork/references/output.md +48 -0
- package/claude/skills/plan-intake/SKILL.md +28 -21
- package/claude/skills/plan-intake-answer/SKILL.md +14 -13
- package/claude/skills/read-frames/SKILL.md +2 -0
- package/claude/skills/record-screencast/SKILL.md +31 -11
- package/claude/skills/review-address/SKILL.md +76 -158
- package/claude/skills/review-address/references/rebase-only.md +34 -0
- package/claude/skills/review-craft/SKILL.md +27 -11
- package/claude/skills/review-pr/SKILL.md +91 -40
- package/claude/skills/review-ui/SKILL.md +63 -37
- package/claude/skills/review-ui/references/hosted-preview.md +30 -0
- package/claude/skills/role-orchestrator/SKILL.md +105 -74
- package/claude/skills/role-orchestrator/references/orchestrator-filing.md +26 -0
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
- package/claude/skills/role-planner/SKILL.md +66 -64
- package/claude/skills/role-reviewer/SKILL.md +57 -46
- package/claude/skills/role-worker/SKILL.md +68 -63
- package/claude/skills/search-craft/SKILL.md +7 -5
- package/claude/skills/session-compact/SKILL.md +16 -11
- package/claude/skills/session-map/SKILL.md +31 -10
- package/claude/skills/session-relay/SKILL.md +32 -65
- package/claude/skills/session-resume/SKILL.md +18 -13
- package/claude/skills/session-worktree/SKILL.md +70 -42
- package/claude/skills/systematic-debugging/SKILL.md +6 -1
- package/claude/skills/target-check/SKILL.md +13 -6
- package/claude/skills/target-setup/SKILL.md +39 -27
- package/claude/skills/task-board/SKILL.md +68 -60
- package/claude/skills/task-board/references/board.md +1 -1
- package/claude/skills/task-board/references/create-report.md +36 -0
- package/claude/skills/task-board/references/tasks.md +1 -1
- package/claude/skills/task-complete/SKILL.md +20 -18
- package/claude/skills/teach-workspace/SKILL.md +61 -33
- package/claude/skills/test-craft/SKILL.md +2 -1
- package/claude/skills/test-first/SKILL.md +15 -9
- package/claude/skills/ui-checklist/SKILL.md +18 -16
- package/claude/skills/ux-audit/SKILL.md +9 -10
- package/claude/skills/ux-measure/SKILL.md +23 -19
- package/claude/skills/video-craft/SKILL.md +15 -7
- package/claude/skills/write-human/SKILL.md +9 -1
- package/claude/skills/youtube-transcripts/SKILL.md +2 -1
- package/docs/agents/audits.md +61 -60
- package/docs/agents/context-audit.md +40 -58
- package/docs/agents/index.md +11 -61
- package/docs/agents/indexes.md +25 -29
- package/docs/agents/install-and-sync.md +125 -235
- package/docs/agents/markdown-audit.md +54 -96
- package/docs/agents/migrate.md +41 -16
- package/docs/agents/output-shape.md +60 -45
- package/docs/agents/overview.md +36 -12
- package/docs/agents/records.md +37 -79
- package/docs/agents/sessions-transfer.md +26 -70
- package/docs/agents/targets.md +25 -77
- package/docs/target/index.md +0 -1
- package/docs/target/migrations.md +57 -66
- package/docs/target/projects.md +36 -78
- package/docs/target/scenarios.md +9 -19
- package/docs/target/sync.md +27 -68
- package/docs/workflow/ai-workflow.md +22 -68
- package/docs/workflow/operating-model.md +32 -186
- package/docs/workflow/parallel-features.md +29 -66
- package/docs/workflow/skill-map-on-request.md +22 -24
- package/docs/workflow/skill-map.md +72 -76
- package/docs/workflow/visual-design-workflow.md +25 -118
- package/docs/workflow/zshrc-aliases.md +18 -37
- package/package.json +1 -1
- package/src/canvas/client/inspector/fields.css +4 -0
- package/src/canvas/index.html +6 -0
- package/src/claude/skills-reach.ts +2 -2
- package/src/cli/ui.ts +3 -1
- package/src/commands/canvas.ts +23 -0
- package/src/commands/capture.ts +14 -0
- package/src/commands/claude.ts +14 -1
- package/src/commands/context.ts +106 -7
- package/src/commands/demo.ts +22 -0
- package/src/commands/deps.ts +6 -0
- package/src/commands/driver.ts +28 -2
- package/src/commands/gov.ts +52 -18
- package/src/commands/hooks.ts +63 -0
- package/src/commands/intake.ts +17 -0
- package/src/commands/labels.ts +38 -5
- package/src/commands/markdown.ts +169 -32
- package/src/commands/pr.ts +160 -43
- package/src/commands/records.ts +8 -0
- package/src/commands/secrets.ts +6 -0
- package/src/commands/sessions.ts +8 -0
- package/src/commands/tasks.ts +52 -0
- package/src/commands/teach.ts +13 -0
- package/src/commands/upstream.ts +25 -1
- package/src/commands/worktrees.ts +5 -0
- package/src/context/audit.ts +274 -5
- package/src/context/classify/prompts.ts +1 -1
- package/src/context/folders.ts +1 -1
- package/src/context/gate.ts +23 -7
- package/src/context/shape-gated.ts +30 -0
- package/src/counts/scan.ts +1 -1
- package/src/design/components.ts +1 -1
- package/src/design/document.ts +1 -1
- package/src/design/tokens.ts +1 -1
- package/src/gate/boundaries.ts +4 -1
- package/src/gate/measures.ts +16 -191
- package/src/gate/stages.ts +14 -30
- package/src/git/env.ts +6 -0
- package/src/hooks/reply-links.ts +110 -0
- package/src/markdown/density.ts +258 -0
- package/src/markdown/gate.ts +57 -18
- package/src/markdown/reference-shape.ts +103 -0
- package/src/markdown/scope.ts +162 -0
- package/src/markdown/structure.ts +9 -59
- package/src/pr/lift.ts +153 -19
- package/src/pr/painted.ts +87 -0
- package/src/roots/record.ts +18 -1
- package/src/sandbox/census.ts +1 -1
- package/src/sandbox/headless/run.ts +3 -2
- package/src/sandbox/headless/watch.ts +3 -4
- package/src/shipped/references.ts +1 -1
- package/src/slides/convert/master.ts +3 -0
- package/src/sync/format.ts +46 -0
- package/src/sync/stamp.ts +6 -0
- package/tooling/astro/reference.md +2 -2
- package/tooling/base/reference.md +1 -1
- package/tooling/claude/seeds/.claude/hooks/reply-links.sh +72 -0
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +22 -1
- package/tooling/claude/seeds/.claude/settings.json +4 -0
- package/tooling/go/reference.md +1 -1
- package/tooling/nextjs/reference.md +1 -1
- package/tooling/php/reference.md +1 -1
- package/tooling/python/reference.md +1 -1
- package/tooling/vite-react/reference.md +1 -1
- package/tooling/web/reference.md +1 -1
- package/docs/agents/canvas.md +0 -86
- package/docs/agents/capture.md +0 -65
- package/docs/agents/census.md +0 -23
- package/docs/agents/commands.md +0 -154
- package/docs/agents/comments.md +0 -34
- package/docs/agents/context-audit-checks.md +0 -90
- package/docs/agents/context-classify.md +0 -101
- package/docs/agents/counts.md +0 -74
- package/docs/agents/demo.md +0 -140
- package/docs/agents/docs.md +0 -27
- package/docs/agents/drift-surfaces.md +0 -201
- package/docs/agents/driver.md +0 -103
- package/docs/agents/gate.md +0 -90
- package/docs/agents/gov-detect.md +0 -47
- package/docs/agents/hooks.md +0 -58
- package/docs/agents/intake.md +0 -81
- package/docs/agents/key-changes.md +0 -116
- package/docs/agents/label-coverage.md +0 -86
- package/docs/agents/markdown-audit-weight.md +0 -81
- package/docs/agents/pr-evidence-layout.md +0 -70
- package/docs/agents/pr-evidence.md +0 -273
- package/docs/agents/pr-frames.md +0 -131
- package/docs/agents/pr-lift.md +0 -132
- package/docs/agents/pr-local.md +0 -111
- package/docs/agents/pr-preview.md +0 -155
- package/docs/agents/pr-reads.md +0 -111
- package/docs/agents/pr-tick.md +0 -72
- package/docs/agents/records-push.md +0 -61
- package/docs/agents/records-stale.md +0 -40
- package/docs/agents/records-validate.md +0 -57
- package/docs/agents/restated.md +0 -83
- package/docs/agents/routing.md +0 -61
- package/docs/agents/rule-citations.md +0 -90
- package/docs/agents/sandbox.md +0 -126
- package/docs/agents/scripting.md +0 -142
- package/docs/agents/sessions.md +0 -128
- package/docs/agents/skills-audit.md +0 -120
- package/docs/agents/skills-reach.md +0 -64
- package/docs/agents/slides.md +0 -176
- package/docs/agents/state-scoped-risk.md +0 -105
- package/docs/agents/superseded.md +0 -85
- package/docs/agents/tasks-archive.md +0 -83
- package/docs/agents/tasks-plans.md +0 -152
- package/docs/agents/tasks-validate.md +0 -133
- package/docs/agents/tasks.md +0 -123
- package/docs/agents/teach-authoring.md +0 -118
- package/docs/agents/teach.md +0 -157
- package/docs/agents/test-order.md +0 -58
- package/docs/agents/upstream.md +0 -94
- package/docs/agents/worktrees.md +0 -118
- package/docs/target/standards-moved.md +0 -34
- package/src/markdown/growth.ts +0 -43
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type BodyLine, visibleText } from '@/markdown/scan'
|
|
2
2
|
|
|
3
|
-
const HEADING = /^#{1,6}\s/
|
|
3
|
+
export const HEADING = /^#{1,6}\s/
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* A section marker holding its line alone, which is the signpost a document
|
|
@@ -65,10 +65,10 @@ const BOLD_LINE = /^\*\*([^*]+)\*\*\s*$/
|
|
|
65
65
|
const SPAN_LABEL = /^`[^`]+`$/
|
|
66
66
|
const MARKER_WIDTH = 20
|
|
67
67
|
|
|
68
|
-
const LIST_ITEM = /^(\s*)([-*+]|\d+\.)\s+/
|
|
69
|
-
const TABLE_ROW = /^\s*\|/
|
|
68
|
+
export const LIST_ITEM = /^(\s*)([-*+]|\d+\.)\s+/
|
|
69
|
+
export const TABLE_ROW = /^\s*\|/
|
|
70
70
|
const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/
|
|
71
|
-
const BLOCKQUOTE = /^\s*>/
|
|
71
|
+
export const BLOCKQUOTE = /^\s*>/
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* A sentence ends on terminal punctuation followed by the start of another
|
|
@@ -109,8 +109,8 @@ const SENTENCE_END = /[.!?]["'’”)\]]*(?=\s+(?:["'“([]*[A-Z]|`)|\s*$)/g
|
|
|
109
109
|
export const CHECKPOINTS = {
|
|
110
110
|
run: 40,
|
|
111
111
|
peerBullet: 130,
|
|
112
|
-
bullet:
|
|
113
|
-
paragraph:
|
|
112
|
+
bullet: 200,
|
|
113
|
+
paragraph: 350,
|
|
114
114
|
sentences: 4,
|
|
115
115
|
renderWidth: 80,
|
|
116
116
|
cadence: 3,
|
|
@@ -336,7 +336,7 @@ function isTableRun(run: readonly BodyLine[]): boolean {
|
|
|
336
336
|
* what lets a linked path reach the span test at all, since the markup around
|
|
337
337
|
* it would fail the pattern the destination is still attached.
|
|
338
338
|
*/
|
|
339
|
-
function isSectionMarker(text: string): boolean {
|
|
339
|
+
export function isSectionMarker(text: string): boolean {
|
|
340
340
|
const marker = text.match(BOLD_LINE)
|
|
341
341
|
if (!marker) return false
|
|
342
342
|
|
|
@@ -438,11 +438,7 @@ interface FoldedBullet extends BulletFinding {
|
|
|
438
438
|
readonly text: string
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
/**
|
|
442
|
-
* The walk `heavyBullets` reports from, keeping each past-checkpoint bullet's
|
|
443
|
-
* text so `longBlocks` can ask whether a base copy of it exists. One walk
|
|
444
|
-
* rather than two, for the reason `paragraphBlocks` is shared.
|
|
445
|
-
*/
|
|
441
|
+
/** The walk `heavyBullets` reports from. */
|
|
446
442
|
function foldBullets(
|
|
447
443
|
lines: readonly BodyLine[],
|
|
448
444
|
checkpoints: Checkpoints,
|
|
@@ -579,10 +575,7 @@ function paragraphText(block: readonly BodyLine[]): string {
|
|
|
579
575
|
* paragraph rule whenever the bullet rule was changed, and an author reading
|
|
580
576
|
* the sentence cap would find no weight rule to read at all.
|
|
581
577
|
*
|
|
582
|
-
*
|
|
583
|
-
* one population, sharing a median near 170 characters with no gap behind
|
|
584
|
-
* either candidate. They are separate checkpoints regardless, so either moves
|
|
585
|
-
* without dragging the other.
|
|
578
|
+
* They are separate checkpoints, so either moves without dragging the other.
|
|
586
579
|
*/
|
|
587
580
|
export function heavyParagraphs(
|
|
588
581
|
lines: readonly BodyLine[],
|
|
@@ -606,49 +599,6 @@ export function heavyParagraphs(
|
|
|
606
599
|
return findings
|
|
607
600
|
}
|
|
608
601
|
|
|
609
|
-
export interface LongBlock {
|
|
610
|
-
readonly kind: 'paragraph' | 'bullet'
|
|
611
|
-
readonly line: number
|
|
612
|
-
readonly characters: number
|
|
613
|
-
/** The block's source lines folded into one, as the paragraph and bullet folds read it. */
|
|
614
|
-
readonly text: string
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/**
|
|
618
|
-
* The paragraphs past the paragraph checkpoint and the top-level bullets past
|
|
619
|
-
* the bullet checkpoint, each with its text.
|
|
620
|
-
*
|
|
621
|
-
* Weight alone, never the sentence cap, since the growth stage asks about the
|
|
622
|
-
* two numbers the standard states in characters. The definitions are the
|
|
623
|
-
* audit's own, so a block this names and a block the weight report names are
|
|
624
|
-
* one set.
|
|
625
|
-
*/
|
|
626
|
-
export function longBlocks(
|
|
627
|
-
lines: readonly BodyLine[],
|
|
628
|
-
checkpoints: Checkpoints,
|
|
629
|
-
): LongBlock[] {
|
|
630
|
-
const blocks: LongBlock[] = []
|
|
631
|
-
|
|
632
|
-
for (const block of paragraphBlocks(lines)) {
|
|
633
|
-
const text = paragraphText(block)
|
|
634
|
-
const characters = visibleText(text).length
|
|
635
|
-
if (characters > checkpoints.paragraph) {
|
|
636
|
-
blocks.push({
|
|
637
|
-
kind: 'paragraph',
|
|
638
|
-
line: block[0].number,
|
|
639
|
-
characters,
|
|
640
|
-
text,
|
|
641
|
-
})
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
for (const bullet of foldBullets(lines, checkpoints)) {
|
|
646
|
-
blocks.push({ kind: 'bullet', ...bullet })
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
return blocks.sort((a, b) => a.line - b.line)
|
|
650
|
-
}
|
|
651
|
-
|
|
652
602
|
/** Punctuation either side of a word, so an opener is compared on its letters. */
|
|
653
603
|
const WORD_EDGE = /^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu
|
|
654
604
|
|
package/src/pr/lift.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
resolveReviewScope,
|
|
6
6
|
resolveUiScope,
|
|
7
7
|
} from '@/pr/review-scope'
|
|
8
|
+
import { DEFAULT_PAINTED, isPainted, type PaintedPatterns } from '@/pr/painted'
|
|
8
9
|
|
|
9
10
|
/** How long `--wait` holds for a pending CI run, in seconds, when none is named. */
|
|
10
11
|
export const DEFAULT_WAIT_SECONDS = 1200
|
|
@@ -42,6 +43,17 @@ export interface GateResult {
|
|
|
42
43
|
readonly isOk: boolean
|
|
43
44
|
/** Absent when the gate passed. */
|
|
44
45
|
readonly reason?: LiftRefusal
|
|
46
|
+
/** The commit a UI close was carried forward from, when it covered an earlier head. */
|
|
47
|
+
readonly carriedFrom?: string
|
|
48
|
+
/** The painted paths that kept a UI close from carrying forward. */
|
|
49
|
+
readonly paintedPaths?: readonly string[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** How an earlier commit sits against the head, and what changed since. */
|
|
53
|
+
export interface ChangedRead {
|
|
54
|
+
/** False when the commit is rewritten away or does not resolve. */
|
|
55
|
+
readonly isAncestor: boolean
|
|
56
|
+
readonly paths: readonly string[]
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
/** How the head sits against the base the pull request targets. */
|
|
@@ -85,6 +97,10 @@ export interface LiftApi {
|
|
|
85
97
|
readChecks(tip: string): Promise<ChecksGate | undefined>
|
|
86
98
|
/** The one GraphQL call the verb makes. */
|
|
87
99
|
markReady(nodeId: string): Promise<boolean>
|
|
100
|
+
/** The paths changed from an earlier commit to the head, undefined when the read failed. */
|
|
101
|
+
readChanged(from: string, to: string): Promise<ChangedRead | undefined>
|
|
102
|
+
/** The one GraphQL call that returns a ready pull request to draft. */
|
|
103
|
+
markDraft(nodeId: string): Promise<boolean>
|
|
88
104
|
sleep(ms: number): Promise<void>
|
|
89
105
|
now(): number
|
|
90
106
|
}
|
|
@@ -93,10 +109,20 @@ export interface LiftOptions {
|
|
|
93
109
|
readonly isCheck: boolean
|
|
94
110
|
readonly waitSeconds: number
|
|
95
111
|
readonly pollMs?: number
|
|
112
|
+
readonly painted?: PaintedPatterns
|
|
96
113
|
}
|
|
97
114
|
|
|
98
115
|
export interface LiftRecord {
|
|
99
|
-
readonly reason:
|
|
116
|
+
readonly reason:
|
|
117
|
+
| 'lifted'
|
|
118
|
+
| 'already-ready'
|
|
119
|
+
| 'redrafted'
|
|
120
|
+
| 'clear'
|
|
121
|
+
| LiftRefusal
|
|
122
|
+
/** The gate that read not clear when a ready pull request went back to draft. */
|
|
123
|
+
readonly failed?: LiftRefusal
|
|
124
|
+
readonly carriedFrom?: string
|
|
125
|
+
readonly paintedPaths?: readonly string[]
|
|
100
126
|
readonly isLifted: boolean
|
|
101
127
|
/** Whether the caller can treat the pull request as ready afterwards. */
|
|
102
128
|
readonly isOk: boolean
|
|
@@ -129,22 +155,51 @@ function reviewGate(reviews: readonly ReviewRow[], head: string): GateResult {
|
|
|
129
155
|
return pass('review')
|
|
130
156
|
}
|
|
131
157
|
|
|
132
|
-
/**
|
|
133
|
-
function
|
|
158
|
+
/** What the UI gate says about a close carried across commits, for a record. */
|
|
159
|
+
function carried(gates: readonly GateResult[]): {
|
|
160
|
+
carriedFrom?: string
|
|
161
|
+
paintedPaths?: readonly string[]
|
|
162
|
+
} {
|
|
163
|
+
const ui = gates.find((gate) => gate.gate === 'ui')
|
|
164
|
+
return {
|
|
165
|
+
...(ui?.carriedFrom !== undefined && { carriedFrom: ui.carriedFrom }),
|
|
166
|
+
...(ui?.paintedPaths !== undefined && { paintedPaths: ui.paintedPaths }),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Binds only where the evidence record carries a checklist to drive.
|
|
172
|
+
*
|
|
173
|
+
* A close behind the head still holds when it is an ancestor of the head and
|
|
174
|
+
* no painted path changed since, since a push that paints nothing leaves the
|
|
175
|
+
* checklist the reviewer drove as true as it was. A close that is not an
|
|
176
|
+
* ancestor, which a force-push or a rebase leaves, never carries.
|
|
177
|
+
*/
|
|
178
|
+
async function uiGate(
|
|
179
|
+
api: LiftApi,
|
|
180
|
+
painted: PaintedPatterns,
|
|
134
181
|
reviews: readonly ReviewRow[],
|
|
135
182
|
head: string,
|
|
136
183
|
hasChecklist: boolean,
|
|
137
|
-
): GateResult {
|
|
184
|
+
): Promise<GateResult> {
|
|
138
185
|
if (!hasChecklist) return pass('ui')
|
|
139
186
|
|
|
140
187
|
const scope = resolveUiScope({ reviews })
|
|
141
188
|
if (scope.state === 'none') return fail('ui', 'ui-missing')
|
|
142
189
|
if (scope.state === 'open') return fail('ui', 'ui-open')
|
|
143
190
|
if (scope.source !== 'marker') return fail('ui', 'ui-unmarked')
|
|
144
|
-
if (scope.commit === undefined
|
|
145
|
-
|
|
191
|
+
if (scope.commit === undefined) return fail('ui', 'ui-behind')
|
|
192
|
+
if (coversHead(scope.commit, head)) return pass('ui')
|
|
193
|
+
|
|
194
|
+
const changed = await api.readChanged(scope.commit, head)
|
|
195
|
+
if (changed === undefined) return fail('ui', 'gh-failed')
|
|
196
|
+
if (!changed.isAncestor) return fail('ui', 'ui-behind')
|
|
197
|
+
|
|
198
|
+
const paintedPaths = changed.paths.filter((path) => isPainted(path, painted))
|
|
199
|
+
if (paintedPaths.length > 0) {
|
|
200
|
+
return { ...fail('ui', 'ui-behind'), paintedPaths }
|
|
146
201
|
}
|
|
147
|
-
return pass('ui')
|
|
202
|
+
return { ...pass('ui'), carriedFrom: scope.commit }
|
|
148
203
|
}
|
|
149
204
|
|
|
150
205
|
function standingGate(standing: Standing): GateResult {
|
|
@@ -207,6 +262,56 @@ function refused(
|
|
|
207
262
|
...(head !== undefined && { head }),
|
|
208
263
|
gates,
|
|
209
264
|
waitedSeconds,
|
|
265
|
+
...carried(gates),
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The gates whose failure sends a ready pull request back to draft. */
|
|
270
|
+
const REDRAFT_GATES: readonly GateName[] = ['review', 'ui', 'evidence']
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Reads a ready pull request against the gates a person's ready mark vouched
|
|
274
|
+
* for. A failing review, UI, or evidence gate returns it to draft, since the
|
|
275
|
+
* mark no longer holds. Standing and CI are reported and never written on, as
|
|
276
|
+
* either would flap a merge-ready pull request on a run still going.
|
|
277
|
+
*/
|
|
278
|
+
async function settleReady(
|
|
279
|
+
api: LiftApi,
|
|
280
|
+
opts: LiftOptions,
|
|
281
|
+
pull: PullState,
|
|
282
|
+
tip: string,
|
|
283
|
+
gates: readonly GateResult[],
|
|
284
|
+
): Promise<LiftRecord> {
|
|
285
|
+
const blocked = gates.find(
|
|
286
|
+
(gate) => REDRAFT_GATES.includes(gate.gate) && !gate.isOk,
|
|
287
|
+
)
|
|
288
|
+
if (blocked?.reason === undefined) {
|
|
289
|
+
return {
|
|
290
|
+
reason: 'already-ready',
|
|
291
|
+
isLifted: false,
|
|
292
|
+
isOk: true,
|
|
293
|
+
head: tip,
|
|
294
|
+
gates,
|
|
295
|
+
waitedSeconds: 0,
|
|
296
|
+
...carried(gates),
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (opts.isCheck || blocked.reason === 'gh-failed') {
|
|
300
|
+
return refused(blocked.reason, gates, tip)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const isDrafted = await api.markDraft(pull.nodeId)
|
|
304
|
+
if (!isDrafted) return refused('gh-failed', gates, tip)
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
reason: 'redrafted',
|
|
308
|
+
failed: blocked.reason,
|
|
309
|
+
isLifted: false,
|
|
310
|
+
isOk: false,
|
|
311
|
+
head: tip,
|
|
312
|
+
gates,
|
|
313
|
+
waitedSeconds: 0,
|
|
314
|
+
...carried(gates),
|
|
210
315
|
}
|
|
211
316
|
}
|
|
212
317
|
|
|
@@ -224,20 +329,11 @@ export async function lift(
|
|
|
224
329
|
opts: LiftOptions,
|
|
225
330
|
): Promise<LiftRecord> {
|
|
226
331
|
const pollMs = opts.pollMs ?? CHECKS_POLL_MS
|
|
332
|
+
const painted = opts.painted ?? DEFAULT_PAINTED
|
|
227
333
|
|
|
228
334
|
const pull = await api.readPull()
|
|
229
335
|
if (pull === undefined) return refused('gh-failed', [], undefined)
|
|
230
336
|
if (!pull.isOpen) return refused('pull-closed', [], pull.head)
|
|
231
|
-
if (!pull.isDraft) {
|
|
232
|
-
return {
|
|
233
|
-
reason: 'already-ready',
|
|
234
|
-
isLifted: false,
|
|
235
|
-
isOk: true,
|
|
236
|
-
head: pull.head,
|
|
237
|
-
gates: [],
|
|
238
|
-
waitedSeconds: 0,
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
337
|
|
|
242
338
|
// The tip, not the object's head, since the object lags the ref by up to a
|
|
243
339
|
// minute after a push and a review marker names the commit it read.
|
|
@@ -255,10 +351,12 @@ export async function lift(
|
|
|
255
351
|
|
|
256
352
|
const gates: GateResult[] = [
|
|
257
353
|
reviewGate(reviews, tip),
|
|
258
|
-
uiGate(reviews, tip, evidence.hasChecklist),
|
|
354
|
+
await uiGate(api, painted, reviews, tip, evidence.hasChecklist),
|
|
259
355
|
evidence.isSettled ? pass('evidence') : fail('evidence', 'evidence-owed'),
|
|
260
356
|
standingGate(standing),
|
|
261
357
|
]
|
|
358
|
+
if (!pull.isDraft) return settleReady(api, opts, pull, tip, gates)
|
|
359
|
+
|
|
262
360
|
const blocked = gates.find((gate) => !gate.isOk)
|
|
263
361
|
|
|
264
362
|
if (opts.isCheck) {
|
|
@@ -284,6 +382,7 @@ export async function lift(
|
|
|
284
382
|
head: tip,
|
|
285
383
|
gates: all,
|
|
286
384
|
waitedSeconds: 0,
|
|
385
|
+
...carried(all),
|
|
287
386
|
}
|
|
288
387
|
}
|
|
289
388
|
|
|
@@ -316,13 +415,14 @@ export async function lift(
|
|
|
316
415
|
head: nowTip,
|
|
317
416
|
gates: cleared,
|
|
318
417
|
waitedSeconds: ci.waited,
|
|
418
|
+
...carried(cleared),
|
|
319
419
|
}
|
|
320
420
|
}
|
|
321
421
|
if (nowTip !== tip) return refused('head-moved', cleared, nowTip, ci.waited)
|
|
322
422
|
|
|
323
423
|
const reread = [
|
|
324
424
|
reviewGate(nowReviews, nowTip),
|
|
325
|
-
uiGate(nowReviews, nowTip, evidence.hasChecklist),
|
|
425
|
+
await uiGate(api, painted, nowReviews, nowTip, evidence.hasChecklist),
|
|
326
426
|
].find((gate) => !gate.isOk)
|
|
327
427
|
if (reread?.reason !== undefined) {
|
|
328
428
|
return refused(reread.reason, cleared, nowTip, ci.waited)
|
|
@@ -345,6 +445,7 @@ export async function lift(
|
|
|
345
445
|
head: nowTip,
|
|
346
446
|
gates: cleared,
|
|
347
447
|
waitedSeconds: ci.waited,
|
|
448
|
+
...carried(cleared),
|
|
348
449
|
}
|
|
349
450
|
}
|
|
350
451
|
|
|
@@ -416,3 +517,36 @@ export async function readBaseStanding(
|
|
|
416
517
|
|
|
417
518
|
return nonEmptyLines(moved).length > 0 ? 'stale' : 'fresh'
|
|
418
519
|
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Reads what changed between a close commit and the head. A commit that does
|
|
523
|
+
* not resolve, or that the head does not descend from, reads as not an
|
|
524
|
+
* ancestor and carries nothing. A head that does not resolve, or a diff that
|
|
525
|
+
* fails, is a read that failed, so the gate refuses rather than passes.
|
|
526
|
+
*
|
|
527
|
+
* The abbreviated marker resolves first, so the ancestry test runs on the full
|
|
528
|
+
* sha. Renames are split into both sides so a move out of a painted path
|
|
529
|
+
* still reads as painted.
|
|
530
|
+
*/
|
|
531
|
+
export async function readChangedPaths(
|
|
532
|
+
git: GitRunner,
|
|
533
|
+
from: string,
|
|
534
|
+
to: string,
|
|
535
|
+
): Promise<ChangedRead | undefined> {
|
|
536
|
+
if ((await git(['cat-file', '-e', `${to}^{commit}`])) === undefined) {
|
|
537
|
+
return undefined
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const full = (
|
|
541
|
+
await git(['rev-parse', '--verify', '-q', `${from}^{commit}`])
|
|
542
|
+
)?.trim()
|
|
543
|
+
if (full === undefined || full === '') return { isAncestor: false, paths: [] }
|
|
544
|
+
|
|
545
|
+
const base = (await git(['merge-base', full, to]))?.trim()
|
|
546
|
+
if (base !== full) return { isAncestor: false, paths: [] }
|
|
547
|
+
|
|
548
|
+
const diff = await git(['diff', '--name-only', '--no-renames', full, to])
|
|
549
|
+
if (diff === undefined) return undefined
|
|
550
|
+
|
|
551
|
+
return { isAncestor: true, paths: nonEmptyLines(diff) }
|
|
552
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/** Where a project adds to or trims the default list of painted paths. */
|
|
5
|
+
export const PAINTED_CONFIG_REL = join('canon', 'config', 'painted.json')
|
|
6
|
+
|
|
7
|
+
export interface PaintedPatterns {
|
|
8
|
+
/** A path matching any of these is painted unless `unpainted` also matches. */
|
|
9
|
+
readonly painted: readonly string[]
|
|
10
|
+
readonly unpainted: readonly string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RENDERED_EXTENSIONS = [
|
|
14
|
+
'tsx',
|
|
15
|
+
'jsx',
|
|
16
|
+
'vue',
|
|
17
|
+
'svelte',
|
|
18
|
+
'astro',
|
|
19
|
+
'html',
|
|
20
|
+
'css',
|
|
21
|
+
'scss',
|
|
22
|
+
'svg',
|
|
23
|
+
'png',
|
|
24
|
+
'jpg',
|
|
25
|
+
'jpeg',
|
|
26
|
+
'gif',
|
|
27
|
+
'webp',
|
|
28
|
+
'avif',
|
|
29
|
+
'ico',
|
|
30
|
+
'woff',
|
|
31
|
+
'woff2',
|
|
32
|
+
'ttf',
|
|
33
|
+
'otf',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What a browser renders, minus the tests and stories beside it. A test-only
|
|
38
|
+
* push touches nothing a reviewer drove, which is the case the carry-forward
|
|
39
|
+
* exists for.
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_PAINTED: PaintedPatterns = {
|
|
42
|
+
painted: [`**/*.{${RENDERED_EXTENSIONS.join(',')}}`],
|
|
43
|
+
unpainted: [
|
|
44
|
+
'**/*.{test,spec,stories}.*',
|
|
45
|
+
'**/__tests__/**',
|
|
46
|
+
'**/__snapshots__/**',
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function strings(value: unknown): string[] {
|
|
51
|
+
return Array.isArray(value)
|
|
52
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
53
|
+
: []
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The default list plus the project's own, read from `canon/config/painted.json`
|
|
58
|
+
* as `{ "painted": [...], "unpainted": [...] }`. A file that is absent or does
|
|
59
|
+
* not parse leaves the default, since a typo there must not make every push
|
|
60
|
+
* read as unpainted.
|
|
61
|
+
*/
|
|
62
|
+
export function loadPaintedPatterns(root: string): PaintedPatterns {
|
|
63
|
+
const path = join(root, PAINTED_CONFIG_REL)
|
|
64
|
+
if (!existsSync(path)) return DEFAULT_PAINTED
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
|
68
|
+
if (typeof parsed !== 'object' || parsed === null) return DEFAULT_PAINTED
|
|
69
|
+
const { painted, unpainted } = parsed as Record<string, unknown>
|
|
70
|
+
return {
|
|
71
|
+
painted: [...DEFAULT_PAINTED.painted, ...strings(painted)],
|
|
72
|
+
unpainted: [...DEFAULT_PAINTED.unpainted, ...strings(unpainted)],
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
return DEFAULT_PAINTED
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function matchesAny(path: string, globs: readonly string[]): boolean {
|
|
80
|
+
return globs.some((glob) => new Bun.Glob(glob).match(path))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isPainted(path: string, patterns: PaintedPatterns): boolean {
|
|
84
|
+
return (
|
|
85
|
+
matchesAny(path, patterns.painted) && !matchesAny(path, patterns.unpainted)
|
|
86
|
+
)
|
|
87
|
+
}
|
package/src/roots/record.ts
CHANGED
|
@@ -177,6 +177,16 @@ export function creationRel(
|
|
|
177
177
|
return join(at, spell(at, folder), ...rest)
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/** Whether a root holds the backup history or a record folder, at its spelling. */
|
|
181
|
+
function holdsRecords(root: string, candidate: RecordRoot): boolean {
|
|
182
|
+
return RECORD_ENTRIES.some(
|
|
183
|
+
(entry) =>
|
|
184
|
+
entry !== 'README.md' &&
|
|
185
|
+
entry !== SCRATCH &&
|
|
186
|
+
existsSync(join(root, candidate, spell(candidate, entry))),
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
180
190
|
/**
|
|
181
191
|
* The record root itself, for a caller whose subject is the root rather than a
|
|
182
192
|
* folder inside it.
|
|
@@ -186,11 +196,18 @@ export function creationRel(
|
|
|
186
196
|
* one answer. Splitting them would let a push resolve a history at one root and
|
|
187
197
|
* stage a work tree at the other, which stages the deletion of every folder the
|
|
188
198
|
* move relocated.
|
|
199
|
+
*
|
|
200
|
+
* A root counts when it holds a `.records.git` or a record folder. The bare
|
|
201
|
+
* directory does not, because `.claude/` exists in every installed project for
|
|
202
|
+
* its hooks and rules, so testing it pinned a fresh clone to the retired root.
|
|
203
|
+
* `README.md` and the scratch folder are left out: a records pull writes the
|
|
204
|
+
* first back and the second is deletable, so a root holding only them has
|
|
205
|
+
* nothing to protect.
|
|
189
206
|
*/
|
|
190
207
|
export function recordRoot(root: string): string {
|
|
191
208
|
return join(
|
|
192
209
|
root,
|
|
193
|
-
RECORD_ROOTS.find((candidate) =>
|
|
210
|
+
RECORD_ROOTS.find((candidate) => holdsRecords(root, candidate)) ??
|
|
194
211
|
CREATION_ROOT,
|
|
195
212
|
)
|
|
196
213
|
}
|
package/src/sandbox/census.ts
CHANGED
|
@@ -69,7 +69,7 @@ export function listSkills(root: string): string[] {
|
|
|
69
69
|
/**
|
|
70
70
|
* Maps a scenario to the skill it drives, trying two spellings in order.
|
|
71
71
|
*
|
|
72
|
-
* `<category>-<command>` is the rule `canon/context/sandbox
|
|
72
|
+
* `<category>-<command>` is the rule `canon/context/development/sandbox.md` states, and it
|
|
73
73
|
* alone pairs 29 of 54 skills. The bare `<command>` fallback is what reaches the
|
|
74
74
|
* rest: `claude/target-setup.ts` drives the `target-setup` skill, not a `claude-target-setup` that
|
|
75
75
|
* does not exist. Stating one spelling and shipping two is what let the audit
|
|
@@ -417,8 +417,9 @@ async function drive(
|
|
|
417
417
|
|
|
418
418
|
// Taken after provisioning, so a scenario that stages a live session record
|
|
419
419
|
// of its own is already in the before snapshot rather than reported as a
|
|
420
|
-
// dispatch this run made.
|
|
421
|
-
//
|
|
420
|
+
// dispatch this run made. A scenario that fakes a live session
|
|
421
|
+
// writes to the real registry, since provisioning and the driven run share
|
|
422
|
+
// no environment to repoint it.
|
|
422
423
|
const registry = sessionsDir()
|
|
423
424
|
const sessionsBefore = snapshotSessions(registry)
|
|
424
425
|
|
|
@@ -65,8 +65,8 @@ export function writesBetween(before: Manifest, after: Manifest): string[] {
|
|
|
65
65
|
*
|
|
66
66
|
* It makes an escape visible rather than impossible. A session can write to a
|
|
67
67
|
* home directory, to a sibling worktree, or to any path outside these roots, and
|
|
68
|
-
* none of that is watched.
|
|
69
|
-
*
|
|
68
|
+
* none of that is watched. `canon/context/development/sandbox.md` names the four
|
|
69
|
+
* directories the watch reads.
|
|
70
70
|
*
|
|
71
71
|
* A linked worktree is the normal place to develop this repository, and the
|
|
72
72
|
* rule an escaping session follows sends scratch to the main root rather than to
|
|
@@ -108,8 +108,7 @@ export function escapeRoots(projectRoot: string): string[] {
|
|
|
108
108
|
* `dispatch.ts`, so the key defined against this file-write watch keeps one
|
|
109
109
|
* meaning. An arm whose skill is meant to reach past the sandbox tree declares
|
|
110
110
|
* `escape_scope` in its `expect.toml`, which bounds a legitimate write rather
|
|
111
|
-
* than the run. Read `canon/context/sandbox
|
|
112
|
-
* `canon/context/sandbox/coverage/workflow-arms.md` before writing a claim past
|
|
111
|
+
* than the run. Read `canon/context/development/sandbox.md` before writing a claim past
|
|
113
112
|
* that.
|
|
114
113
|
*/
|
|
115
114
|
export const ESCAPE_SCRATCH_DIRS = [
|
|
@@ -132,7 +132,7 @@ const SAME_REPOSITORY =
|
|
|
132
132
|
* `canon/context/` carries no equivalent pattern. The same resolution test
|
|
133
133
|
* cannot separate a path every scaffolded project holds, such as
|
|
134
134
|
* `canon/context/index.md`, from this repository's own domain entry, such
|
|
135
|
-
* as `canon/context/context-model
|
|
135
|
+
* as `canon/context/context-model.md`, since both resolve here and only the
|
|
136
136
|
* second is a defect. That is a semantic read no pattern makes, so it stays
|
|
137
137
|
* with the rule and the review checklist rather than joining this reader.
|
|
138
138
|
*/
|
|
@@ -157,6 +157,9 @@ export function slideNumberProps(theme: DeckTheme): PptxGenJS.SlideNumberProps {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// An SVG mark keeps the broken PNG fallback pptxgenjs writes, because the
|
|
161
|
+
// screenshot patch in export.ts reaches slides and never layouts, so the mark
|
|
162
|
+
// is a raster file.
|
|
160
163
|
function markImage(path: string, size: MarkSize): PptxGenJS.ImageProps {
|
|
161
164
|
const scale = Math.min(MARK_BOX.w / size.width, MARK_BOX.h / size.height)
|
|
162
165
|
const w = Math.round(size.width * scale * 1000) / 1000
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { extname, join, resolve } from 'node:path'
|
|
3
|
+
import { execa } from 'execa'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolved in order, so a project carrying both formats with the repo's own.
|
|
7
|
+
* Prettier prints to stdout unless told to write, where oxfmt rewrites in place.
|
|
8
|
+
*/
|
|
9
|
+
const FORMATTERS = [
|
|
10
|
+
{ name: 'oxfmt', flags: [] },
|
|
11
|
+
{ name: 'prettier', flags: ['--write'] },
|
|
12
|
+
] as const
|
|
13
|
+
|
|
14
|
+
/** What a formatter reads. A shell script or a binary seed is left to its own tool. */
|
|
15
|
+
const FORMATTABLE = new Set(['.json', '.jsonc', '.md', '.yml', '.yaml'])
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Runs the project's installed formatter over the files a sync wrote, so a
|
|
19
|
+
* write the formatter would reshape does not leave a whitespace diff on a file
|
|
20
|
+
* nobody edited. Only the named paths are passed, never the project, which
|
|
21
|
+
* keeps a dirty tree unformatted. A path is absolute or relative to `target`.
|
|
22
|
+
*
|
|
23
|
+
* Best effort: no installed formatter, or one that fails, leaves the output as
|
|
24
|
+
* written and the sync unaffected. Never pass a file the sync hashes against
|
|
25
|
+
* its source, since the bytes on disk would stop matching the record.
|
|
26
|
+
*/
|
|
27
|
+
export async function formatWritten(
|
|
28
|
+
target: string,
|
|
29
|
+
paths: readonly string[],
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const files = paths
|
|
32
|
+
.filter((path) => FORMATTABLE.has(extname(path)))
|
|
33
|
+
.map((path) => resolve(target, path))
|
|
34
|
+
if (files.length === 0) return
|
|
35
|
+
|
|
36
|
+
const formatter = FORMATTERS.map((candidate) => ({
|
|
37
|
+
...candidate,
|
|
38
|
+
path: join(target, 'node_modules', '.bin', candidate.name),
|
|
39
|
+
})).find((candidate) => existsSync(candidate.path))
|
|
40
|
+
if (formatter === undefined) return
|
|
41
|
+
|
|
42
|
+
await execa(formatter.path, [...formatter.flags, ...files], {
|
|
43
|
+
cwd: target,
|
|
44
|
+
reject: false,
|
|
45
|
+
})
|
|
46
|
+
}
|
package/src/sync/stamp.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { mkdir, writeFile } from 'node:fs/promises'
|
|
|
4
4
|
import { dirname, join, sep } from 'node:path'
|
|
5
5
|
import { execa } from 'execa'
|
|
6
6
|
import { resolveExisting } from '@/roots/legacy'
|
|
7
|
+
import { formatWritten } from '@/sync/format'
|
|
7
8
|
import { recordTarget } from '@/targets/registry'
|
|
8
9
|
import { compareVersions, parseVersion } from '@/version/compare'
|
|
9
10
|
import { readInstalled } from '@/version/installed'
|
|
@@ -287,6 +288,11 @@ async function putDomain(
|
|
|
287
288
|
await mkdir(dirname(path), { recursive: true })
|
|
288
289
|
await writeFile(path, `${JSON.stringify(stamp, null, 2)}\n`)
|
|
289
290
|
|
|
291
|
+
// The stamp hashes every file but itself, so reshaping it cannot read as
|
|
292
|
+
// drift. Seeds and goldens stay as copied for the opposite reason: their
|
|
293
|
+
// hashes are taken against the source bytes.
|
|
294
|
+
await formatWritten(target, [path])
|
|
295
|
+
|
|
290
296
|
// Every install and sync that stamps a target passes through here, which is
|
|
291
297
|
// what makes this the one place the machine-level index can be kept without
|
|
292
298
|
// each command remembering to. Its outcome is dropped rather than reported:
|
|
@@ -22,7 +22,7 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
|
|
|
22
22
|
- `playwright.config.ts`: all browsers, `webServer` runs `bun run build && bun run preview` on port `4321` plus `WORKTREE_PORT_OFFSET`, `reuseExistingServer: false`. Astro's dev/prod gap is wide (MDX, island hydration, asset optimization), so E2E always tests the built `dist/`. `DIST_PREBUILT` set in the environment drops the `build` half, running `bun run preview` alone against a `dist/` a prior CI job already produced.
|
|
23
23
|
- `tsconfig.json`: extends `astro/tsconfigs/strict`, adds `skipLibCheck`, `vitest/globals` and `@testing-library/jest-dom/vitest` in types, `@/` paths.
|
|
24
24
|
- `card.config.mjs`, `card-src/pages/og-card.astro`, and `scripts/check-card-exclusion.sh`: the social card route, its structural build exclusion, and the check enforcing it. See `## Social card route` below.
|
|
25
|
-
- `eslint.config.js`: this stack's own flat config, since the web layer lints with oxlint, which parses no `.astro` file. Adds `eslint-plugin-astro` (`.astro` parser via `astro-eslint-parser`). React-hooks scoped to `.jsx`/`.tsx` only (`.astro` is not React). The shared block's `files` selector includes `.astro`, so `check-file/filename-naming-convention` reaches `.ts`, `.tsx`, and `.astro` under `KEBAB_CASE`, overriding Astro's own PascalCase component convention deliberately, on the ground that a component's name in markup comes from the import binding rather than the filename. `.js` and `.jsx` stay out of the rule's own pattern, matching `web` and `nextjs`. `check-file/folder-naming-convention` reaches every `src/**` folder except `__tests__` and the whole `pages/` subtree, which carries its own off-block for the bracket-named dynamic routes and nested slug folders Astro's file-based routing produces. See `canon/context/tooling
|
|
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.
|
|
26
26
|
|
|
27
27
|
## Typecheck
|
|
28
28
|
|
|
@@ -60,7 +60,7 @@ Append to the `## Scripts` table:
|
|
|
60
60
|
|
|
61
61
|
## CI docs (extend)
|
|
62
62
|
|
|
63
|
-
In `canon/context/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/ci.md -->
|
|
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 -->
|
|
64
64
|
|
|
65
65
|
## Social card route
|
|
66
66
|
|