@erclx/canon 5.42.0 → 5.44.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/auto-ship/SKILL.md +3 -3
- package/claude/skills/backlog-triage/references/apply.md +6 -6
- package/claude/skills/canon-cli/SKILL.md +4 -4
- package/claude/skills/canvas-board/REQUIREMENT.md +1 -1
- package/claude/skills/canvas-board/SKILL.md +1 -1
- package/claude/skills/ci-workflow/REQUIREMENT.md +2 -1
- package/claude/skills/ci-workflow/SKILL.md +12 -2
- package/claude/skills/ci-workflow/references/workflows.md +12 -9
- package/claude/skills/context-fold/references/context.md +1 -1
- package/claude/skills/create-rule/SKILL.md +1 -0
- package/claude/skills/create-skill/REQUIREMENT.md +1 -1
- package/claude/skills/create-skill/SKILL.md +2 -2
- package/claude/skills/create-skill/references/skill-requirement.md +68 -9
- package/claude/skills/create-skill/references/skill.md +1 -1
- package/claude/skills/decision-escalate/SKILL.md +1 -2
- package/claude/skills/design-identity/REQUIREMENT.md +40 -0
- package/claude/skills/design-identity/SKILL.md +138 -0
- package/claude/skills/{draft-identity → design-identity}/references/card-route.md +2 -2
- package/claude/skills/design-taste/REQUIREMENT.md +4 -4
- package/claude/skills/design-taste/SKILL.md +2 -2
- package/claude/skills/design-taste/references/kinds.md +1 -1
- package/claude/skills/design-taste/references/preflight.md +1 -1
- package/claude/skills/document-health/references/axes.md +2 -1
- package/claude/skills/draft-and-pick/REQUIREMENT.md +8 -69
- package/claude/skills/draft-and-pick/SKILL.md +3 -153
- package/claude/skills/draft-doc/REQUIREMENT.md +9 -30
- package/claude/skills/draft-doc/SKILL.md +4 -76
- package/claude/skills/draft-figure/REQUIREMENT.md +8 -42
- package/claude/skills/draft-figure/SKILL.md +3 -77
- package/claude/skills/draft-identity/REQUIREMENT.md +8 -23
- package/claude/skills/draft-identity/SKILL.md +3 -130
- package/claude/skills/draft-ready/REQUIREMENT.md +8 -37
- package/claude/skills/draft-ready/SKILL.md +3 -128
- package/claude/skills/draft-screencast/REQUIREMENT.md +8 -23
- package/claude/skills/draft-screencast/SKILL.md +3 -62
- package/claude/skills/draft-slides/REQUIREMENT.md +8 -27
- package/claude/skills/draft-slides/SKILL.md +3 -72
- package/claude/skills/draw-figure/REQUIREMENT.md +59 -0
- package/claude/skills/draw-figure/SKILL.md +85 -0
- package/claude/skills/{draft-figure → draw-figure}/references/architecture-set.md +2 -2
- package/claude/skills/{draft-figure → draw-figure}/references/hand-drawn.md +2 -2
- package/claude/skills/{draft-figure → draw-figure}/references/mermaid-path.md +2 -2
- package/claude/skills/{draft-figure → draw-figure}/references/mermaid.md +1 -1
- package/claude/skills/git-commit/references/commit.md +1 -5
- package/claude/skills/git-pr/references/pr.md +1 -0
- package/claude/skills/make-deck/REQUIREMENT.md +44 -0
- package/claude/skills/make-deck/SKILL.md +80 -0
- package/claude/skills/markdown-craft/REQUIREMENT.md +1 -1
- package/claude/skills/markdown-craft/SKILL.md +2 -2
- package/claude/skills/pick-by-eye/REQUIREMENT.md +86 -0
- package/claude/skills/pick-by-eye/SKILL.md +161 -0
- package/claude/skills/plan-feature/SKILL.md +3 -2
- package/claude/skills/plan-feature/references/plan.md +25 -11
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/plan-intake/SKILL.md +1 -1
- package/claude/skills/plan-ready/REQUIREMENT.md +54 -0
- package/claude/skills/plan-ready/SKILL.md +136 -0
- package/claude/skills/{draft-ready → plan-ready}/references/assembly.md +1 -1
- package/claude/skills/{draft-ready → plan-ready}/references/ready.md +1 -1
- package/claude/skills/pr-evidence/references/evidence.md +1 -1
- package/claude/skills/record-screencast/REQUIREMENT.md +3 -3
- package/claude/skills/record-screencast/SKILL.md +2 -2
- package/claude/skills/role-orchestrator/REQUIREMENT.md +1 -1
- package/claude/skills/role-orchestrator/SKILL.md +2 -0
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +5 -2
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -0
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +18 -13
- package/claude/skills/role-planner/REQUIREMENT.md +1 -1
- package/claude/skills/role-planner/SKILL.md +5 -4
- package/claude/skills/role-worker/REQUIREMENT.md +2 -0
- package/claude/skills/role-worker/SKILL.md +35 -2
- package/claude/skills/script-screencast/REQUIREMENT.md +40 -0
- package/claude/skills/script-screencast/SKILL.md +70 -0
- package/claude/skills/{draft-screencast → script-screencast}/references/beats-template.md +2 -2
- package/claude/skills/search-craft/REQUIREMENT.md +4 -4
- package/claude/skills/search-craft/SKILL.md +2 -2
- package/claude/skills/target-setup/SKILL.md +1 -0
- package/claude/skills/task-board/SKILL.md +11 -9
- package/claude/skills/task-board/references/board.md +15 -12
- package/claude/skills/teach-workspace/SKILL.md +1 -1
- package/claude/skills/teach-workspace/references/lesson-craft.md +2 -2
- package/claude/skills/ui-checklist/SKILL.md +2 -0
- package/claude/skills/write-doc/REQUIREMENT.md +46 -0
- package/claude/skills/write-doc/SKILL.md +83 -0
- package/claude/skills/{draft-doc → write-doc}/references/context.md +2 -2
- package/claude/skills/{draft-doc → write-doc}/references/docs.md +2 -2
- package/claude/skills/{draft-doc → write-doc}/references/readme.md +2 -2
- package/claude/skills/{draft-doc → write-doc}/references/wiki-kind.md +4 -4
- package/claude/skills/{draft-doc → write-doc}/references/wiki.md +1 -1
- package/docs/agents/audits.md +1 -0
- package/docs/agents/markdown-audit.md +4 -2
- package/docs/agents/migrate.md +13 -10
- package/docs/target/migrations.md +28 -4
- package/docs/target/projects.md +1 -1
- package/docs/workflow/parallel-features.md +3 -2
- package/docs/workflow/skill-map-on-request.md +18 -12
- package/docs/workflow/skill-map.md +13 -12
- package/docs/workflow/visual-design-workflow.md +2 -2
- package/governance/detect.toml +4 -0
- package/governance/ledger/framework/astro.md +73 -0
- package/governance/ledger/framework/fastapi.md +76 -0
- package/governance/ledger/framework/nextjs.md +86 -0
- package/governance/ledger/framework/react.md +83 -0
- package/governance/ledger/framework/shadcn.md +60 -0
- package/governance/ledger/framework/tailwind.md +55 -0
- package/governance/rules/canon/context.md +2 -1
- package/governance/rules/canon/plan.md +3 -5
- package/governance/rules/canon/scratch.md +3 -1
- package/governance/rules/canon/task-scope.md +2 -1
- package/governance/rules/canon/tasks.md +2 -1
- package/governance/rules/canon/worktrees.md +5 -2
- package/governance/rules/claude/output.md +7 -4
- package/governance/rules/claude/skill.md +6 -4
- package/governance/rules/framework/astro.md +12 -14
- package/governance/rules/framework/fastapi.md +20 -24
- package/governance/rules/framework/nextjs.md +19 -27
- package/governance/rules/framework/react.md +22 -33
- package/governance/rules/framework/shadcn.md +11 -15
- package/governance/rules/framework/tailwind.md +12 -14
- package/governance/rules/tooling/ci-workflow.md +6 -39
- package/governance/rules/ui/design-reference.md +37 -0
- package/governance/rules/writing/markdown.md +3 -2
- package/package.json +6 -6
- package/src/audits/catalog.ts +42 -0
- package/src/claude/cases/authoring.ts +15 -3
- package/src/claude/cases/misc.ts +12 -0
- package/src/claude/cases/workflow.ts +19 -1
- package/src/claude/settings.ts +8 -4
- package/src/claude/skills-audit.ts +113 -1
- package/src/commands/claude.ts +45 -12
- package/src/commands/demo.ts +1 -1
- package/src/commands/gov.ts +198 -0
- package/src/commands/markdown.ts +1 -1
- package/src/commands/migrate.ts +125 -2
- package/src/commands/pr.ts +34 -83
- package/src/commands/tasks.ts +383 -2
- package/src/commands/tooling.ts +3 -3
- package/src/commands/worktrees.ts +49 -0
- package/src/context/audit.ts +1 -1
- package/src/demo/beats.ts +1 -1
- package/src/gate/stages.ts +15 -0
- package/src/gov/audit.ts +441 -0
- package/src/listeners/belongs.ts +21 -0
- package/src/listeners/machine.ts +93 -0
- package/src/listeners/parse.ts +41 -0
- package/src/markdown/scope.ts +45 -35
- package/src/migrate/record-layout.ts +1 -1
- package/src/migrate/session-defaults.ts +138 -0
- package/src/migrate/skill-names.ts +68 -21
- package/src/pr/frames.ts +14 -3
- package/src/pr/local.ts +4 -69
- package/src/records/validate.ts +31 -0
- package/src/sync/check.ts +15 -0
- package/src/tasks/board.ts +854 -0
- package/src/tasks/record.ts +20 -1
- package/src/tasks/validate.ts +206 -9
- package/src/tooling/stamp.ts +56 -12
- package/src/worktrees/stop.ts +92 -0
- package/tooling/astro/configs/card-src/pages/og-card.astro +1 -1
- package/tooling/astro/reference.md +2 -2
- package/tooling/base/configs/.github/workflows/phase-label-gate.yml +6 -2
- package/tooling/base/configs/commitlint.config.js +0 -20
- package/tooling/base/reference.md +1 -1
- package/tooling/base/seeds/.github/workflows/verify.yml +4 -1
- package/tooling/claude/reference.md +13 -7
- package/tooling/claude/seeds/.claude/hooks/board-validate.sh +65 -0
- package/tooling/claude/seeds/.claude/settings.json +8 -0
- package/tooling/claude/user/settings.template.json +0 -4
- package/tooling/cloudflare/configs/.github/workflows/deploy.yml +5 -5
- package/tooling/public/configs/.github/dependabot.yml +12 -0
- package/tooling/public/configs/.github/workflows/release-please.yml +30 -0
- package/tooling/public/manifest.toml +8 -0
- package/tooling/public/reference.md +53 -0
- package/tooling/public/seeds/.github/ISSUE_TEMPLATE/bug.yml +48 -0
- package/tooling/public/seeds/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/tooling/public/seeds/.github/ISSUE_TEMPLATE/feature.yml +16 -0
- package/tooling/public/seeds/.release-please-manifest.json +3 -0
- package/tooling/public/seeds/CONTRIBUTING.md +50 -0
- package/tooling/public/seeds/LICENSE +21 -0
- package/tooling/public/seeds/SECURITY.md +13 -0
- package/tooling/public/seeds/release-please-config.json +12 -0
- package/tooling/shell/configs/.github/workflows/shell.yml +4 -1
- package/tooling/web/configs/.github/workflows/readme-screenshot.yml +6 -3
- package/tooling/web/configs/.github/workflows/verify.yml +15 -12
- /package/claude/skills/{draft-figure → draw-figure}/references/figures.md +0 -0
- /package/claude/skills/{draft-slides → make-deck}/references/deck.md +0 -0
- /package/claude/skills/{draft-and-pick → pick-by-eye}/references/live-arms.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/agent-facing.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/application.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/cli.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/library.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/plugin.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/requirements.md +0 -0
|
@@ -22,12 +22,18 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
prompt: 'Draw me a diagram of how the pieces of this system connect.',
|
|
25
|
+
expect: 'draw-figure',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
prompt:
|
|
29
|
+
'I typed draft-figure and it said the figure skill was renamed to draw-figure.', // canon-keep-retired
|
|
30
|
+
// The pointer answers under its old name on purpose. canon-keep-retired
|
|
25
31
|
expect: 'draft-figure',
|
|
26
32
|
},
|
|
27
33
|
{
|
|
28
34
|
prompt:
|
|
29
35
|
'This onboarding doc needs a hand-drawn picture of the signup-to-active path, sketch it in and make sure it actually renders correctly.',
|
|
30
|
-
expect: '
|
|
36
|
+
expect: 'draw-figure',
|
|
31
37
|
},
|
|
32
38
|
{
|
|
33
39
|
prompt:
|
|
@@ -108,10 +114,22 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
108
114
|
},
|
|
109
115
|
{
|
|
110
116
|
prompt: 'Draft me a script with beats for a screencast recording.',
|
|
117
|
+
expect: 'script-screencast',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
prompt:
|
|
121
|
+
'I typed draft-screencast and it said the screencast skill was renamed to script-screencast.', // canon-keep-retired
|
|
122
|
+
// The pointer answers under its old name on purpose. canon-keep-retired
|
|
111
123
|
expect: 'draft-screencast',
|
|
112
124
|
},
|
|
113
125
|
{
|
|
114
126
|
prompt: 'Turn this topic into a slide deck I can render.',
|
|
127
|
+
expect: 'make-deck',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
prompt:
|
|
131
|
+
'I typed draft-slides and it said the slides skill was renamed to make-deck.', // canon-keep-retired
|
|
132
|
+
// The pointer answers under its old name on purpose. canon-keep-retired
|
|
115
133
|
expect: 'draft-slides',
|
|
116
134
|
},
|
|
117
135
|
// Rot across documents nobody changed is what this reads, rather than a
|
package/src/claude/settings.ts
CHANGED
|
@@ -5,6 +5,10 @@ import { dirname } from 'node:path'
|
|
|
5
5
|
const DEFAULT_INDENT = ' '
|
|
6
6
|
const INDENT = /^([ \t]+)\S/m
|
|
7
7
|
|
|
8
|
+
// The pair setup once wrote to suppress Claude's attribution. Only this exact
|
|
9
|
+
// value is retracted, since anything else is a person's own choice.
|
|
10
|
+
const RETIRED_ATTRIBUTION = { commit: '', pr: '' }
|
|
11
|
+
|
|
8
12
|
export type SettingsRecord = Record<string, unknown>
|
|
9
13
|
|
|
10
14
|
export interface SettingsKey {
|
|
@@ -107,10 +111,10 @@ export function planSettings(
|
|
|
107
111
|
}
|
|
108
112
|
keys.push({ label: 'statusLine', changed: statusLineChanged })
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
if (equals(next.attribution, RETIRED_ATTRIBUTION)) {
|
|
115
|
+
delete next.attribution
|
|
116
|
+
keys.push({ label: 'attribution', changed: true })
|
|
117
|
+
}
|
|
114
118
|
|
|
115
119
|
const permissions = asRecord(next.permissions)
|
|
116
120
|
const merged: SettingsRecord = { ...permissions }
|
|
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
|
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
3
|
import { isFamilyKey } from '@/claude/skills-families'
|
|
4
4
|
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
5
|
-
import { bodyLines, maskDisplayed } from '@/markdown/scan'
|
|
5
|
+
import { type BodyLine, bodyLines, maskDisplayed } from '@/markdown/scan'
|
|
6
6
|
|
|
7
7
|
/** Returned when a skill folder carries no `REQUIREMENT.md`, the gating check. */
|
|
8
8
|
export const EXIT_MISSING_REQUIREMENT = 2
|
|
@@ -13,6 +13,19 @@ export const DESCRIPTION_LIMIT = 1024
|
|
|
13
13
|
/** The two headings the requirement template declares, matched at any level. */
|
|
14
14
|
export const REQUIREMENT_SECTIONS: readonly string[] = ['Gap', 'Must']
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The caps `claude/skills/create-skill/references/skill-requirement.md` states.
|
|
18
|
+
* Words count body tokens carrying a letter or digit, so a heading or bullet
|
|
19
|
+
* marker costs nothing and frontmatter is left out.
|
|
20
|
+
*/
|
|
21
|
+
export const REQUIREMENT_LIMITS = { bullets: 5, words: 400 } as const
|
|
22
|
+
|
|
23
|
+
/** The section the standard retired into Must not, matched at any level. */
|
|
24
|
+
export const RETIRED_REQUIREMENT_SECTION = 'Guards'
|
|
25
|
+
|
|
26
|
+
/** The section that states today's failures, where history is reported. */
|
|
27
|
+
const GAP_SECTION = 'Gap'
|
|
28
|
+
|
|
16
29
|
/** The one tree that ships to a target, and the only one the practice list names. */
|
|
17
30
|
export const SHIPPED_CORPUS = join('claude', 'skills')
|
|
18
31
|
|
|
@@ -100,6 +113,13 @@ const SECTION = /^##\s+(.+?)\s*$/
|
|
|
100
113
|
*/
|
|
101
114
|
const ISO_DATE = /\b\d{4}-\d{2}-\d{2}\b/g
|
|
102
115
|
|
|
116
|
+
/** An issue or pull request number, which a heading's `#` never matches. */
|
|
117
|
+
const ISSUE_NUMBER = /(?<![\w&/#])#\d+\b/g
|
|
118
|
+
|
|
119
|
+
const TOP_LEVEL_BULLET = /^(?:[-*+]|\d+[.)])\s/
|
|
120
|
+
|
|
121
|
+
const WORD = /\S*[\p{L}\p{N}]\S*/gu
|
|
122
|
+
|
|
103
123
|
export interface SkillFinding {
|
|
104
124
|
readonly rel: string
|
|
105
125
|
readonly detail: string
|
|
@@ -126,6 +146,7 @@ export interface SkillsAudit {
|
|
|
126
146
|
readonly nameMismatch: readonly SkillFinding[]
|
|
127
147
|
readonly longDescription: readonly SkillFinding[]
|
|
128
148
|
readonly requirementSections: readonly SkillFinding[]
|
|
149
|
+
readonly requirementLimits: readonly SkillFinding[]
|
|
129
150
|
readonly datedProvenance: readonly SkillFinding[]
|
|
130
151
|
readonly practiceShape: readonly SkillFinding[]
|
|
131
152
|
readonly referenceContents: readonly SkillFinding[]
|
|
@@ -142,6 +163,7 @@ interface SkillSource {
|
|
|
142
163
|
readonly family: string | undefined
|
|
143
164
|
/** Undefined when the folder carries no `REQUIREMENT.md` at all. */
|
|
144
165
|
readonly requirementHeadings: readonly string[] | undefined
|
|
166
|
+
readonly requirement: string | undefined
|
|
145
167
|
/** Every H2 in `SKILL.md` outside a fence, for the practice shape. */
|
|
146
168
|
readonly sections: readonly string[]
|
|
147
169
|
readonly hasLedger: boolean
|
|
@@ -198,6 +220,7 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
|
|
|
198
220
|
nameMismatch: sources.flatMap(nameFindings),
|
|
199
221
|
longDescription: sources.flatMap(lengthFindings),
|
|
200
222
|
requirementSections: sources.flatMap(sectionFindings),
|
|
223
|
+
requirementLimits: sources.flatMap(limitFindings),
|
|
201
224
|
datedProvenance: sources.flatMap((source) =>
|
|
202
225
|
source.bodies.flatMap(dateFindings),
|
|
203
226
|
),
|
|
@@ -269,6 +292,7 @@ async function readSkill(
|
|
|
269
292
|
family: declaredFamily(fields?.fields.metadata),
|
|
270
293
|
requirementHeadings:
|
|
271
294
|
requirement === undefined ? undefined : headings(requirement),
|
|
295
|
+
requirement,
|
|
272
296
|
sections: sectionsOf(body),
|
|
273
297
|
hasLedger: existsSync(join(skillDir, PRACTICE_LEDGER)),
|
|
274
298
|
bodies: [
|
|
@@ -349,6 +373,94 @@ function sectionFindings(source: SkillSource): SkillFinding[] {
|
|
|
349
373
|
]
|
|
350
374
|
}
|
|
351
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Reports every cap the standard states, each as its own finding so a file
|
|
378
|
+
* breaching two reads as two edits. Silent on an absent requirement for the
|
|
379
|
+
* reason `sectionFindings` is.
|
|
380
|
+
*/
|
|
381
|
+
function limitFindings(source: SkillSource): SkillFinding[] {
|
|
382
|
+
const { requirement, requirementHeadings } = source
|
|
383
|
+
if (requirement === undefined || requirementHeadings === undefined) return []
|
|
384
|
+
|
|
385
|
+
const rel = join(source.rel, 'REQUIREMENT.md')
|
|
386
|
+
const lines = bodyLines(requirement)
|
|
387
|
+
const details = [
|
|
388
|
+
...bulletDetails(lines),
|
|
389
|
+
...wordDetails(lines),
|
|
390
|
+
...gapHistoryDetails(lines),
|
|
391
|
+
...(requirementHeadings.includes(RETIRED_REQUIREMENT_SECTION)
|
|
392
|
+
? [`## ${RETIRED_REQUIREMENT_SECTION}: retired, fold it into Must not`]
|
|
393
|
+
: []),
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
return details.map((detail) => ({ rel, detail }))
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Counts top-level bullets per H2, since the template holds one list per
|
|
401
|
+
* section. A nested bullet elaborates its parent rather than adding to it.
|
|
402
|
+
*/
|
|
403
|
+
function bulletDetails(lines: readonly BodyLine[]): string[] {
|
|
404
|
+
const counts = new Map<string, number>()
|
|
405
|
+
let section: string | undefined
|
|
406
|
+
|
|
407
|
+
for (const line of lines) {
|
|
408
|
+
if (line.fenced) continue
|
|
409
|
+
const heading = SECTION.exec(line.text)?.[1]
|
|
410
|
+
if (heading !== undefined) {
|
|
411
|
+
section = heading
|
|
412
|
+
continue
|
|
413
|
+
}
|
|
414
|
+
if (section === undefined || !TOP_LEVEL_BULLET.test(line.text)) continue
|
|
415
|
+
counts.set(section, (counts.get(section) ?? 0) + 1)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return [...counts]
|
|
419
|
+
.filter(([, count]) => count > REQUIREMENT_LIMITS.bullets)
|
|
420
|
+
.map(
|
|
421
|
+
([name, count]) =>
|
|
422
|
+
`## ${name}: ${count} bullets, cap ${REQUIREMENT_LIMITS.bullets}`,
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function wordDetails(lines: readonly BodyLine[]): string[] {
|
|
427
|
+
const words = lines.reduce(
|
|
428
|
+
(total, line) => total + (line.text.match(WORD)?.length ?? 0),
|
|
429
|
+
0,
|
|
430
|
+
)
|
|
431
|
+
if (words <= REQUIREMENT_LIMITS.words) return []
|
|
432
|
+
return [`${words} words, cap ${REQUIREMENT_LIMITS.words}`]
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Gap states what fails today, so a date or a number naming the incident that
|
|
437
|
+
* earned a line is history git already holds. A code span is quoted data, such
|
|
438
|
+
* as a sample filename, and is masked before matching.
|
|
439
|
+
*/
|
|
440
|
+
function gapHistoryDetails(lines: readonly BodyLine[]): string[] {
|
|
441
|
+
const details: string[] = []
|
|
442
|
+
let inGap = false
|
|
443
|
+
|
|
444
|
+
for (const line of lines) {
|
|
445
|
+
if (line.fenced) continue
|
|
446
|
+
const heading = HEADING.exec(line.text)?.[1]
|
|
447
|
+
if (heading !== undefined) {
|
|
448
|
+
inGap = heading === GAP_SECTION
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
451
|
+
if (!inGap) continue
|
|
452
|
+
const masked = maskDisplayed(line.text)
|
|
453
|
+
for (const match of [
|
|
454
|
+
...masked.matchAll(ISO_DATE),
|
|
455
|
+
...masked.matchAll(ISSUE_NUMBER),
|
|
456
|
+
]) {
|
|
457
|
+
details.push(`line ${line.number}: ${match[0]} in ${GAP_SECTION}`)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return details
|
|
462
|
+
}
|
|
463
|
+
|
|
352
464
|
/**
|
|
353
465
|
* A listed folder the shipped corpus does not hold, such as a renamed skill or
|
|
354
466
|
* a misspelled append, would otherwise read as a pass. Silent where the
|
package/src/commands/claude.ts
CHANGED
|
@@ -32,7 +32,9 @@ import {
|
|
|
32
32
|
PRACTICE_LEDGER,
|
|
33
33
|
PRACTICE_SECTIONS,
|
|
34
34
|
PRACTICE_SKILLS,
|
|
35
|
+
REQUIREMENT_LIMITS,
|
|
35
36
|
REQUIREMENT_SECTIONS,
|
|
37
|
+
RETIRED_REQUIREMENT_SECTION,
|
|
36
38
|
SHIPPED_CORPUS,
|
|
37
39
|
type SkillFinding,
|
|
38
40
|
type SkillsAudit,
|
|
@@ -69,6 +71,7 @@ import {
|
|
|
69
71
|
serializeSettings,
|
|
70
72
|
writeSettings,
|
|
71
73
|
} from '@/claude/settings'
|
|
74
|
+
import { readSessionDefaults } from '@/migrate/session-defaults'
|
|
72
75
|
import { copyPreservingMode } from '@/sync/copy'
|
|
73
76
|
import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
|
|
74
77
|
import { recordDir } from '@/roots/record'
|
|
@@ -255,7 +258,7 @@ export function register(program: Command): void {
|
|
|
255
258
|
|
|
256
259
|
claude
|
|
257
260
|
.command('setup')
|
|
258
|
-
.description('One-shot user-level config (statusline,
|
|
261
|
+
.description('One-shot user-level config (statusline, permissions)')
|
|
259
262
|
.argument('[dest]', 'User config directory', join(homedir(), '.claude'))
|
|
260
263
|
.helpOption('-h, --help', 'Show this help message')
|
|
261
264
|
.action(async (dest: string) => {
|
|
@@ -440,7 +443,7 @@ export function register(program: Command): void {
|
|
|
440
443
|
'Refuses when neither claude/skills/ nor .claude/skills/ resolves.',
|
|
441
444
|
'',
|
|
442
445
|
'Only a missing REQUIREMENT.md sets a failing exit code. Name, description,',
|
|
443
|
-
'folder, and requirement
|
|
446
|
+
'folder, and requirement shape and limit findings are advisory.',
|
|
444
447
|
'',
|
|
445
448
|
'Examples:',
|
|
446
449
|
' canon claude skills audit',
|
|
@@ -663,6 +666,15 @@ async function runSync(target: string): Promise<number> {
|
|
|
663
666
|
else logWarn(`${name}/ missing. Run \`canon claude init\``)
|
|
664
667
|
}
|
|
665
668
|
|
|
669
|
+
// Sync never rewrites an existing settings file, so a target seeded before
|
|
670
|
+
// the session defaults learns of them only here.
|
|
671
|
+
const defaults = await readSessionDefaults(resolved, PROJECT_ROOT)
|
|
672
|
+
if (defaults.kind === 'planned' && defaults.plan.predates) {
|
|
673
|
+
logWarn(
|
|
674
|
+
'settings.json lacks the session defaults. Run `canon migrate session-defaults --write`',
|
|
675
|
+
)
|
|
676
|
+
}
|
|
677
|
+
|
|
666
678
|
logStep('Scanning .gitignore')
|
|
667
679
|
const chain = claudeChain(PROJECT_ROOT)
|
|
668
680
|
const pruned = await pruneGitignore(chain, resolved)
|
|
@@ -1493,6 +1505,7 @@ async function runSkillsAudit(
|
|
|
1493
1505
|
readme: report.readme,
|
|
1494
1506
|
folderName: report.folderName,
|
|
1495
1507
|
requirementSections: report.requirementSections,
|
|
1508
|
+
requirementLimits: report.requirementLimits,
|
|
1496
1509
|
datedProvenance: report.datedProvenance,
|
|
1497
1510
|
practiceShape: report.practiceShape,
|
|
1498
1511
|
referenceContents: report.referenceContents,
|
|
@@ -1501,6 +1514,7 @@ async function runSkillsAudit(
|
|
|
1501
1514
|
checkpoints: {
|
|
1502
1515
|
descriptionLimit: DESCRIPTION_LIMIT,
|
|
1503
1516
|
requirementSections: REQUIREMENT_SECTIONS,
|
|
1517
|
+
requirementLimits: REQUIREMENT_LIMITS,
|
|
1504
1518
|
corpora: CORPORA,
|
|
1505
1519
|
practiceSkills: PRACTICE_SKILLS,
|
|
1506
1520
|
ledgerSkills: LEDGER_SKILLS,
|
|
@@ -1661,21 +1675,40 @@ function reportRequirementShape(report: SkillsAudit): void {
|
|
|
1661
1675
|
'A folder carrying no requirement is reported above rather than counted twice here.',
|
|
1662
1676
|
)
|
|
1663
1677
|
|
|
1664
|
-
|
|
1665
|
-
|
|
1678
|
+
logInfo(
|
|
1679
|
+
`Each list holds at most ${REQUIREMENT_LIMITS.bullets} top-level bullets, the body at most ${REQUIREMENT_LIMITS.words} words, and Gap no date or issue number outside a code span.`,
|
|
1680
|
+
)
|
|
1681
|
+
logInfo(
|
|
1682
|
+
`A ## ${RETIRED_REQUIREMENT_SECTION} section is retired into Must not. These limits report and never fail the run.`,
|
|
1683
|
+
)
|
|
1684
|
+
|
|
1685
|
+
if (
|
|
1686
|
+
report.requirementSections.length === 0 &&
|
|
1687
|
+
report.requirementLimits.length === 0
|
|
1688
|
+
) {
|
|
1689
|
+
logInfo('Every requirement declares both and holds to the limits.')
|
|
1666
1690
|
return
|
|
1667
1691
|
}
|
|
1668
1692
|
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1693
|
+
if (report.requirementSections.length > 0) {
|
|
1694
|
+
logWarn(
|
|
1695
|
+
`${plural(report.requirementSections.length, 'requirement')} short a declared section`,
|
|
1696
|
+
)
|
|
1697
|
+
reportFindings(report.requirementSections)
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
if (report.requirementLimits.length > 0) {
|
|
1701
|
+
logWarn(
|
|
1702
|
+
`${plural(report.requirementLimits.length, 'finding')} past a requirement limit`,
|
|
1703
|
+
)
|
|
1704
|
+
reportFindings(report.requirementLimits)
|
|
1705
|
+
}
|
|
1673
1706
|
}
|
|
1674
1707
|
|
|
1675
1708
|
/**
|
|
1676
|
-
* Names where the date belongs, since
|
|
1677
|
-
*
|
|
1678
|
-
*
|
|
1709
|
+
* Names where the date belongs, since a reader deleting it should know git
|
|
1710
|
+
* still holds it, and reports without failing the run for the same reason the
|
|
1711
|
+
* frontmatter measures do.
|
|
1679
1712
|
*/
|
|
1680
1713
|
function reportProvenance(report: SkillsAudit): void {
|
|
1681
1714
|
logStep('Provenance')
|
|
@@ -1683,7 +1716,7 @@ function reportProvenance(report: SkillsAudit): void {
|
|
|
1683
1716
|
'A SKILL.md and its references state the current rule, with no ISO date outside a fence or code span.',
|
|
1684
1717
|
)
|
|
1685
1718
|
logInfo(
|
|
1686
|
-
'The incident and date that earned a rule
|
|
1719
|
+
'The incident and date that earned a rule stay in git, where the log of the file finds them.',
|
|
1687
1720
|
)
|
|
1688
1721
|
|
|
1689
1722
|
if (report.datedProvenance.length === 0) {
|
package/src/commands/demo.ts
CHANGED
|
@@ -62,7 +62,7 @@ export function register(program: Command): void {
|
|
|
62
62
|
demo
|
|
63
63
|
.command('compile')
|
|
64
64
|
.description('Turn a screencast draft into a plan a run can drive')
|
|
65
|
-
.argument('<draft>', 'Screencast draft written by
|
|
65
|
+
.argument('<draft>', 'Screencast draft written by script-screencast')
|
|
66
66
|
.helpOption('-h, --help', 'Show this help message')
|
|
67
67
|
.option('-o, --out <dir>', 'Directory the plan is written to', DEFAULT_OUT)
|
|
68
68
|
.option('-s, --slug <slug>', 'Plan name, defaulting to the draft filename')
|
package/src/commands/gov.ts
CHANGED
|
@@ -32,6 +32,16 @@ import {
|
|
|
32
32
|
SEED_REL,
|
|
33
33
|
SHIPPED_SKILLS_REL,
|
|
34
34
|
} from '@/gov/restated'
|
|
35
|
+
import {
|
|
36
|
+
auditExit,
|
|
37
|
+
hasFinding,
|
|
38
|
+
PRACTICE_FOLDERS,
|
|
39
|
+
readRuleAudit,
|
|
40
|
+
RULE_GATED_FOLDERS,
|
|
41
|
+
RULE_LIMITS,
|
|
42
|
+
type RuleAuditReport,
|
|
43
|
+
type RuleReading,
|
|
44
|
+
} from '@/gov/audit'
|
|
35
45
|
import {
|
|
36
46
|
CITATION_MARKER,
|
|
37
47
|
type CitationReport,
|
|
@@ -123,6 +133,12 @@ interface CountsOptions {
|
|
|
123
133
|
readonly json?: boolean
|
|
124
134
|
}
|
|
125
135
|
|
|
136
|
+
interface AuditOptions {
|
|
137
|
+
readonly root?: string
|
|
138
|
+
readonly json?: boolean
|
|
139
|
+
readonly gate?: boolean
|
|
140
|
+
}
|
|
141
|
+
|
|
126
142
|
/** What a reader does about each way the sweep produced no reading. */
|
|
127
143
|
const RESTATED_REFUSALS: Record<RestatedRefusal, string> = {
|
|
128
144
|
'no-instructions': `No ${INSTRUCTIONS_REL} here, or it carries no bullet, so there is no instruction corpus to sweep.`,
|
|
@@ -573,6 +589,188 @@ export function register(program: Command): void {
|
|
|
573
589
|
.action((opts: RestatedOptions) => {
|
|
574
590
|
process.exitCode = runRestated(opts)
|
|
575
591
|
})
|
|
592
|
+
|
|
593
|
+
gov
|
|
594
|
+
.command('audit')
|
|
595
|
+
.description(
|
|
596
|
+
'Report every rule past a shape limit, and in this repository every outside-practice rule missing a source ledger',
|
|
597
|
+
)
|
|
598
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
599
|
+
.argument(
|
|
600
|
+
'[paths...]',
|
|
601
|
+
'Rule files or folders of rules to read, defaulting to the corpus',
|
|
602
|
+
)
|
|
603
|
+
.option('--root <path>', 'Tree to read, defaulting to the cwd')
|
|
604
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
605
|
+
.option(
|
|
606
|
+
'--gate',
|
|
607
|
+
'Set 2 only for a finding in a gated folder, which the push stage reads',
|
|
608
|
+
)
|
|
609
|
+
.addHelpText(
|
|
610
|
+
'after',
|
|
611
|
+
[
|
|
612
|
+
'',
|
|
613
|
+
'Limits, each a finding one past its number:',
|
|
614
|
+
` words ${RULE_LIMITS.words} words in the body`,
|
|
615
|
+
` bullets ${RULE_LIMITS.bullets} bullets in the file`,
|
|
616
|
+
` h2 ${RULE_LIMITS.h2} H2 headings`,
|
|
617
|
+
` perH2 ${RULE_LIMITS.perH2} bullets under one H2, the text above the first H2 a group`,
|
|
618
|
+
` chars ${RULE_LIMITS.chars} characters in one bullet, continuation lines folded in`,
|
|
619
|
+
` sentences ${RULE_LIMITS.sentences} sentences in one bullet`,
|
|
620
|
+
' emphasis any CRITICAL, IMPORTANT, MUST, ALWAYS, NEVER, or "you must"',
|
|
621
|
+
' outside a code span',
|
|
622
|
+
'',
|
|
623
|
+
'Corpus:',
|
|
624
|
+
' A tree holding governance/rules/ reads it and internal/rules/, and',
|
|
625
|
+
` asks a ledger of each rule under ${PRACTICE_FOLDERS.join(', ')}`,
|
|
626
|
+
' at governance/ledger/<folder>/<slug>.md, one table row per bullet',
|
|
627
|
+
' under a heading of the same name.',
|
|
628
|
+
' Any other tree reads .claude/rules/project/ alone and asks no ledger.',
|
|
629
|
+
' .claude/rules/canon/ is fixed at the toolkit source and never read',
|
|
630
|
+
' unless named. A named path resolves against --root and must sit',
|
|
631
|
+
' inside it.',
|
|
632
|
+
'',
|
|
633
|
+
`Gated folders: ${RULE_GATED_FOLDERS.length === 0 ? 'none yet' : RULE_GATED_FOLDERS.join(', ')}`,
|
|
634
|
+
'',
|
|
635
|
+
'Exit codes:',
|
|
636
|
+
' 0 no finding, or under --gate no finding in a gated folder',
|
|
637
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
638
|
+
' 2 at least one finding, or under --gate one in a gated folder',
|
|
639
|
+
'',
|
|
640
|
+
'Reasons:',
|
|
641
|
+
' no-rules the tree holds no rule corpus to read, or a named folder holds no rule',
|
|
642
|
+
' no-path a named path does not exist',
|
|
643
|
+
' outside-root a named path sits outside --root',
|
|
644
|
+
'',
|
|
645
|
+
'Examples:',
|
|
646
|
+
' canon gov audit',
|
|
647
|
+
' canon gov audit .claude/rules/project/api.md',
|
|
648
|
+
' canon gov audit --json',
|
|
649
|
+
'',
|
|
650
|
+
].join('\n'),
|
|
651
|
+
)
|
|
652
|
+
.action(async (paths: string[], opts: AuditOptions) => {
|
|
653
|
+
process.exitCode = await runAudit(paths, opts)
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Reports by default and gates only under `--gate`, and there only for the
|
|
659
|
+
* folders `RULE_GATED_FOLDERS` lists. The corpus starts far from zero, so the
|
|
660
|
+
* push stage fails on a folder once its rewrite has brought it there.
|
|
661
|
+
*/
|
|
662
|
+
async function runAudit(
|
|
663
|
+
paths: readonly string[],
|
|
664
|
+
opts: AuditOptions,
|
|
665
|
+
): Promise<number> {
|
|
666
|
+
const root = resolve(opts.root ?? process.cwd())
|
|
667
|
+
const report = await readRuleAudit(root, paths)
|
|
668
|
+
const emitJson = opts.json ?? false
|
|
669
|
+
|
|
670
|
+
if (report.kind === 'unreadable') {
|
|
671
|
+
intro('canon gov audit')
|
|
672
|
+
logStep('Refused')
|
|
673
|
+
logError(report.message)
|
|
674
|
+
outro()
|
|
675
|
+
|
|
676
|
+
if (emitJson) {
|
|
677
|
+
process.stdout.write(
|
|
678
|
+
`${JSON.stringify({ ok: false, reason: report.reason, message: report.message })}\n`,
|
|
679
|
+
)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return 1
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
reportAudit(report, root, paths.length > 0)
|
|
686
|
+
|
|
687
|
+
if (emitJson) {
|
|
688
|
+
process.stdout.write(
|
|
689
|
+
`${JSON.stringify({
|
|
690
|
+
ok: true,
|
|
691
|
+
root,
|
|
692
|
+
limits: RULE_LIMITS,
|
|
693
|
+
gated: RULE_GATED_FOLDERS,
|
|
694
|
+
...report,
|
|
695
|
+
})}\n`,
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return auditExit(report, {
|
|
700
|
+
gate: opts.gate ?? false,
|
|
701
|
+
gated: RULE_GATED_FOLDERS,
|
|
702
|
+
})
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function describeLedger(reading: RuleReading): string[] {
|
|
706
|
+
const { ledger } = reading
|
|
707
|
+
if (ledger.status === 'missing') {
|
|
708
|
+
return [`${reading.file}: no ledger at ${ledger.path}`]
|
|
709
|
+
}
|
|
710
|
+
if (ledger.status !== 'present') return []
|
|
711
|
+
return ledger.mismatches.map(
|
|
712
|
+
(mismatch) =>
|
|
713
|
+
`${ledger.path}: ${mismatch.heading ?? '(above the first H2)'} has ${mismatch.rows} row(s) for ${mismatch.bullets} bullet(s)`,
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function reportAudit(
|
|
718
|
+
report: Extract<RuleAuditReport, { kind: 'measured' }>,
|
|
719
|
+
root: string,
|
|
720
|
+
named: boolean,
|
|
721
|
+
): void {
|
|
722
|
+
intro('canon gov audit')
|
|
723
|
+
|
|
724
|
+
const shaped = report.rules.filter((reading) => reading.findings.length > 0)
|
|
725
|
+
const ledgers = report.rules.flatMap(describeLedger)
|
|
726
|
+
|
|
727
|
+
logStep('Corpus')
|
|
728
|
+
logInfo(
|
|
729
|
+
named
|
|
730
|
+
? `${report.rules.length} named rule(s) in ${root}`
|
|
731
|
+
: `${report.rules.length} rule(s) under ${report.corpus.join(' and ')} in ${root}`,
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
logStep('Shape')
|
|
735
|
+
if (shaped.length === 0) {
|
|
736
|
+
logInfo('every rule is within the six limits and carries no emphasis word')
|
|
737
|
+
} else {
|
|
738
|
+
for (const reading of shaped) {
|
|
739
|
+
for (const finding of reading.findings) {
|
|
740
|
+
const at =
|
|
741
|
+
finding.line === undefined
|
|
742
|
+
? reading.file
|
|
743
|
+
: `${reading.file}:${finding.line}`
|
|
744
|
+
logWarn(
|
|
745
|
+
finding.limit === 'emphasis'
|
|
746
|
+
? `${at}: emphasis word`
|
|
747
|
+
: `${at}: ${finding.limit} ${finding.count} past ${finding.max}`,
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
logStep('Ledgers')
|
|
754
|
+
if (report.scope === 'target') {
|
|
755
|
+
logInfo('not read, since ledgers live in the toolkit and never ship')
|
|
756
|
+
} else if (ledgers.length === 0) {
|
|
757
|
+
logInfo('every outside-practice rule has a ledger row per bullet')
|
|
758
|
+
} else {
|
|
759
|
+
for (const line of ledgers) logWarn(line)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const failing = report.rules.filter(hasFinding).length
|
|
763
|
+
logStep('Summary')
|
|
764
|
+
logInfo(
|
|
765
|
+
`${failing} of ${report.rules.length} rule(s) carry a finding: ${shaped.length} with a shape finding, ${ledgers.length} ledger finding(s)`,
|
|
766
|
+
)
|
|
767
|
+
logInfo(
|
|
768
|
+
RULE_GATED_FOLDERS.length === 0
|
|
769
|
+
? 'no folder is gated yet, so --gate passes every finding'
|
|
770
|
+
: `gated: ${RULE_GATED_FOLDERS.join(', ')}`,
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
outro()
|
|
576
774
|
}
|
|
577
775
|
|
|
578
776
|
/**
|
package/src/commands/markdown.ts
CHANGED
|
@@ -486,7 +486,7 @@ function reportDensity(reports: readonly FileReport[], gates: boolean): void {
|
|
|
486
486
|
`Paragraphs past ${CHECKPOINTS.paragraph} characters, more than 2 paragraphs in a row, bullets past ${CHECKPOINTS.bullet} characters, more than 5 plain bullets in a row, a heading past H3, and an H3 opening with neither a paragraph nor a table.`,
|
|
487
487
|
)
|
|
488
488
|
logInfo(
|
|
489
|
-
'Measured on context entries, docs, skill bodies and
|
|
489
|
+
'Measured on context entries, docs, skill bodies, references, and requirements, tooling seeds and stack references, the wiki, README.md, and CONTRIBUTING.md. Every other path takes no density finding.',
|
|
490
490
|
)
|
|
491
491
|
logInfo(
|
|
492
492
|
gates
|