@erclx/canon 5.33.0 → 5.35.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 +3 -5
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/REQUIREMENT.md +2 -3
- package/claude/skills/api-design/SKILL.md +2 -3
- package/claude/skills/api-design/references/adopted.md +1 -1
- package/claude/skills/auto-ship/references/draft-mark.md +1 -1
- package/claude/skills/canon-cli/SKILL.md +10 -11
- package/claude/skills/canon-feedback/REQUIREMENT.md +1 -0
- package/claude/skills/canon-feedback/SKILL.md +10 -28
- package/claude/skills/canon-feedback/references/template.md +53 -0
- package/claude/skills/canvas-board/SKILL.md +4 -1
- package/claude/skills/canvas-board/references/canvas.md +26 -3
- package/claude/skills/code-craft/REQUIREMENT.md +1 -1
- package/claude/skills/code-craft/SKILL.md +0 -1
- package/claude/skills/create-rule/SKILL.md +1 -1
- package/claude/skills/create-rule/references/rule.md +3 -1
- package/claude/skills/create-skill/SKILL.md +2 -1
- package/claude/skills/create-skill/references/skill-paths.md +12 -15
- package/{standards/standard.md → claude/skills/create-skill/references/skill-reference.md} +50 -45
- package/claude/skills/create-skill/references/skill.md +10 -4
- package/claude/skills/draft-and-pick/SKILL.md +1 -1
- package/claude/skills/git-commit/references/versioning.md +13 -13
- package/claude/skills/git-issue/references/issue.md +4 -1
- package/claude/skills/git-pr/SKILL.md +1 -1
- package/claude/skills/git-pr/references/publish.md +1 -1
- package/claude/skills/review-address/REQUIREMENT.md +1 -1
- package/claude/skills/review-address/SKILL.md +2 -2
- package/claude/skills/review-craft/references/ui.md +1 -1
- package/claude/skills/review-pr/SKILL.md +3 -3
- package/claude/skills/review-pr/references/post.md +1 -1
- package/claude/skills/review-ui/REQUIREMENT.md +1 -1
- package/claude/skills/review-ui/SKILL.md +8 -6
- package/claude/skills/review-ui/references/driving.md +8 -8
- package/claude/skills/review-ui/references/post.md +52 -16
- package/claude/skills/role-orchestrator/REQUIREMENT.md +2 -2
- package/claude/skills/role-orchestrator/SKILL.md +11 -12
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +3 -1
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +11 -8
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +10 -4
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +11 -2
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +11 -12
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +12 -15
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +6 -4
- package/claude/skills/role-orchestrator/scripts/poll.ts +50 -1
- package/claude/skills/role-reviewer/REQUIREMENT.md +17 -7
- package/claude/skills/role-reviewer/SKILL.md +17 -10
- package/claude/skills/role-worker/REQUIREMENT.md +1 -1
- package/claude/skills/role-worker/SKILL.md +6 -6
- package/claude/skills/session-compact/references/handoff-note.md +1 -1
- package/claude/skills/target-setup/SKILL.md +12 -6
- package/claude/skills/target-setup/references/detect.md +22 -5
- package/claude/skills/test-craft/REQUIREMENT.md +2 -3
- package/claude/skills/test-craft/SKILL.md +15 -3
- package/docs/agents/audits.md +1 -1
- package/docs/agents/canvas.md +5 -1
- package/docs/agents/commands.md +3 -5
- package/docs/agents/counts.md +5 -6
- package/docs/agents/drift-surfaces.md +15 -19
- package/docs/agents/gov-detect.md +47 -0
- package/docs/agents/index.md +6 -4
- package/docs/agents/install-and-sync.md +11 -31
- package/docs/agents/markdown-audit.md +10 -12
- package/docs/agents/migrate.md +1 -1
- package/docs/agents/pr-evidence-layout.md +70 -0
- package/docs/agents/pr-evidence.md +8 -9
- package/docs/agents/pr-frames.md +7 -2
- package/docs/agents/pr-lift.md +132 -0
- package/docs/agents/records-stale.md +1 -1
- package/docs/agents/records-validate.md +11 -23
- package/docs/agents/records.md +1 -1
- package/docs/agents/routing.md +1 -1
- package/docs/agents/rule-citations.md +11 -14
- package/docs/agents/scripting.md +4 -21
- package/docs/agents/state-scoped-risk.md +1 -1
- package/docs/agents/tasks-validate.md +1 -1
- package/docs/agents/teach.md +1 -1
- package/docs/target/index.md +1 -0
- package/docs/target/migrations.md +4 -0
- package/docs/target/projects.md +11 -11
- package/docs/target/scenarios.md +5 -6
- package/docs/target/standards-moved.md +32 -0
- package/docs/target/sync.md +5 -5
- package/docs/workflow/operating-model.md +16 -18
- package/docs/workflow/skill-map.md +1 -1
- package/governance/detect.toml +197 -0
- package/governance/renames.toml +0 -1
- package/governance/rules/code/000-code.md +1 -13
- package/governance/rules/code/010-testing.md +2 -30
- package/governance/rules/ui/400-ui.md +1 -1
- package/governance/rules/ui/480-canvas.md +13 -0
- package/governance/stacks/base.toml +15 -8
- package/governance/stacks/go.toml +2 -0
- package/governance/stacks/node.toml +1 -1
- package/governance/stacks/php.toml +2 -0
- package/governance/stacks/python.toml +1 -1
- package/package.json +1 -4
- package/scripts/core/install-check.sh +2 -3
- package/scripts/core/regen-hero.ts +0 -6
- package/scripts/lib/ui.sh +0 -9
- package/src/audits/catalog.ts +0 -9
- package/src/canvas/arrange.ts +66 -0
- package/src/canvas/capture.ts +20 -6
- package/src/canvas/content.ts +39 -0
- package/src/canvas/fit.ts +238 -0
- package/src/canvas/tokens.ts +28 -10
- package/src/capture/frame-values.ts +2 -32
- package/src/claude/routing.ts +1 -1
- package/src/claude/skills-reach.ts +4 -6
- package/src/cli/help.ts +3 -4
- package/src/cli.ts +0 -2
- package/src/commands/canvas.ts +179 -1
- package/src/commands/gov.ts +149 -10
- package/src/commands/init.ts +1 -1
- package/src/commands/labels.ts +4 -44
- package/src/commands/migrate.ts +1 -1
- package/src/commands/pr.ts +376 -1
- package/src/commands/records.ts +4 -20
- package/src/commands/tooling.ts +2 -4
- package/src/counts/catalogs.ts +0 -6
- package/src/feedback/body.ts +26 -5
- package/src/gate/boundaries.ts +2 -2
- package/src/gate/measures.ts +64 -72
- package/src/gate/sequencer.ts +51 -2
- package/src/gate/stages.ts +3 -22
- package/src/gov/citations.ts +11 -43
- package/src/gov/detect.ts +301 -0
- package/src/gov/list.ts +23 -2
- package/src/gov/stacks.ts +9 -0
- package/src/markdown/bans.ts +1 -1
- package/src/migrate/scratch-evidence.ts +2 -2
- package/src/pr/evidence.ts +218 -45
- package/src/pr/frames.ts +23 -3
- package/src/pr/lift.ts +418 -0
- package/src/pr/rest.ts +3 -0
- package/src/pr/review-scope.ts +76 -0
- package/src/records/validate.ts +6 -194
- package/src/sandbox/equivalence.ts +14 -7
- package/src/sandbox/provision.ts +0 -5
- package/src/secrets/shipped.ts +1 -2
- package/src/serve/static.ts +1 -2
- package/src/shipped/references.ts +29 -80
- package/src/sync/check.ts +18 -14
- package/src/tooling/inject.ts +6 -37
- package/src/tooling/read.ts +2 -3
- package/src/tooling/scan.ts +4 -17
- package/src/tooling/subfolder.ts +2 -66
- package/tooling/base/configs/scripts/verify.json +1 -1
- package/tooling/base/configs/scripts/verify.ts +1 -12
- package/tooling/base/manifest.toml +1 -2
- package/tooling/base/reference.md +10 -16
- package/tooling/base/seeds/.github/workflows/verify.yml +0 -3
- package/tooling/base/seeds/.lintstagedrc +1 -2
- package/tooling/base/seeds/canon/context/ci.md +0 -1
- package/tooling/base/seeds/canon/context/development.md +1 -1
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +13 -1
- package/tooling/go/reference.md +1 -1
- package/tooling/php/reference.md +1 -1
- package/tooling/python/reference.md +2 -2
- package/tooling/web/configs/.github/workflows/verify.yml +0 -3
- package/tooling/web/manifest.toml +0 -1
- package/tooling/web/reference.md +2 -4
- package/docs/agents/standards-audit.md +0 -40
- package/governance/rules/tooling/765-spelling.md +0 -19
- package/governance/stacks/astro.toml +0 -2
- package/governance/stacks/nextjs.toml +0 -2
- package/governance/stacks/node-server.toml +0 -2
- package/governance/stacks/python-fastapi.toml +0 -2
- package/governance/stacks/react.toml +0 -2
- package/src/commands/standards.ts +0 -278
- package/src/labels/spelling.ts +0 -120
- package/src/standards/audit.ts +0 -146
- package/src/standards/list.ts +0 -33
- package/src/standards/read.ts +0 -177
- package/src/standards/scope.ts +0 -56
- package/standards/index.md +0 -10
- package/tooling/base/configs/.vscode/extensions.json +0 -9
- package/tooling/base/configs/.vscode/settings.json +0 -3
- package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
- package/tooling/base/seeds/.cspell/tech-stack.txt +0 -25
- package/tooling/base/seeds/cspell.json +0 -20
- package/tooling/go/seeds/.cspell/tech-stack.txt +0 -16
- package/tooling/nextjs/seeds/.cspell/tech-stack.txt +0 -3
- package/tooling/php/seeds/.cspell/tech-stack.txt +0 -5
- package/tooling/python/seeds/.cspell/tech-stack.txt +0 -18
- package/tooling/vite-react/seeds/.cspell/project-terms.txt +0 -1
- package/tooling/vite-react/seeds/.cspell/tech-stack.txt +0 -2
- package/tooling/web/configs/.vscode/extensions.json +0 -13
- package/tooling/web/configs/.vscode/settings.json +0 -10
- package/tooling/web/seeds/.cspell/tech-stack.txt +0 -21
package/src/commands/pr.ts
CHANGED
|
@@ -48,15 +48,27 @@ import {
|
|
|
48
48
|
FRAMES_BRANCH,
|
|
49
49
|
type FramesApi,
|
|
50
50
|
type FramesRefusal,
|
|
51
|
+
type FrameTheme,
|
|
51
52
|
frameLink,
|
|
52
53
|
isFrameHead,
|
|
53
54
|
isFramePass,
|
|
55
|
+
isFrameTheme,
|
|
54
56
|
pruneFrames,
|
|
55
57
|
pushFrame,
|
|
56
58
|
readFrame,
|
|
57
59
|
type TreeEntry,
|
|
58
60
|
} from '@/pr/frames'
|
|
59
61
|
import { type HeadRefusal, resolveHead, resolveTip } from '@/pr/head'
|
|
62
|
+
import {
|
|
63
|
+
DEFAULT_WAIT_SECONDS,
|
|
64
|
+
type EvidenceGate,
|
|
65
|
+
type GitRunner,
|
|
66
|
+
lift,
|
|
67
|
+
type LiftApi,
|
|
68
|
+
type LiftRefusal,
|
|
69
|
+
type PullState,
|
|
70
|
+
readBaseStanding,
|
|
71
|
+
} from '@/pr/lift'
|
|
60
72
|
import {
|
|
61
73
|
dropAddressLine,
|
|
62
74
|
findLocalServer,
|
|
@@ -82,6 +94,7 @@ import { type TickRefusal, tickBoxes } from '@/pr/tick'
|
|
|
82
94
|
import {
|
|
83
95
|
commentRowOf,
|
|
84
96
|
identityOf,
|
|
97
|
+
mergeStateOf,
|
|
85
98
|
type PullIdentity,
|
|
86
99
|
parseJsonLines,
|
|
87
100
|
type RestComment,
|
|
@@ -91,6 +104,7 @@ import {
|
|
|
91
104
|
selectBranchPull,
|
|
92
105
|
} from '@/pr/rest'
|
|
93
106
|
import { resolveReviewScope } from '@/pr/review-scope'
|
|
107
|
+
import { PROJECT_ROOT } from '@/roots/project'
|
|
94
108
|
import { intro, logInfo, logStep, logWarn, outro, plural } from '@/cli/ui'
|
|
95
109
|
|
|
96
110
|
const GH_TIMEOUT_MS = 30_000
|
|
@@ -117,6 +131,11 @@ interface EvidenceOptions extends ReadOptions {
|
|
|
117
131
|
readonly check?: boolean
|
|
118
132
|
}
|
|
119
133
|
|
|
134
|
+
interface LiftCommandOptions extends ReadOptions {
|
|
135
|
+
readonly check?: boolean
|
|
136
|
+
readonly wait?: string
|
|
137
|
+
}
|
|
138
|
+
|
|
120
139
|
interface LocalOptions extends ReadOptions {
|
|
121
140
|
readonly remove?: boolean
|
|
122
141
|
}
|
|
@@ -164,6 +183,7 @@ interface FramesOptions extends ReadOptions {
|
|
|
164
183
|
readonly box?: string
|
|
165
184
|
readonly head?: string
|
|
166
185
|
readonly pass?: string
|
|
186
|
+
readonly theme?: string
|
|
167
187
|
readonly drop?: boolean
|
|
168
188
|
readonly prune?: string
|
|
169
189
|
}
|
|
@@ -175,6 +195,7 @@ type FramesCommandRefusal =
|
|
|
175
195
|
| 'bad-box'
|
|
176
196
|
| 'bad-head'
|
|
177
197
|
| 'bad-pass'
|
|
198
|
+
| 'bad-theme'
|
|
178
199
|
| 'bad-days'
|
|
179
200
|
| 'unreadable-frame'
|
|
180
201
|
| 'read-only'
|
|
@@ -193,6 +214,8 @@ const FRAMES_REFUSALS: Record<FramesCommandRefusal, string> = {
|
|
|
193
214
|
'--head takes a commit sha, 7 to 40 lowercase hex characters. A branch name or a path would file the frame under a folder that names no commit.',
|
|
194
215
|
'bad-pass':
|
|
195
216
|
'--add takes --pass <stamp>, the instant the pass started in compact UTC such as 20261002T154450Z, so a second pass never replaces an earlier one.',
|
|
217
|
+
'bad-theme':
|
|
218
|
+
'--theme takes dark or light and goes with --add, so a second shot of one box never replaces the first.',
|
|
196
219
|
'bad-days': '--prune takes a positive whole number of days.',
|
|
197
220
|
'unreadable-frame':
|
|
198
221
|
'The file named by --add is missing, empty, or not a PNG, so it would embed as a broken image. Nothing is pushed.',
|
|
@@ -650,6 +673,62 @@ export function register(program: Command): void {
|
|
|
650
673
|
process.exitCode = await runEvidence(number, opts)
|
|
651
674
|
})
|
|
652
675
|
|
|
676
|
+
pr.command('lift')
|
|
677
|
+
.description(
|
|
678
|
+
"Lift a pull request's draft mark once its reviews, evidence, standing, and CI all read clear at the head",
|
|
679
|
+
)
|
|
680
|
+
.argument('[number]', 'Pull request to lift, defaulting to this branch')
|
|
681
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
682
|
+
.option('--root <path>', 'Repository to read, defaulting to the cwd')
|
|
683
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
684
|
+
.option('--check', 'Report every gate and write nothing')
|
|
685
|
+
.option(
|
|
686
|
+
'--wait <seconds>',
|
|
687
|
+
`Hold for a pending CI run up to this long, default ${DEFAULT_WAIT_SECONDS}`,
|
|
688
|
+
)
|
|
689
|
+
.addHelpText(
|
|
690
|
+
'after',
|
|
691
|
+
[
|
|
692
|
+
'',
|
|
693
|
+
'The verb reads the gate itself, so any session that closes a review can',
|
|
694
|
+
'run it and nobody has to vouch for who is asking. Gates, in the order',
|
|
695
|
+
'they are read:',
|
|
696
|
+
' review the newest `## Review` is closed at the head, off its marker',
|
|
697
|
+
' ui the newest `## UI review` is closed at the head, bound only',
|
|
698
|
+
' where `canon pr evidence --check` carries a checklist',
|
|
699
|
+
' evidence the evidence check reads settled',
|
|
700
|
+
' standing the head is fresh against the base, not stale or conflicted',
|
|
701
|
+
' ci every check run on the tip passed, waited on up to --wait',
|
|
702
|
+
'',
|
|
703
|
+
'Review state, UI state, evidence, and standing are read before the CI',
|
|
704
|
+
'wait, so a reviewer whose partner is still driving refuses at once. The',
|
|
705
|
+
'head and the reviews are read again before the mutation.',
|
|
706
|
+
'',
|
|
707
|
+
'Reasons:',
|
|
708
|
+
' lifted, already-ready, clear (--check)',
|
|
709
|
+
' pull-closed, review-missing, review-open, review-unmarked,',
|
|
710
|
+
' review-behind, ui-missing, ui-open, ui-unmarked, ui-behind,',
|
|
711
|
+
' evidence-owed, standing-stale, standing-conflict, standing-unknown,',
|
|
712
|
+
' ci-pending (--check), ci-failing, ci-conflicted, ci-timeout,',
|
|
713
|
+
' head-moved, gh-failed',
|
|
714
|
+
'',
|
|
715
|
+
'Every read is REST. The mutation is the one GraphQL call.',
|
|
716
|
+
'',
|
|
717
|
+
'Exit codes:',
|
|
718
|
+
' 0 lifted, already-ready, or every gate clear under --check',
|
|
719
|
+
' 1 refused, with the reason in the JSON record',
|
|
720
|
+
'',
|
|
721
|
+
'Examples:',
|
|
722
|
+
' canon pr lift 1341 --json',
|
|
723
|
+
' canon pr lift 1341 --check --json',
|
|
724
|
+
' canon pr lift --wait 600',
|
|
725
|
+
'',
|
|
726
|
+
].join('\n'),
|
|
727
|
+
)
|
|
728
|
+
.action(async (number: string | undefined, opts: LiftCommandOptions) => {
|
|
729
|
+
process.exitCode = await runLift(number, opts)
|
|
730
|
+
})
|
|
731
|
+
|
|
653
732
|
pr.command('preview')
|
|
654
733
|
.description(
|
|
655
734
|
"Deploy the pull request's branch to a Cloudflare Pages preview and report its address",
|
|
@@ -841,6 +920,10 @@ export function register(program: Command): void {
|
|
|
841
920
|
'--pass <stamp>',
|
|
842
921
|
'The instant the pass started, in compact UTC such as 20261002T154450Z, with --add',
|
|
843
922
|
)
|
|
923
|
+
.option(
|
|
924
|
+
'--theme <theme>',
|
|
925
|
+
'The theme the frame was shot at, dark or light, with --add, keeping two shots of one box apart',
|
|
926
|
+
)
|
|
844
927
|
.option('--drop', "Rewrite the branch without this pull request's frames")
|
|
845
928
|
.option(
|
|
846
929
|
'--prune <days>',
|
|
@@ -852,7 +935,8 @@ export function register(program: Command): void {
|
|
|
852
935
|
'',
|
|
853
936
|
`Writes only refs/heads/${FRAMES_BRANCH}, through the GitHub git data API,`,
|
|
854
937
|
'so nothing is checked out and no other branch is touched. A frame lands',
|
|
855
|
-
'at pr-<number>/<short-head>/<pass>/box-<n>.png,
|
|
938
|
+
'at pr-<number>/<short-head>/<pass>/box-<n>.png, or box-<n>-<theme>.png',
|
|
939
|
+
'with --theme, and `link` names that',
|
|
856
940
|
`path on ${FRAMES_BRANCH}, so it survives a rewrite that drops other pull`,
|
|
857
941
|
'requests and resolves until its own pull request is dropped. The pass',
|
|
858
942
|
'segment keeps a second pass at one head and box from replacing an image',
|
|
@@ -871,6 +955,7 @@ export function register(program: Command): void {
|
|
|
871
955
|
' bad-box --add without a positive --box',
|
|
872
956
|
' bad-head --head is not 7 to 40 lowercase hex characters',
|
|
873
957
|
' bad-pass --add without a compact UTC --pass stamp',
|
|
958
|
+
' bad-theme --theme is not dark or light, or no --add',
|
|
874
959
|
' bad-days --prune without a positive number of days',
|
|
875
960
|
' unreadable-frame the --add file is missing, empty, or not a PNG',
|
|
876
961
|
' read-only GitHub refused the write, as on a fork token',
|
|
@@ -2906,6 +2991,12 @@ function framesArgumentRefusal(
|
|
|
2906
2991
|
) {
|
|
2907
2992
|
return 'bad-pass'
|
|
2908
2993
|
}
|
|
2994
|
+
if (
|
|
2995
|
+
opts.theme !== undefined &&
|
|
2996
|
+
(opts.add === undefined || !isFrameTheme(opts.theme))
|
|
2997
|
+
) {
|
|
2998
|
+
return 'bad-theme'
|
|
2999
|
+
}
|
|
2909
3000
|
return undefined
|
|
2910
3001
|
}
|
|
2911
3002
|
|
|
@@ -3005,11 +3096,13 @@ async function runFrames(
|
|
|
3005
3096
|
|
|
3006
3097
|
const box = Number(opts.box)
|
|
3007
3098
|
const pass = opts.pass ?? ''
|
|
3099
|
+
const theme = opts.theme as FrameTheme | undefined
|
|
3008
3100
|
const result = await pushFrame(api, {
|
|
3009
3101
|
number: pull,
|
|
3010
3102
|
head,
|
|
3011
3103
|
pass,
|
|
3012
3104
|
box,
|
|
3105
|
+
theme,
|
|
3013
3106
|
bytes: bytes ?? new Uint8Array(),
|
|
3014
3107
|
})
|
|
3015
3108
|
if (result.kind === 'refused') return refuseWrite(result.reason)
|
|
@@ -3018,6 +3111,7 @@ async function runFrames(
|
|
|
3018
3111
|
{
|
|
3019
3112
|
number: pull,
|
|
3020
3113
|
box,
|
|
3114
|
+
...(theme === undefined ? {} : { theme }),
|
|
3021
3115
|
head,
|
|
3022
3116
|
pass,
|
|
3023
3117
|
path: result.path,
|
|
@@ -3029,6 +3123,287 @@ async function runFrames(
|
|
|
3029
3123
|
)
|
|
3030
3124
|
}
|
|
3031
3125
|
|
|
3126
|
+
const LIFT_REFUSALS: Record<LiftRefusal | 'bad-wait' | 'gh-missing', string> = {
|
|
3127
|
+
'bad-wait': '--wait takes a whole number of seconds, zero or more.',
|
|
3128
|
+
'gh-missing': PULL_REFUSALS['gh-missing'],
|
|
3129
|
+
'pull-closed': 'The pull request is closed, so there is no mark to lift.',
|
|
3130
|
+
'review-missing': 'The thread carries no code review, so none has closed.',
|
|
3131
|
+
'review-open':
|
|
3132
|
+
'The newest code review is open, so a finding still owes a fix.',
|
|
3133
|
+
'review-unmarked':
|
|
3134
|
+
'The newest code review carries no read-time marker, so the commit it covers is a GitHub stamp this gate does not trust.',
|
|
3135
|
+
'review-behind':
|
|
3136
|
+
'The newest code review closed on an earlier commit than the head.',
|
|
3137
|
+
'ui-missing':
|
|
3138
|
+
'The evidence comment carries a checklist and no UI review has been posted.',
|
|
3139
|
+
'ui-open': 'The newest UI review is open, so a box still owes a fix.',
|
|
3140
|
+
'ui-unmarked':
|
|
3141
|
+
'The newest UI review carries no head marker, so the commit it covers is a GitHub stamp this gate does not trust.',
|
|
3142
|
+
'ui-behind':
|
|
3143
|
+
'The newest UI review closed on an earlier commit than the head.',
|
|
3144
|
+
'evidence-owed':
|
|
3145
|
+
'The evidence check reads owed, so the ship chain left a step undone.',
|
|
3146
|
+
'standing-stale':
|
|
3147
|
+
'The base moved under a path this branch writes since it branched, so CI measured a tree that is gone.',
|
|
3148
|
+
'standing-conflict': 'The head does not merge cleanly into the base.',
|
|
3149
|
+
'standing-unknown':
|
|
3150
|
+
'The head or the base did not resolve, which a fork or a deleted head leaves, so its standing is unread and waits on a person.',
|
|
3151
|
+
'ci-pending': 'A check run on the tip has yet to conclude.',
|
|
3152
|
+
'ci-failing': 'A check run on the tip failed.',
|
|
3153
|
+
'ci-conflicted':
|
|
3154
|
+
'The branch conflicts with its base, so no run will start for the tip.',
|
|
3155
|
+
'ci-timeout': 'A check run was still pending when --wait ran out.',
|
|
3156
|
+
'head-moved':
|
|
3157
|
+
'A push landed during the wait, so the gates cleared a commit that is no longer the head.',
|
|
3158
|
+
'gh-failed':
|
|
3159
|
+
'A read or the mutation was refused, so nothing is reported as lifted.',
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
const READY_MUTATION =
|
|
3163
|
+
'mutation($id: ID!) { markPullRequestReadyForReview(input: {pullRequestId: $id}) { pullRequest { isDraft } } }'
|
|
3164
|
+
|
|
3165
|
+
function gitRunner(root: string): GitRunner {
|
|
3166
|
+
return async (args) => {
|
|
3167
|
+
const result = await execa('git', ['-C', root, ...args], {
|
|
3168
|
+
reject: false,
|
|
3169
|
+
timeout: GH_TIMEOUT_MS,
|
|
3170
|
+
env: gitEnv(),
|
|
3171
|
+
extendEnv: false,
|
|
3172
|
+
})
|
|
3173
|
+
return result.exitCode === 0 ? String(result.stdout) : undefined
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
/**
|
|
3178
|
+
* Reads the evidence check through this checkout's own CLI rather than a
|
|
3179
|
+
* second copy of its logic, so the lift and a caller running
|
|
3180
|
+
* `canon pr evidence --check` read one answer.
|
|
3181
|
+
*/
|
|
3182
|
+
async function readEvidenceGate(
|
|
3183
|
+
root: string,
|
|
3184
|
+
number: string,
|
|
3185
|
+
): Promise<EvidenceGate | undefined> {
|
|
3186
|
+
const result = await execa(
|
|
3187
|
+
process.execPath,
|
|
3188
|
+
[
|
|
3189
|
+
join(PROJECT_ROOT, 'src', 'cli.ts'),
|
|
3190
|
+
'pr',
|
|
3191
|
+
'evidence',
|
|
3192
|
+
number,
|
|
3193
|
+
'--check',
|
|
3194
|
+
'--json',
|
|
3195
|
+
'--root',
|
|
3196
|
+
root,
|
|
3197
|
+
],
|
|
3198
|
+
{
|
|
3199
|
+
cwd: root,
|
|
3200
|
+
reject: false,
|
|
3201
|
+
timeout: GH_TIMEOUT_MS * 2,
|
|
3202
|
+
env: { ...gitEnv(), CANON_NON_INTERACTIVE: '1' },
|
|
3203
|
+
extendEnv: false,
|
|
3204
|
+
},
|
|
3205
|
+
)
|
|
3206
|
+
const last = String(result.stdout)
|
|
3207
|
+
.split('\n')
|
|
3208
|
+
.filter((line) => line.trim() !== '')
|
|
3209
|
+
.at(-1)
|
|
3210
|
+
if (last === undefined) return undefined
|
|
3211
|
+
|
|
3212
|
+
let record: { reason?: unknown; owed?: unknown; checklist?: unknown }
|
|
3213
|
+
try {
|
|
3214
|
+
record = JSON.parse(last)
|
|
3215
|
+
} catch {
|
|
3216
|
+
return undefined
|
|
3217
|
+
}
|
|
3218
|
+
if (record.reason !== 'settled' && record.reason !== 'owed') return undefined
|
|
3219
|
+
|
|
3220
|
+
return {
|
|
3221
|
+
isSettled: record.reason === 'settled',
|
|
3222
|
+
owed: Array.isArray(record.owed) ? record.owed.map(String) : [],
|
|
3223
|
+
hasChecklist:
|
|
3224
|
+
typeof record.checklist === 'string' && record.checklist !== '',
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
function liftApi(root: string, number: string, baseRef: string): LiftApi {
|
|
3229
|
+
const git = gitRunner(root)
|
|
3230
|
+
|
|
3231
|
+
return {
|
|
3232
|
+
readPull: async (): Promise<PullState | undefined> => {
|
|
3233
|
+
const stdout = await gh(root, [
|
|
3234
|
+
'api',
|
|
3235
|
+
`repos/{owner}/{repo}/pulls/${number}`,
|
|
3236
|
+
])
|
|
3237
|
+
if (stdout === null) return undefined
|
|
3238
|
+
try {
|
|
3239
|
+
const row = JSON.parse(stdout) as RestPull
|
|
3240
|
+
if (row.node_id === undefined || row.head?.ref === undefined) {
|
|
3241
|
+
return undefined
|
|
3242
|
+
}
|
|
3243
|
+
return {
|
|
3244
|
+
isDraft: row.draft === true,
|
|
3245
|
+
isOpen: row.state === 'open',
|
|
3246
|
+
nodeId: row.node_id,
|
|
3247
|
+
branch: row.head.ref,
|
|
3248
|
+
head: row.head.sha ?? '',
|
|
3249
|
+
}
|
|
3250
|
+
} catch {
|
|
3251
|
+
return undefined
|
|
3252
|
+
}
|
|
3253
|
+
},
|
|
3254
|
+
readTip: async (branch) => {
|
|
3255
|
+
const resolved = await resolveTip(branch, refReader(root))
|
|
3256
|
+
return resolved.kind === 'read' ? resolved.tip : undefined
|
|
3257
|
+
},
|
|
3258
|
+
readReviews: async () => {
|
|
3259
|
+
const reviews = await listReviews(root, number)
|
|
3260
|
+
return reviews.kind === 'read' ? reviews.rows.map(reviewRowOf) : undefined
|
|
3261
|
+
},
|
|
3262
|
+
readEvidence: () => readEvidenceGate(root, number),
|
|
3263
|
+
readStanding: (head) =>
|
|
3264
|
+
readBaseStanding(git, Number(number), head, baseRef),
|
|
3265
|
+
readChecks: async (tip) => {
|
|
3266
|
+
const [listed, pulled] = await Promise.all([
|
|
3267
|
+
gh(root, [
|
|
3268
|
+
'api',
|
|
3269
|
+
`repos/{owner}/{repo}/commits/${tip}/check-runs?per_page=100`,
|
|
3270
|
+
]),
|
|
3271
|
+
gh(root, ['api', `repos/{owner}/{repo}/pulls/${number}`]),
|
|
3272
|
+
])
|
|
3273
|
+
if (listed === null) return undefined
|
|
3274
|
+
try {
|
|
3275
|
+
const mergeState =
|
|
3276
|
+
pulled === null
|
|
3277
|
+
? undefined
|
|
3278
|
+
: mergeStateOf((JSON.parse(pulled) as RestPull).mergeable_state)
|
|
3279
|
+
const reading = withMergeState(
|
|
3280
|
+
mergeState,
|
|
3281
|
+
collapseChecks(tip, JSON.parse(listed) as CheckRunListing),
|
|
3282
|
+
)
|
|
3283
|
+
return { state: reading.state, isConflicted: reading.conflicted }
|
|
3284
|
+
} catch {
|
|
3285
|
+
return undefined
|
|
3286
|
+
}
|
|
3287
|
+
},
|
|
3288
|
+
markReady: async (nodeId) => {
|
|
3289
|
+
const result = await execa(
|
|
3290
|
+
'gh',
|
|
3291
|
+
[
|
|
3292
|
+
'api',
|
|
3293
|
+
'graphql',
|
|
3294
|
+
'-f',
|
|
3295
|
+
`query=${READY_MUTATION}`,
|
|
3296
|
+
'-f',
|
|
3297
|
+
`id=${nodeId}`,
|
|
3298
|
+
],
|
|
3299
|
+
{
|
|
3300
|
+
cwd: root,
|
|
3301
|
+
reject: false,
|
|
3302
|
+
timeout: GH_TIMEOUT_MS,
|
|
3303
|
+
env: gitEnv(),
|
|
3304
|
+
extendEnv: false,
|
|
3305
|
+
},
|
|
3306
|
+
)
|
|
3307
|
+
if (result.exitCode !== 0) return false
|
|
3308
|
+
// The flag is read back rather than the exit trusted, since the
|
|
3309
|
+
// mutation can answer 200 with an errors array and no change.
|
|
3310
|
+
try {
|
|
3311
|
+
const body = JSON.parse(String(result.stdout)) as {
|
|
3312
|
+
data?: {
|
|
3313
|
+
markPullRequestReadyForReview?: {
|
|
3314
|
+
pullRequest?: { isDraft?: boolean }
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
return (
|
|
3319
|
+
body.data?.markPullRequestReadyForReview?.pullRequest?.isDraft ===
|
|
3320
|
+
false
|
|
3321
|
+
)
|
|
3322
|
+
} catch {
|
|
3323
|
+
return false
|
|
3324
|
+
}
|
|
3325
|
+
},
|
|
3326
|
+
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
3327
|
+
now: () => Date.now(),
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
async function runLift(
|
|
3332
|
+
number: string | undefined,
|
|
3333
|
+
opts: LiftCommandOptions,
|
|
3334
|
+
): Promise<number> {
|
|
3335
|
+
const root = resolve(opts.root ?? process.cwd())
|
|
3336
|
+
const emitJson = opts.json ?? false
|
|
3337
|
+
|
|
3338
|
+
intro('canon pr lift')
|
|
3339
|
+
|
|
3340
|
+
let waitSeconds = DEFAULT_WAIT_SECONDS
|
|
3341
|
+
if (opts.wait !== undefined) {
|
|
3342
|
+
if (!/^\d+$/.test(opts.wait)) {
|
|
3343
|
+
return refuseWith('bad-wait', LIFT_REFUSALS['bad-wait'], emitJson, root)
|
|
3344
|
+
}
|
|
3345
|
+
waitSeconds = Number(opts.wait)
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
if (Bun.which('gh') === null) {
|
|
3349
|
+
return refuseWith('gh-missing', LIFT_REFUSALS['gh-missing'], emitJson, root)
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
const resolved = await resolvePullNumber(root, number)
|
|
3353
|
+
if (resolved.kind === 'refused') {
|
|
3354
|
+
return refuseWith(
|
|
3355
|
+
resolved.reason,
|
|
3356
|
+
PULL_REFUSALS[resolved.reason],
|
|
3357
|
+
emitJson,
|
|
3358
|
+
root,
|
|
3359
|
+
)
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
// Read once up front for the base the standing is measured against. The
|
|
3363
|
+
// injected `readPull` re-reads the object on every gate that needs it.
|
|
3364
|
+
const initial = await readPull(root, resolved.number)
|
|
3365
|
+
if (initial.kind === 'refused') {
|
|
3366
|
+
return refuseWith(
|
|
3367
|
+
initial.reason,
|
|
3368
|
+
PULL_REFUSALS[initial.reason],
|
|
3369
|
+
emitJson,
|
|
3370
|
+
root,
|
|
3371
|
+
)
|
|
3372
|
+
}
|
|
3373
|
+
const base = initial.row.base?.ref
|
|
3374
|
+
if (base === undefined || base === '') {
|
|
3375
|
+
return refuseWith('gh-failed', LIFT_REFUSALS['gh-failed'], emitJson, root)
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
const record = await lift(liftApi(root, resolved.number, `origin/${base}`), {
|
|
3379
|
+
isCheck: opts.check ?? false,
|
|
3380
|
+
waitSeconds,
|
|
3381
|
+
})
|
|
3382
|
+
|
|
3383
|
+
logStep(record.reason)
|
|
3384
|
+
logInfo(
|
|
3385
|
+
record.reason === 'lifted'
|
|
3386
|
+
? `#${resolved.number} is ready for review at ${record.head?.slice(0, 8)}`
|
|
3387
|
+
: record.reason === 'already-ready'
|
|
3388
|
+
? `#${resolved.number} is already ready for review`
|
|
3389
|
+
: record.reason === 'clear'
|
|
3390
|
+
? `every gate reads clear for #${resolved.number}`
|
|
3391
|
+
: LIFT_REFUSALS[record.reason],
|
|
3392
|
+
)
|
|
3393
|
+
for (const gate of record.gates) {
|
|
3394
|
+
if (!gate.isOk) logWarn(`${gate.gate}: ${gate.reason ?? 'not clear'}`)
|
|
3395
|
+
}
|
|
3396
|
+
outro()
|
|
3397
|
+
|
|
3398
|
+
if (emitJson) {
|
|
3399
|
+
process.stdout.write(
|
|
3400
|
+
`${JSON.stringify({ root, number: Number(resolved.number), ...record })}\n`,
|
|
3401
|
+
)
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
return record.isOk ? 0 : 1
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3032
3407
|
/**
|
|
3033
3408
|
* Frames a refusal on stderr in both modes and puts the record on stdout alone,
|
|
3034
3409
|
* so an operator reading the terminal sees the reason rather than a command
|
package/src/commands/records.ts
CHANGED
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
import {
|
|
30
30
|
type Finding,
|
|
31
31
|
isRecordKind,
|
|
32
|
-
isSharedScratch,
|
|
33
32
|
RECORD_KINDS,
|
|
34
33
|
type RecordKind,
|
|
35
34
|
type ValidateOutcome,
|
|
@@ -92,10 +91,7 @@ export function register(program: Command): void {
|
|
|
92
91
|
.argument('<kind>', `Record folder: ${RECORD_KINDS.join(', ')}`)
|
|
93
92
|
.helpOption('-h, --help', 'Show this help message')
|
|
94
93
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
95
|
-
.option(
|
|
96
|
-
'--root <path>',
|
|
97
|
-
'Project root, defaulting to the main worktree except on standards',
|
|
98
|
-
)
|
|
94
|
+
.option('--root <path>', 'Project root, defaulting to the main worktree')
|
|
99
95
|
.addHelpText(
|
|
100
96
|
'after',
|
|
101
97
|
[
|
|
@@ -104,8 +100,6 @@ export function register(program: Command): void {
|
|
|
104
100
|
' plans filename, required sections, and the suggested-and-answer contract',
|
|
105
101
|
' groundwork README and current-state files, numbering, dating, and a half-closed track',
|
|
106
102
|
' intake overview file, numbering, dating, and the four bullets every item carries',
|
|
107
|
-
' standards frontmatter, the scope section and its handoff list, and a filename',
|
|
108
|
-
' derived from the governed path',
|
|
109
103
|
'',
|
|
110
104
|
'Exit codes:',
|
|
111
105
|
' 0 every check passed',
|
|
@@ -113,12 +107,11 @@ export function register(program: Command): void {
|
|
|
113
107
|
' 2 at least one record carries a finding',
|
|
114
108
|
'',
|
|
115
109
|
'It reports and never writes, so a session fixes what the report names. A',
|
|
116
|
-
'session record has no history to undo a wrong repair from
|
|
117
|
-
'is installed and cited everywhere, so a rename is larger than a file move.',
|
|
110
|
+
'session record has no history to undo a wrong repair from.',
|
|
118
111
|
'',
|
|
119
112
|
'Examples:',
|
|
120
113
|
' canon records validate plans',
|
|
121
|
-
' canon records validate
|
|
114
|
+
' canon records validate teach',
|
|
122
115
|
' canon records validate intake --json',
|
|
123
116
|
'',
|
|
124
117
|
].join('\n'),
|
|
@@ -718,15 +711,6 @@ function reportRefusal(
|
|
|
718
711
|
return 1
|
|
719
712
|
}
|
|
720
713
|
|
|
721
|
-
/**
|
|
722
|
-
* A shared-scratch kind reads the main worktree root, so a linked worktree
|
|
723
|
-
* validates the records every other session reads. A tracked corpus reads the
|
|
724
|
-
* checkout the caller stands in, which is the copy that session has edited.
|
|
725
|
-
*/
|
|
726
|
-
function defaultRoot(kind: RecordKind): Promise<string> {
|
|
727
|
-
return isSharedScratch(kind) ? mainWorktreeRoot() : currentWorktreeRoot()
|
|
728
|
-
}
|
|
729
|
-
|
|
730
714
|
async function runValidate(
|
|
731
715
|
kind: string,
|
|
732
716
|
opts: ValidateCommandOptions,
|
|
@@ -745,7 +729,7 @@ async function runValidate(
|
|
|
745
729
|
)
|
|
746
730
|
}
|
|
747
731
|
|
|
748
|
-
const root = opts.root ?? (await
|
|
732
|
+
const root = opts.root ?? (await mainWorktreeRoot())
|
|
749
733
|
|
|
750
734
|
return report(await validateWithStacks(root, kind), emitJson, root)
|
|
751
735
|
}
|
package/src/commands/tooling.ts
CHANGED
|
@@ -99,8 +99,7 @@ export function register(program: Command): void {
|
|
|
99
99
|
' canon tooling sync vite-react web --skip base',
|
|
100
100
|
'',
|
|
101
101
|
'A target below its git root syncs as a subfolder. It withholds',
|
|
102
|
-
'.github/, which GitHub reads only at the root
|
|
103
|
-
'cspell.json registering the stack word lists.',
|
|
102
|
+
'.github/, which GitHub reads only at the root.',
|
|
104
103
|
'',
|
|
105
104
|
'To gate CI on tooling drift, run headlessly with neither flag. That',
|
|
106
105
|
'exits 1 when a file would be replaced and 0 when none would, which is',
|
|
@@ -276,8 +275,7 @@ function runList(opts: ListOptions): number {
|
|
|
276
275
|
|
|
277
276
|
/**
|
|
278
277
|
* Writes the reference to stdout and every frame line to stderr, so a caller
|
|
279
|
-
* capturing the output with `$(...)` receives the document alone.
|
|
280
|
-
* `print` in `src/commands/standards.ts`.
|
|
278
|
+
* capturing the output with `$(...)` receives the document alone.
|
|
281
279
|
*/
|
|
282
280
|
function printReference(stack: string): number {
|
|
283
281
|
intro('canon tooling reference')
|
package/src/counts/catalogs.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { join } from 'node:path'
|
|
|
3
3
|
import { AUDITS } from '@/audits/catalog'
|
|
4
4
|
import { listSkills } from '@/claude/skills-list'
|
|
5
5
|
import { buildGovCatalog } from '@/gov/list'
|
|
6
|
-
import { listStandards } from '@/standards/read'
|
|
7
6
|
|
|
8
7
|
export interface Catalog {
|
|
9
8
|
readonly id: string
|
|
@@ -59,11 +58,6 @@ export const CATALOGS: readonly Catalog[] = [
|
|
|
59
58
|
nouns: ['rule', 'rules'],
|
|
60
59
|
count: (root) => buildGovCatalog(root).rules.length,
|
|
61
60
|
},
|
|
62
|
-
{
|
|
63
|
-
id: 'standards',
|
|
64
|
-
nouns: ['standard', 'standards'],
|
|
65
|
-
count: (root) => listStandards(root).length,
|
|
66
|
-
},
|
|
67
61
|
{
|
|
68
62
|
id: 'commands',
|
|
69
63
|
nouns: ['command', 'commands'],
|
package/src/feedback/body.ts
CHANGED
|
@@ -18,13 +18,18 @@ export const REQUIRED_FIELDS = ['Surface', 'Observed', 'Proposed fix'] as const
|
|
|
18
18
|
* retire, and the validator would then enforce nothing.
|
|
19
19
|
*/
|
|
20
20
|
const FIELD_HEADING = /^###\s+(.+?)\s*$/
|
|
21
|
-
const FENCE = /^\s*(
|
|
21
|
+
const FENCE = /^\s*(`{3,}|~{3,})(.*)$/
|
|
22
|
+
|
|
23
|
+
interface OpenFence {
|
|
24
|
+
readonly marker: string
|
|
25
|
+
readonly length: number
|
|
26
|
+
}
|
|
22
27
|
|
|
23
28
|
export function parseSections(body: string): Map<string, string> {
|
|
24
29
|
const sections = new Map<string, string>()
|
|
25
30
|
let heading: string | undefined
|
|
26
31
|
let lines: string[] = []
|
|
27
|
-
let
|
|
32
|
+
let fence: OpenFence | undefined
|
|
28
33
|
|
|
29
34
|
const flush = (): void => {
|
|
30
35
|
if (heading === undefined) return
|
|
@@ -34,9 +39,25 @@ export function parseSections(body: string): Map<string, string> {
|
|
|
34
39
|
for (const line of body.split('\n')) {
|
|
35
40
|
// A Repro field routinely carries a fenced block, and a `###` comment
|
|
36
41
|
// inside one is shell rather than the next field. Reading it as a heading
|
|
37
|
-
// splits the section and drops everything under it.
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
// splits the section and drops everything under it. A Suggested shape
|
|
43
|
+
// wraps a template that holds its own fence in a longer one, so a fence
|
|
44
|
+
// closes only on the same marker at least as long, per CommonMark.
|
|
45
|
+
const fenceMatch = line.match(FENCE)
|
|
46
|
+
const run = fenceMatch?.[1]
|
|
47
|
+
if (run !== undefined) {
|
|
48
|
+
const marker = run.charAt(0)
|
|
49
|
+
const isBare = (fenceMatch?.[2] ?? '').trim() === ''
|
|
50
|
+
if (fence === undefined) {
|
|
51
|
+
fence = { marker, length: run.length }
|
|
52
|
+
} else if (
|
|
53
|
+
isBare &&
|
|
54
|
+
marker === fence.marker &&
|
|
55
|
+
run.length >= fence.length
|
|
56
|
+
) {
|
|
57
|
+
fence = undefined
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const match = fence ? null : line.match(FIELD_HEADING)
|
|
40
61
|
if (match?.[1] === undefined) {
|
|
41
62
|
lines.push(line)
|
|
42
63
|
continue
|
package/src/gate/boundaries.ts
CHANGED
|
@@ -337,8 +337,8 @@ export const capabilitySeeding: Measure = async (ctx) => {
|
|
|
337
337
|
* Every file under `claude/` resolved through its symlinks and rejected when it
|
|
338
338
|
* lands under `internal/`.
|
|
339
339
|
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
340
|
+
* An installer dereferences any symlink under the plugin root with no code in
|
|
341
|
+
* the path to filter, so a link added there ships whatever it points at. Walking with symlinks
|
|
342
342
|
* followed is what an install copies, so this is the only place the boundary
|
|
343
343
|
* can be measured. The walk tracks the real directories on its current descent
|
|
344
344
|
* and does not enter one a second time, the way `find -L` stops on a loop.
|