@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/canvas.ts
CHANGED
|
@@ -9,12 +9,15 @@ import {
|
|
|
9
9
|
DEFAULT_FRAME,
|
|
10
10
|
listPages,
|
|
11
11
|
moveFrame,
|
|
12
|
+
pruneLayout,
|
|
12
13
|
readPage,
|
|
13
14
|
readSelection,
|
|
14
15
|
renamePage,
|
|
15
16
|
resizeFrame,
|
|
16
17
|
} from '@/canvas/content'
|
|
18
|
+
import { planArrange } from '@/canvas/arrange'
|
|
17
19
|
import { captureCanvas, captureComposite } from '@/canvas/capture'
|
|
20
|
+
import { fitCanvas, isOwed } from '@/canvas/fit'
|
|
18
21
|
import {
|
|
19
22
|
type EditRefused,
|
|
20
23
|
editFrameAtIndex,
|
|
@@ -148,7 +151,7 @@ export function register(program: Command): void {
|
|
|
148
151
|
.command('canvas')
|
|
149
152
|
.helpOption('-h, --help', 'Show this help message')
|
|
150
153
|
.description(
|
|
151
|
-
'Local canvas of pages and HTML frames (serve, list, page, frame, selection, edit, editing, capture)',
|
|
154
|
+
'Local canvas of pages and HTML frames (serve, list, page, frame, selection, edit, editing, capture, fit, arrange)',
|
|
152
155
|
)
|
|
153
156
|
.addHelpText(
|
|
154
157
|
'after',
|
|
@@ -167,6 +170,8 @@ export function register(program: Command): void {
|
|
|
167
170
|
" canon canvas edit drafts/hero --element 4 --set 'color=var(--color-accent)'",
|
|
168
171
|
' canon canvas editing drafts/hero',
|
|
169
172
|
' canon canvas capture drafts/hero',
|
|
173
|
+
' canon canvas fit drafts/hero --json',
|
|
174
|
+
' canon canvas arrange drafts --check',
|
|
170
175
|
' canon canvas list --json',
|
|
171
176
|
'',
|
|
172
177
|
].join('\n'),
|
|
@@ -728,6 +733,179 @@ export function register(program: Command): void {
|
|
|
728
733
|
outro()
|
|
729
734
|
if (failed) process.exitCode = 1
|
|
730
735
|
})
|
|
736
|
+
|
|
737
|
+
withRoot(
|
|
738
|
+
canvas
|
|
739
|
+
.command('fit')
|
|
740
|
+
.description("Set each frame's height to its content")
|
|
741
|
+
.argument('<target>', '<page>/<frame> for one frame, <page> for all')
|
|
742
|
+
.option('--check', 'Report what is owed and write nothing'),
|
|
743
|
+
)
|
|
744
|
+
.addHelpText(
|
|
745
|
+
'after',
|
|
746
|
+
[
|
|
747
|
+
'',
|
|
748
|
+
'Measures each frame in the capture browser at its box width, through',
|
|
749
|
+
'the served address, so the faces the canvas serves are the ones read.',
|
|
750
|
+
'The height is the content height rounded up, with no margin. A',
|
|
751
|
+
'content wider than the box is reported and the box is never widened.',
|
|
752
|
+
'A frame whose body text names a family the canvas does not serve is',
|
|
753
|
+
'reported as font, since its height may not hold in another browser.',
|
|
754
|
+
'',
|
|
755
|
+
'Run canvas arrange after a fit that grew a frame, since rows below',
|
|
756
|
+
'do not move on their own.',
|
|
757
|
+
'',
|
|
758
|
+
'Exit codes:',
|
|
759
|
+
' 0 written, or nothing owed under --check',
|
|
760
|
+
' 1 refused or the browser failed',
|
|
761
|
+
' 2 --check found a height off, a wide frame, or a font',
|
|
762
|
+
'',
|
|
763
|
+
].join('\n'),
|
|
764
|
+
)
|
|
765
|
+
.action(async (target: string, opts: FitOptions) => {
|
|
766
|
+
process.exitCode = await runFit(target, opts)
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
withRoot(
|
|
770
|
+
canvas
|
|
771
|
+
.command('arrange')
|
|
772
|
+
.description('Re-lay a page in rows on the frame gap')
|
|
773
|
+
.argument('<page>', 'Page to arrange')
|
|
774
|
+
.option('--check', 'Report what is owed and write nothing'),
|
|
775
|
+
)
|
|
776
|
+
.addHelpText(
|
|
777
|
+
'after',
|
|
778
|
+
[
|
|
779
|
+
'',
|
|
780
|
+
'Keeps the rows the frames already sit in, orders each left to right on',
|
|
781
|
+
'the frame gap, stacks the rows on their tallest frame, and drops',
|
|
782
|
+
'layout entries whose frame file is gone.',
|
|
783
|
+
'',
|
|
784
|
+
'Exit codes:',
|
|
785
|
+
' 0 written, or nothing owed under --check',
|
|
786
|
+
' 1 refused',
|
|
787
|
+
' 2 --check found frames to move or entries to drop',
|
|
788
|
+
'',
|
|
789
|
+
].join('\n'),
|
|
790
|
+
)
|
|
791
|
+
.action(async (pageName: string, opts: FitOptions) => {
|
|
792
|
+
process.exitCode = await runArrange(pageName, opts)
|
|
793
|
+
})
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
interface FitOptions extends RootOptions {
|
|
797
|
+
readonly check?: boolean
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function runFit(target: string, opts: FitOptions): Promise<number> {
|
|
801
|
+
const emitJson = opts.json ?? false
|
|
802
|
+
const resolved = await resolveRoot(opts)
|
|
803
|
+
if (!resolved.ok) return refuse(resolved, emitJson)
|
|
804
|
+
|
|
805
|
+
const check = opts.check ?? false
|
|
806
|
+
let outcome: Awaited<ReturnType<typeof fitCanvas>>
|
|
807
|
+
try {
|
|
808
|
+
outcome = await fitCanvas(resolved.root, target, !check)
|
|
809
|
+
} catch (error) {
|
|
810
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
811
|
+
return refuse({ ok: false, reason: 'capture-failed', detail }, emitJson)
|
|
812
|
+
}
|
|
813
|
+
if (!outcome.ok) return refuse(outcome, emitJson)
|
|
814
|
+
|
|
815
|
+
const frames = outcome.rows.map((row) => ({ ...row, owed: isOwed(row) }))
|
|
816
|
+
const owed = frames.some((row) => row.owed)
|
|
817
|
+
if (emitJson) writeJson({ ok: true, written: outcome.written, frames })
|
|
818
|
+
intro(BANNER)
|
|
819
|
+
for (const row of frames) {
|
|
820
|
+
const name = `${row.page}/${row.frame}`
|
|
821
|
+
if (row.fitted === row.height) logInfo(`${name} fits at ${row.height}`)
|
|
822
|
+
else if (outcome.written) logAdd(`${name} ${row.height} to ${row.fitted}`)
|
|
823
|
+
else logStep(`${name} owes ${row.height} to ${row.fitted}`)
|
|
824
|
+
if (row.wide) logWarn(`${name} is wider than its box, so widen the box`)
|
|
825
|
+
if (row.viewport) {
|
|
826
|
+
logWarn(
|
|
827
|
+
`${name} sets a height in viewport units, so its height was not written and a fixed height fits it`,
|
|
828
|
+
)
|
|
829
|
+
}
|
|
830
|
+
if (row.font) {
|
|
831
|
+
logWarn(
|
|
832
|
+
`${name} sets ${row.family}, which the canvas does not serve, so its height may not hold in another browser`,
|
|
833
|
+
)
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
outro()
|
|
837
|
+
return check && owed ? 2 : 0
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async function runArrange(pageName: string, opts: FitOptions): Promise<number> {
|
|
841
|
+
const emitJson = opts.json ?? false
|
|
842
|
+
const resolved = await resolveRoot(opts)
|
|
843
|
+
if (!resolved.ok) return refuse(resolved, emitJson)
|
|
844
|
+
const { root } = resolved
|
|
845
|
+
|
|
846
|
+
const page = readPage(root, pageName)
|
|
847
|
+
if (!page) {
|
|
848
|
+
return refuse(
|
|
849
|
+
{
|
|
850
|
+
ok: false,
|
|
851
|
+
reason: 'no-page',
|
|
852
|
+
detail: `page ${pageName} does not exist`,
|
|
853
|
+
},
|
|
854
|
+
emitJson,
|
|
855
|
+
)
|
|
856
|
+
}
|
|
857
|
+
if (page.layoutIssue) {
|
|
858
|
+
return refuse(
|
|
859
|
+
{
|
|
860
|
+
ok: false,
|
|
861
|
+
reason: 'malformed-layout',
|
|
862
|
+
detail: `${pageName}/layout.json does not parse`,
|
|
863
|
+
},
|
|
864
|
+
emitJson,
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const check = opts.check ?? false
|
|
869
|
+
const pruned = pruneLayout(root, pageName, !check)
|
|
870
|
+
if (!pruned.ok) return refuse(pruned, emitJson)
|
|
871
|
+
|
|
872
|
+
const moves = planArrange(page.frames)
|
|
873
|
+
if (!check) {
|
|
874
|
+
for (const move of moves) {
|
|
875
|
+
const outcome = resizeFrame(root, pageName, move.name, move.to)
|
|
876
|
+
if (!outcome.ok) return refuse(outcome, emitJson)
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (emitJson) {
|
|
881
|
+
writeJson({
|
|
882
|
+
ok: true,
|
|
883
|
+
page: pageName,
|
|
884
|
+
written: !check,
|
|
885
|
+
moved: moves.map((move) => ({
|
|
886
|
+
frame: move.name,
|
|
887
|
+
from: { x: move.from.x, y: move.from.y },
|
|
888
|
+
to: { x: move.to.x, y: move.to.y },
|
|
889
|
+
})),
|
|
890
|
+
dropped: pruned.dropped,
|
|
891
|
+
})
|
|
892
|
+
}
|
|
893
|
+
intro(BANNER)
|
|
894
|
+
for (const move of moves) {
|
|
895
|
+
const text = `${pageName}/${move.name} ${move.from.x}, ${move.from.y} to ${move.to.x}, ${move.to.y}`
|
|
896
|
+
if (check) logStep(`${text} is owed`)
|
|
897
|
+
else logAdd(text)
|
|
898
|
+
}
|
|
899
|
+
for (const name of pruned.dropped) {
|
|
900
|
+
logWarn(
|
|
901
|
+
`${pageName}/${name} has no frame file, so its entry is ${check ? 'owed a drop' : 'dropped'}`,
|
|
902
|
+
)
|
|
903
|
+
}
|
|
904
|
+
if (moves.length === 0 && pruned.dropped.length === 0) {
|
|
905
|
+
logInfo(`${pageName} is already laid out`)
|
|
906
|
+
}
|
|
907
|
+
outro()
|
|
908
|
+
return check && (moves.length > 0 || pruned.dropped.length > 0) ? 2 : 0
|
|
731
909
|
}
|
|
732
910
|
|
|
733
911
|
interface CaptureOptions extends RootOptions {
|
package/src/commands/gov.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
govStackExists,
|
|
20
20
|
listGovStacks,
|
|
21
21
|
mergeExtraRules,
|
|
22
|
+
resolveExtraRules,
|
|
22
23
|
resolveRules,
|
|
23
24
|
} from '@/gov/stacks'
|
|
24
25
|
import {
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
type TestOrderReport,
|
|
52
53
|
} from '@/gov/test-order'
|
|
53
54
|
import { recordStamp, runDomainSync } from '@/sync/engine'
|
|
55
|
+
import { detectRules } from '@/gov/detect'
|
|
54
56
|
import { writeChainStamp } from '@/sync/stamp'
|
|
55
57
|
import { resolveTarget } from '@/targets/validate'
|
|
56
58
|
import {
|
|
@@ -141,7 +143,7 @@ export function register(program: Command): void {
|
|
|
141
143
|
gov
|
|
142
144
|
.command('install')
|
|
143
145
|
.description('Install a governance stack into .claude/rules/canon/')
|
|
144
|
-
.argument('[stack]', 'Stack name (e.g. base, node,
|
|
146
|
+
.argument('[stack]', 'Stack name (e.g. base, node, python)')
|
|
145
147
|
.argument('[target]', 'Target directory', '.')
|
|
146
148
|
.helpOption('-h, --help', 'Show this help message')
|
|
147
149
|
.option('--add <rules>', 'Comma-separated rules to layer on the stack')
|
|
@@ -150,9 +152,14 @@ export function register(program: Command): void {
|
|
|
150
152
|
[
|
|
151
153
|
'',
|
|
152
154
|
'Examples:',
|
|
153
|
-
' canon gov install
|
|
155
|
+
' canon gov install node',
|
|
154
156
|
' canon gov install node ../my-app',
|
|
155
|
-
' canon gov install
|
|
157
|
+
' canon gov install node --add 210-astro,260-shadcn ../my-app',
|
|
158
|
+
' canon gov install --add tasks,plan ../my-app',
|
|
159
|
+
'',
|
|
160
|
+
'With --add and no stack, the named rules install alone. A first',
|
|
161
|
+
'positional naming a stack is read as the stack, so pass . or a path',
|
|
162
|
+
'carrying a slash to force a target that shares a stack name.',
|
|
156
163
|
'',
|
|
157
164
|
].join('\n'),
|
|
158
165
|
)
|
|
@@ -221,6 +228,31 @@ export function register(program: Command): void {
|
|
|
221
228
|
process.exitCode = runList(opts)
|
|
222
229
|
})
|
|
223
230
|
|
|
231
|
+
gov
|
|
232
|
+
.command('detect')
|
|
233
|
+
.description('Propose opt-in rules from the evidence a target holds')
|
|
234
|
+
.argument('[target]', 'Target directory', '.')
|
|
235
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
236
|
+
.option('--stack <stack>', 'Leave out every rule this stack resolves')
|
|
237
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
238
|
+
.addHelpText(
|
|
239
|
+
'after',
|
|
240
|
+
[
|
|
241
|
+
'',
|
|
242
|
+
'Reads governance/detect.toml and matches each rule against the',
|
|
243
|
+
"target's manifests and git-listed paths to depth two. Pass the",
|
|
244
|
+
'proposals to canon gov install --add once confirmed.',
|
|
245
|
+
'',
|
|
246
|
+
'Examples:',
|
|
247
|
+
' canon gov detect --json',
|
|
248
|
+
' canon gov detect ../my-app --stack node --json',
|
|
249
|
+
'',
|
|
250
|
+
].join('\n'),
|
|
251
|
+
)
|
|
252
|
+
.action(async (target: string, opts: DetectOptions) => {
|
|
253
|
+
process.exitCode = await runDetect(target, opts)
|
|
254
|
+
})
|
|
255
|
+
|
|
224
256
|
gov
|
|
225
257
|
.command('test-order')
|
|
226
258
|
.description(
|
|
@@ -275,7 +307,7 @@ export function register(program: Command): void {
|
|
|
275
307
|
'members a closed catalog holds, then compares the stated figure',
|
|
276
308
|
'against what the tree actually counts.',
|
|
277
309
|
'',
|
|
278
|
-
'Catalogs read: skills, governance rules,
|
|
310
|
+
'Catalogs read: skills, governance rules, CLI commands,',
|
|
279
311
|
'and audit ids. Closed rather than derived, so widening the',
|
|
280
312
|
'set is a deliberate change rather than a side effect of a new list',
|
|
281
313
|
'command shipping elsewhere.',
|
|
@@ -409,10 +441,6 @@ export function register(program: Command): void {
|
|
|
409
441
|
'present and the question answerable.',
|
|
410
442
|
'',
|
|
411
443
|
'Forms read, with where each resolves:',
|
|
412
|
-
' standard canon standards <name> , against standards/ alone, which',
|
|
413
|
-
' is where the verb itself looks. internal/standards/ is not',
|
|
414
|
-
' tried, since a name resolving only there refuses for the',
|
|
415
|
-
' session that opens the citation.',
|
|
416
444
|
' path a backticked path carrying a directory segment and a file',
|
|
417
445
|
' extension, against the root',
|
|
418
446
|
" sibling a bare <nnn>-<slug>.md , against the citing rule's folder",
|
|
@@ -422,7 +450,7 @@ export function register(program: Command): void {
|
|
|
422
450
|
' naming a file, such as canon/context/<domain>.md or app/**/route.ts',
|
|
423
451
|
' a bare filename naming a convention, such as route.ts or manifest.toml',
|
|
424
452
|
' a span carrying no file extension, which is a folder or a module',
|
|
425
|
-
' specifier, such as src/pages/ , next/font , or claude/
|
|
453
|
+
' specifier, such as src/pages/ , next/font , or claude/skills',
|
|
426
454
|
' a fenced block, which displays a path rather than pointing at one',
|
|
427
455
|
'',
|
|
428
456
|
'Exempt, reported by name rather than dropped:',
|
|
@@ -1070,6 +1098,7 @@ function runList(opts: ListOptions): number {
|
|
|
1070
1098
|
...(sections.stacks ? { stacks: catalog.stacks } : {}),
|
|
1071
1099
|
...(sections.rules ? { rules: catalog.rules } : {}),
|
|
1072
1100
|
unreferenced: catalog.unreferenced,
|
|
1101
|
+
staleDetect: catalog.staleDetect,
|
|
1073
1102
|
})}\n`,
|
|
1074
1103
|
)
|
|
1075
1104
|
return 0
|
|
@@ -1097,6 +1126,51 @@ function runList(opts: ListOptions): number {
|
|
|
1097
1126
|
return 0
|
|
1098
1127
|
}
|
|
1099
1128
|
|
|
1129
|
+
interface DetectOptions {
|
|
1130
|
+
readonly stack?: string
|
|
1131
|
+
readonly json?: boolean
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async function runDetect(target: string, opts: DetectOptions): Promise<number> {
|
|
1135
|
+
const resolved = resolve(target)
|
|
1136
|
+
if (!existsSync(resolved)) {
|
|
1137
|
+
logError(`Target not found: ${target}`)
|
|
1138
|
+
return 1
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const result = await detectRules(PROJECT_ROOT, resolved, opts.stack)
|
|
1142
|
+
if (!result.ok) {
|
|
1143
|
+
logError(`Stack not found: ${result.missingStack}`)
|
|
1144
|
+
return 1
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (opts.json) {
|
|
1148
|
+
process.stdout.write(
|
|
1149
|
+
`${JSON.stringify({
|
|
1150
|
+
target: resolved,
|
|
1151
|
+
stack: opts.stack ?? null,
|
|
1152
|
+
proposals: result.proposals,
|
|
1153
|
+
})}\n`,
|
|
1154
|
+
)
|
|
1155
|
+
return 0
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
intro('canon gov detect')
|
|
1159
|
+
if (result.proposals.length === 0) {
|
|
1160
|
+
logInfo('No opt-in rule matched this target')
|
|
1161
|
+
} else {
|
|
1162
|
+
logStep('Proposed rules')
|
|
1163
|
+
for (const proposal of result.proposals) {
|
|
1164
|
+
const reasons = proposal.evidence
|
|
1165
|
+
.map((evidence) => `${evidence.match} in ${evidence.path}`)
|
|
1166
|
+
.join(', ')
|
|
1167
|
+
logInfo(`${proposal.rule}: ${reasons}`)
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
outro()
|
|
1171
|
+
return 0
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1100
1174
|
/**
|
|
1101
1175
|
* Silent on success so the consumed-copy stage that calls it stays quiet. It is
|
|
1102
1176
|
* the only work that stage does now. The installed set is readable on disk, so
|
|
@@ -1171,6 +1245,11 @@ async function runInstall(
|
|
|
1171
1245
|
): Promise<number> {
|
|
1172
1246
|
intro('canon gov install')
|
|
1173
1247
|
|
|
1248
|
+
const add = opts.add ?? ''
|
|
1249
|
+
if (add !== '' && isAddWithoutStack(stack, target)) {
|
|
1250
|
+
return installExtras(add, stack ?? target)
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1174
1253
|
const resolved = resolveTarget(target, PROJECT_ROOT)
|
|
1175
1254
|
if (typeof resolved === 'number') return resolved
|
|
1176
1255
|
|
|
@@ -1194,7 +1273,6 @@ async function runInstall(
|
|
|
1194
1273
|
return 1
|
|
1195
1274
|
}
|
|
1196
1275
|
|
|
1197
|
-
const add = opts.add ?? ''
|
|
1198
1276
|
const rules =
|
|
1199
1277
|
add === '' ? resolution.rules : mergeExtraRules(resolution.rules, add)
|
|
1200
1278
|
|
|
@@ -1247,6 +1325,67 @@ async function runInstall(
|
|
|
1247
1325
|
return 0
|
|
1248
1326
|
}
|
|
1249
1327
|
|
|
1328
|
+
/**
|
|
1329
|
+
* Whether `--add` runs with no stack under it. Commander fills the first
|
|
1330
|
+
* positional before the second, so `--add a,b ../my-app` arrives with the
|
|
1331
|
+
* target in `stack` and the default `.` in `target`. A first positional that
|
|
1332
|
+
* names a stack keeps today's meaning, so a folder named `node` reads as the
|
|
1333
|
+
* stack, and `.` or a path carrying a slash is how a caller forces the target.
|
|
1334
|
+
*/
|
|
1335
|
+
function isAddWithoutStack(stack: string | undefined, target: string): boolean {
|
|
1336
|
+
if (stack === undefined) return true
|
|
1337
|
+
if (target !== '.') return false
|
|
1338
|
+
if (stack === '.' || stack.includes('/')) return true
|
|
1339
|
+
return !govStackExists(PROJECT_ROOT, stack)
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Installs `--add` names alone. The chain stamp is left as it was, since the
|
|
1344
|
+
* extras are not a stack, and overwriting the chain would cost a target the
|
|
1345
|
+
* missing-rule report its stack install recorded.
|
|
1346
|
+
*/
|
|
1347
|
+
async function installExtras(add: string, target: string): Promise<number> {
|
|
1348
|
+
const resolved = resolveTarget(target, PROJECT_ROOT)
|
|
1349
|
+
if (typeof resolved === 'number') return resolved
|
|
1350
|
+
|
|
1351
|
+
logStep('Resolving extras with no stack')
|
|
1352
|
+
|
|
1353
|
+
const { found, missing } = lookupRules(PROJECT_ROOT, resolveExtraRules(add))
|
|
1354
|
+
for (const rule of missing) logWarn(`${rule} (source not found, skipping)`)
|
|
1355
|
+
for (const source of found) logInfo(source.rule)
|
|
1356
|
+
|
|
1357
|
+
if (found.length === 0) {
|
|
1358
|
+
logError('No named rule resolved, so nothing was installed.')
|
|
1359
|
+
outro()
|
|
1360
|
+
return 1
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const shouldInstall = await select({
|
|
1364
|
+
message: `Install ${found.length} rules to ${displayPath(target, CANON_RULES_REL)}?`,
|
|
1365
|
+
options: [
|
|
1366
|
+
{ value: true, label: 'Yes' },
|
|
1367
|
+
{ value: false, label: 'No' },
|
|
1368
|
+
],
|
|
1369
|
+
nonInteractiveDefault: true,
|
|
1370
|
+
})
|
|
1371
|
+
|
|
1372
|
+
if (!shouldInstall) {
|
|
1373
|
+
logWarn('Cancelled')
|
|
1374
|
+
outro()
|
|
1375
|
+
return 0
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
logStep('Installing rules')
|
|
1379
|
+
for (const rel of await installRules(found, resolved)) logAdd(rel)
|
|
1380
|
+
|
|
1381
|
+
await recordStamp(createGovAdapter(PROJECT_ROOT), resolved, new Date())
|
|
1382
|
+
|
|
1383
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
1384
|
+
outro()
|
|
1385
|
+
process.stderr.write(`${GREEN}✓ Rules installed${NC}\n`)
|
|
1386
|
+
return 0
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1250
1389
|
/**
|
|
1251
1390
|
* Emits the rules payload the `gov-*` skills read. The output lands in the
|
|
1252
1391
|
* target's scratch directory rather than stdout because the file is the
|
package/src/commands/init.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function register(program: Command): void {
|
|
|
40
40
|
'Examples:',
|
|
41
41
|
' canon init',
|
|
42
42
|
' canon init ../my-app',
|
|
43
|
-
' canon init --stack
|
|
43
|
+
' canon init --stack node --add 210-astro,260-shadcn ../my-app',
|
|
44
44
|
' canon init --skip governance ../my-app',
|
|
45
45
|
'',
|
|
46
46
|
].join('\n'),
|
package/src/commands/labels.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { checkTitleFormat, type TitleFormatIssue } from '@/labels/format'
|
|
|
10
10
|
import { MAP_REL } from '@/labels/map'
|
|
11
11
|
import { isReleasePullRequest, scanPhaseLabels } from '@/labels/phase'
|
|
12
12
|
import { checkSections } from '@/labels/sections'
|
|
13
|
-
import { scanTitleSpelling } from '@/labels/spelling'
|
|
14
13
|
import { BAN_SETS } from '@/markdown/bans'
|
|
15
14
|
import { bodyLines, scanBans } from '@/markdown/scan'
|
|
16
15
|
import { intro, logInfo, logStep, logWarn, outro, plural } from '@/cli/ui'
|
|
@@ -169,7 +168,7 @@ export function register(program: Command): void {
|
|
|
169
168
|
labels
|
|
170
169
|
.command('scan')
|
|
171
170
|
.description(
|
|
172
|
-
'Fail a pull request or a posted review whose title, body, or review comment carries a phase label, a board identifier, a session link
|
|
171
|
+
'Fail a pull request or a posted review whose title, body, or review comment carries a phase label, a board identifier, or a session link',
|
|
173
172
|
)
|
|
174
173
|
.helpOption('-h, --help', 'Show this help message')
|
|
175
174
|
.option(
|
|
@@ -188,7 +187,7 @@ export function register(program: Command): void {
|
|
|
188
187
|
'after',
|
|
189
188
|
[
|
|
190
189
|
'',
|
|
191
|
-
'Reads
|
|
190
|
+
'Reads the git-commit versioning reference for the two namespaces and sorts every',
|
|
192
191
|
'version-shaped token this pull request carries into the one the pull',
|
|
193
192
|
'request is allowed to hold. A release-please pull request, read off its',
|
|
194
193
|
'own fixed head branch and title, may carry semver references. Every',
|
|
@@ -212,13 +211,6 @@ export function register(program: Command): void {
|
|
|
212
211
|
'session and for no other reader, which no clone repairs, so it is read',
|
|
213
212
|
'on a release pull request too, where the board identifier is not.',
|
|
214
213
|
'',
|
|
215
|
-
"It also spell-checks the title alone against this repository's own",
|
|
216
|
-
'cspell config, since release-please copies the title into',
|
|
217
|
-
'CHANGELOG.md and nothing else spell-checks it first. The check shells',
|
|
218
|
-
"this repository's own resolved cspell binary and reports nothing when",
|
|
219
|
-
'a target project carries none, rather than reaching the network or',
|
|
220
|
-
'forcing a new dependency.',
|
|
221
|
-
'',
|
|
222
214
|
"It also grades the title alone against claude/skills/git-pr/references/pr.md's ## Title",
|
|
223
215
|
'section: the `<type>(<scope>)[!]: <subject>` structure, lowercase casing',
|
|
224
216
|
'for the type, the scope, and the first subject word, and a 72-',
|
|
@@ -234,11 +226,11 @@ export function register(program: Command): void {
|
|
|
234
226
|
'request or a review, outside a code span or a link.',
|
|
235
227
|
'',
|
|
236
228
|
'Exit codes:',
|
|
237
|
-
' 0 none of the
|
|
229
|
+
' 0 none of the six found',
|
|
238
230
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
239
231
|
' 2 the title or body carries a phase label, a board identifier, a',
|
|
240
232
|
' session link, a banned character, a heading outside the closed',
|
|
241
|
-
' list or a required one missing,
|
|
233
|
+
' list or a required one missing, or a title',
|
|
242
234
|
" breaking claude/skills/git-pr/references/pr.md's format, casing, or length",
|
|
243
235
|
'',
|
|
244
236
|
'Examples:',
|
|
@@ -532,7 +524,6 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
532
524
|
}
|
|
533
525
|
|
|
534
526
|
const result = scanPhaseLabels(resolved)
|
|
535
|
-
const spelling = await scanTitleSpelling(resolved.title, process.cwd())
|
|
536
527
|
// A scan given no title, whatever produced that absence, has nothing to
|
|
537
528
|
// grade and would otherwise fail as `structure` for the wrong reason.
|
|
538
529
|
const titleFormat =
|
|
@@ -579,34 +570,6 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
579
570
|
for (const link of result.sessionLinks) logWarn(link)
|
|
580
571
|
}
|
|
581
572
|
|
|
582
|
-
const spellingChecked = spelling.kind === 'checked'
|
|
583
|
-
const unspelledWords =
|
|
584
|
-
spelling.kind === 'checked' ? spelling.unknownWords : []
|
|
585
|
-
|
|
586
|
-
logStep(
|
|
587
|
-
spelling.kind === 'unavailable'
|
|
588
|
-
? 'Spelling unavailable'
|
|
589
|
-
: unspelledWords.length === 0
|
|
590
|
-
? 'Clean'
|
|
591
|
-
: 'Unspelled word found',
|
|
592
|
-
)
|
|
593
|
-
if (spelling.kind === 'unavailable' && spelling.reason === 'no-binary') {
|
|
594
|
-
logInfo(
|
|
595
|
-
`no node_modules/.bin/cspell resolved walking up from ${spelling.probedFrom}, so the title was not checked`,
|
|
596
|
-
)
|
|
597
|
-
} else if (spelling.kind === 'unavailable') {
|
|
598
|
-
logInfo(
|
|
599
|
-
`cspell did not exit clean or with issues found (${spelling.message}), so the title was not checked`,
|
|
600
|
-
)
|
|
601
|
-
} else if (unspelledWords.length === 0) {
|
|
602
|
-
logInfo('no word in the title is absent from every dictionary')
|
|
603
|
-
} else {
|
|
604
|
-
logWarn(
|
|
605
|
-
`${plural(unspelledWords.length, 'word')} in the title absent from every dictionary. Fix the spelling, or add jargon and project-specific terms to .cspell/project-terms.txt and dependency vocabulary to .cspell/tech-stack.txt.`,
|
|
606
|
-
)
|
|
607
|
-
for (const word of unspelledWords) logWarn(word)
|
|
608
|
-
}
|
|
609
|
-
|
|
610
573
|
const titleFormatIssues = titleFormat?.issues ?? []
|
|
611
574
|
|
|
612
575
|
// A review carries its own headings, a release-please body opens on a
|
|
@@ -707,8 +670,6 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
707
670
|
semverTags: result.semverTags,
|
|
708
671
|
boardReferences: result.boardReferences,
|
|
709
672
|
sessionLinks: result.sessionLinks,
|
|
710
|
-
unspelledWords,
|
|
711
|
-
spellingChecked,
|
|
712
673
|
titleFormatIssues,
|
|
713
674
|
sections: { checked: sectionsChecked, ...sections },
|
|
714
675
|
bannedCharacters,
|
|
@@ -719,7 +680,6 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
719
680
|
return result.phaseLabels.length === 0 &&
|
|
720
681
|
result.boardReferences.length === 0 &&
|
|
721
682
|
result.sessionLinks.length === 0 &&
|
|
722
|
-
unspelledWords.length === 0 &&
|
|
723
683
|
titleFormatIssues.length === 0 &&
|
|
724
684
|
sections.outside.length === 0 &&
|
|
725
685
|
sections.missing.length === 0 &&
|
package/src/commands/migrate.ts
CHANGED
|
@@ -1124,7 +1124,7 @@ export function register(program: Command): void {
|
|
|
1124
1124
|
'repoints every citation that names one, live or archived.',
|
|
1125
1125
|
'',
|
|
1126
1126
|
'A promoted folder is one a durable record cites and no source file',
|
|
1127
|
-
'under claude/, scripts/, src/,
|
|
1127
|
+
'under claude/, scripts/, src/, or governance/ names by',
|
|
1128
1128
|
'path. A folder a script still writes into stays in scratch, since',
|
|
1129
1129
|
'moving it needs a code change first.',
|
|
1130
1130
|
'',
|