@erclx/canon 5.45.0 → 5.47.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/api-design/REQUIREMENT.md +15 -31
- package/claude/skills/auto-ship/REQUIREMENT.md +14 -25
- package/claude/skills/backlog-triage/REQUIREMENT.md +12 -23
- package/claude/skills/build-in-slices/REQUIREMENT.md +15 -25
- package/claude/skills/canon-cli/REQUIREMENT.md +15 -27
- package/claude/skills/canon-feedback/REQUIREMENT.md +10 -18
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +10 -15
- package/claude/skills/canvas-board/REQUIREMENT.md +14 -28
- package/claude/skills/ci-workflow/REQUIREMENT.md +12 -21
- package/claude/skills/code-craft/REQUIREMENT.md +14 -28
- package/claude/skills/codebase-layout/REQUIREMENT.md +14 -31
- package/claude/skills/context-fold/REQUIREMENT.md +15 -39
- package/claude/skills/context-fold/references/context.md +3 -3
- package/claude/skills/create-rule/REQUIREMENT.md +10 -24
- package/claude/skills/create-rule/SKILL.md +3 -0
- package/claude/skills/create-skill/REQUIREMENT.md +10 -21
- package/claude/skills/decision-escalate/REQUIREMENT.md +12 -24
- package/claude/skills/deploy-app/REQUIREMENT.md +12 -22
- package/claude/skills/deprecation-migration/REQUIREMENT.md +14 -42
- package/claude/skills/design-identity/REQUIREMENT.md +14 -20
- package/claude/skills/design-taste/REQUIREMENT.md +16 -57
- package/claude/skills/docs-sync/REQUIREMENT.md +14 -23
- package/claude/skills/document-health/REQUIREMENT.md +15 -29
- package/claude/skills/document-health/SKILL.md +2 -2
- package/claude/skills/document-repair/REQUIREMENT.md +32 -0
- package/claude/skills/document-repair/SKILL.md +76 -0
- package/claude/skills/document-repair/references/batches.md +44 -0
- package/claude/skills/draft-and-pick/REQUIREMENT.md +4 -3
- package/claude/skills/draft-doc/REQUIREMENT.md +4 -3
- package/claude/skills/draft-figure/REQUIREMENT.md +4 -3
- package/claude/skills/draft-identity/REQUIREMENT.md +4 -3
- package/claude/skills/draft-ready/REQUIREMENT.md +4 -3
- package/claude/skills/draft-screencast/REQUIREMENT.md +4 -3
- package/claude/skills/draft-slides/REQUIREMENT.md +4 -3
- package/claude/skills/draw-figure/REQUIREMENT.md +15 -39
- package/claude/skills/git-branch/REQUIREMENT.md +12 -26
- package/claude/skills/git-commit/REQUIREMENT.md +11 -15
- package/claude/skills/git-followup/REQUIREMENT.md +11 -22
- package/claude/skills/git-followup/SKILL.md +1 -0
- package/claude/skills/git-issue/REQUIREMENT.md +9 -15
- package/claude/skills/git-pr/REQUIREMENT.md +13 -32
- package/claude/skills/git-pr/SKILL.md +11 -2
- package/claude/skills/git-pr/references/closing-issues.md +47 -0
- package/claude/skills/git-pr/references/pr.md +62 -7
- package/claude/skills/git-ship/REQUIREMENT.md +13 -26
- package/claude/skills/git-split/REQUIREMENT.md +10 -17
- package/claude/skills/git-stage/REQUIREMENT.md +12 -18
- package/claude/skills/git-worktree/REQUIREMENT.md +15 -19
- package/claude/skills/make-deck/REQUIREMENT.md +13 -21
- package/claude/skills/markdown-craft/REQUIREMENT.md +9 -3
- package/claude/skills/plan-feature/REQUIREMENT.md +15 -24
- package/claude/skills/plan-groundwork/REQUIREMENT.md +12 -28
- package/claude/skills/plan-groundwork/SKILL.md +5 -2
- package/claude/skills/plan-groundwork/references/output.md +47 -1
- package/claude/skills/plan-intake/REQUIREMENT.md +11 -26
- package/claude/skills/plan-intake/references/intake.md +2 -2
- package/claude/skills/plan-intake-answer/REQUIREMENT.md +14 -29
- package/claude/skills/plan-intake-answer/SKILL.md +7 -1
- package/claude/skills/plan-ready/REQUIREMENT.md +17 -36
- package/claude/skills/pr-evidence/REQUIREMENT.md +11 -17
- package/claude/skills/read-frames/REQUIREMENT.md +3 -8
- package/claude/skills/record-screencast/REQUIREMENT.md +14 -17
- package/claude/skills/review-address/REQUIREMENT.md +14 -38
- package/claude/skills/review-address/SKILL.md +18 -6
- package/claude/skills/review-address/references/post-review-findings.md +4 -1
- package/claude/skills/review-address/references/rebase-only.md +14 -2
- package/claude/skills/review-craft/REQUIREMENT.md +14 -32
- package/claude/skills/review-pr/REQUIREMENT.md +15 -34
- package/claude/skills/review-pr/SKILL.md +9 -2
- package/claude/skills/review-pr/references/body-variants.md +29 -8
- package/claude/skills/review-pr/references/post.md +29 -10
- package/claude/skills/review-ui/REQUIREMENT.md +14 -39
- package/claude/skills/review-ui/SKILL.md +1 -0
- package/claude/skills/review-ui/references/post.md +2 -0
- package/claude/skills/review-ui/references/tick.md +3 -0
- package/claude/skills/role-orchestrator/REQUIREMENT.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +5 -0
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +9 -0
- package/claude/skills/role-reviewer/REQUIREMENT.md +15 -65
- package/claude/skills/role-reviewer/SKILL.md +8 -1
- package/claude/skills/role-worker/REQUIREMENT.md +2 -2
- package/claude/skills/role-worker/SKILL.md +3 -0
- package/claude/skills/script-screencast/REQUIREMENT.md +10 -16
- package/claude/skills/search-craft/REQUIREMENT.md +12 -36
- package/claude/skills/search-craft/SKILL.md +1 -1
- package/claude/skills/search-craft/references/adopted.md +2 -2
- package/claude/skills/session-compact/REQUIREMENT.md +13 -23
- package/claude/skills/session-map/REQUIREMENT.md +13 -29
- package/claude/skills/session-relay/REQUIREMENT.md +14 -58
- package/claude/skills/session-relay/SKILL.md +6 -0
- package/claude/skills/session-resume/REQUIREMENT.md +13 -27
- package/claude/skills/session-worktree/REQUIREMENT.md +14 -43
- package/claude/skills/systematic-debugging/REQUIREMENT.md +13 -34
- package/claude/skills/target-check/REQUIREMENT.md +16 -23
- package/claude/skills/target-setup/REQUIREMENT.md +11 -34
- package/claude/skills/task-board/REQUIREMENT.md +13 -22
- package/claude/skills/task-board/SKILL.md +7 -0
- package/claude/skills/task-board/references/decline.md +2 -2
- package/claude/skills/task-board/references/tasks.md +1 -1
- package/claude/skills/task-complete/REQUIREMENT.md +11 -24
- package/claude/skills/task-complete/references/archive.md +4 -2
- package/claude/skills/teach-workspace/REQUIREMENT.md +14 -45
- package/claude/skills/test-craft/REQUIREMENT.md +15 -46
- package/claude/skills/test-first/REQUIREMENT.md +14 -20
- package/claude/skills/ui-checklist/REQUIREMENT.md +14 -29
- package/claude/skills/ui-checklist/SKILL.md +3 -3
- package/claude/skills/ux-audit/REQUIREMENT.md +3 -7
- package/claude/skills/ux-measure/REQUIREMENT.md +14 -28
- package/claude/skills/video-craft/REQUIREMENT.md +14 -27
- package/claude/skills/write-doc/REQUIREMENT.md +14 -24
- package/claude/skills/write-human/REQUIREMENT.md +15 -27
- package/claude/skills/youtube-transcripts/REQUIREMENT.md +9 -15
- package/docs/agents/context-audit.md +19 -12
- package/docs/agents/markdown-audit.md +15 -10
- package/docs/target/projects.md +2 -0
- package/docs/workflow/skill-map.md +6 -5
- package/docs/workflow/zshrc-aliases.md +3 -0
- package/governance/renames.toml +0 -2
- package/governance/rules/claude/output.md +5 -1
- package/governance/rules/code/routing.md +5 -1
- package/governance/stacks/base.toml +0 -1
- package/package.json +1 -1
- package/scripts/core/bootstrap.sh +1 -0
- package/src/claude/cases/workflow.ts +12 -0
- package/src/commands/context.ts +57 -3
- package/src/commands/labels.ts +149 -4
- package/src/commands/markdown.ts +188 -21
- package/src/commands/pr.ts +113 -4
- package/src/commands/sessions.ts +58 -11
- package/src/commands/tasks.ts +182 -0
- package/src/commands/worktrees.ts +2 -0
- package/src/context/coverage.ts +210 -0
- package/src/context/gate.ts +12 -3
- package/src/gate/stages.ts +1 -1
- package/src/init/steps.ts +45 -0
- package/src/intake/items.ts +14 -0
- package/src/labels/closing.ts +46 -0
- package/src/labels/density.ts +363 -0
- package/src/markdown/gated.ts +250 -0
- package/src/markdown/scope.ts +115 -2
- package/src/migrate/rename.ts +6 -0
- package/src/pr/evidence.ts +1 -1
- package/src/pr/rest.ts +2 -0
- package/src/pr/tick.ts +89 -0
- package/src/sessions/claim.ts +7 -0
- package/src/sessions/live.ts +79 -6
- package/src/sessions/registry.ts +11 -0
- package/src/sessions/resolve.ts +85 -7
- package/src/tasks/archive.ts +15 -4
- package/src/tasks/closes.ts +136 -0
- package/src/tasks/record.ts +1 -1
- package/src/worktrees/reclaim.ts +16 -1
- package/tooling/base/configs/scripts/verify.ts +1 -1
- package/tooling/base/seeds/.github/pull_request_template.md +1 -1
- package/tooling/claude/seeds/.claude/hooks/board-validate.sh +1 -1
- package/tooling/claude/seeds/CLAUDE.md +2 -2
- package/governance/rules/canon/task-scope.md +0 -12
package/src/commands/labels.ts
CHANGED
|
@@ -5,6 +5,13 @@ import { execa } from 'execa'
|
|
|
5
5
|
import { gitEnv } from '@/git/env'
|
|
6
6
|
import { diffLabels } from '@/labels/apply'
|
|
7
7
|
import { type LabelAuditRefusal, auditLabels } from '@/labels/audit'
|
|
8
|
+
import { checkClosing } from '@/labels/closing'
|
|
9
|
+
import {
|
|
10
|
+
bodyDensity,
|
|
11
|
+
isPost,
|
|
12
|
+
POST_HEADINGS,
|
|
13
|
+
postDensity,
|
|
14
|
+
} from '@/labels/density'
|
|
8
15
|
import { resolveScanInput } from '@/labels/event'
|
|
9
16
|
import { checkTitleFormat, type TitleFormatIssue } from '@/labels/format'
|
|
10
17
|
import { MAP_REL } from '@/labels/map'
|
|
@@ -34,6 +41,7 @@ interface ScanOptions {
|
|
|
34
41
|
readonly body?: string
|
|
35
42
|
readonly bodyFile?: string
|
|
36
43
|
readonly head?: string
|
|
44
|
+
readonly closes?: string
|
|
37
45
|
readonly json?: boolean
|
|
38
46
|
}
|
|
39
47
|
|
|
@@ -196,6 +204,10 @@ export function register(program: Command): void {
|
|
|
196
204
|
'Body to scan, read from a file, overriding the event payload',
|
|
197
205
|
)
|
|
198
206
|
.option('--head <ref>', 'Head branch, overriding the event payload')
|
|
207
|
+
.option(
|
|
208
|
+
'--closes <numbers>',
|
|
209
|
+
'Issues the body must close, a comma list or `none`, graded when given',
|
|
210
|
+
)
|
|
199
211
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
200
212
|
.addHelpText(
|
|
201
213
|
'after',
|
|
@@ -235,16 +247,44 @@ export function register(program: Command): void {
|
|
|
235
247
|
'It grades the body of an ordinary pull request against the closed',
|
|
236
248
|
'section list: a level-two heading outside it, or a missing',
|
|
237
249
|
'## Summary or ## Key changes, fails. A review, a release-please body,',
|
|
238
|
-
'and a scan carrying no title skip this check.
|
|
250
|
+
'and a scan carrying no title skip this check.',
|
|
251
|
+
'',
|
|
252
|
+
'It reports the density of that same body per section under `density`,',
|
|
253
|
+
'without failing on it: a bullet past 200 characters, more than 5 plain',
|
|
254
|
+
'bullets or 2 paragraphs in a row, a paragraph past 350 characters, any',
|
|
255
|
+
'H3 or deeper heading, and Technical context mixing bullets with',
|
|
256
|
+
'paragraphs. A bold label alone on a line splits a Key changes run, a',
|
|
257
|
+
'bold-led paragraph starts a new Technical context run, and Testing',
|
|
258
|
+
'takes the heading limit alone. Each finding carries its section, kind,',
|
|
259
|
+
'line in the whole body, value, and limit.',
|
|
260
|
+
'',
|
|
261
|
+
'A body the section check skips is graded as a post under the same',
|
|
262
|
+
'`density` key when its first level-two heading is one of',
|
|
263
|
+
`${POST_HEADINGS.map((heading) => `## ${heading}`).join(', ')}.`,
|
|
264
|
+
'A post takes the same length and run limits, allows an H3, and',
|
|
265
|
+
'measures a nested item as a bullet of its own. The footer, the',
|
|
266
|
+
'marker, and a table never report, a block inside <details> takes the',
|
|
267
|
+
'length limits alone, and a checklist box under ## What to look at',
|
|
268
|
+
'takes no length limit. Post density never fails the scan.',
|
|
269
|
+
'',
|
|
270
|
+
'It also fails a banned',
|
|
239
271
|
'character, an em dash or a semicolon, in the title or body of a pull',
|
|
240
272
|
'request or a review, outside a code span or a link.',
|
|
241
273
|
'',
|
|
274
|
+
'With --closes, it also grades the closing lines. Each issue named needs',
|
|
275
|
+
'its own `Closes #NNN` keyword in the body, and a closing keyword for an',
|
|
276
|
+
'issue the list leaves out fails. `none` expects no closing line.',
|
|
277
|
+
'Without the flag the check does not run, so the pull request job and a',
|
|
278
|
+
'followup keep their behavior. A keyword in a code span or a fence is',
|
|
279
|
+
'not read, as GitHub does not read it.',
|
|
280
|
+
'',
|
|
242
281
|
'Exit codes:',
|
|
243
|
-
' 0 none of the
|
|
282
|
+
' 0 none of the eight found',
|
|
244
283
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
245
284
|
' 2 the title or body carries a phase label, a board identifier, a',
|
|
246
285
|
' session link, a banned character, a heading outside the closed',
|
|
247
|
-
' list or a required one missing, or
|
|
286
|
+
' list or a required one missing, a missing or stray closing line',
|
|
287
|
+
' under --closes, or a title',
|
|
248
288
|
" breaking claude/skills/git-pr/references/pr.md's format, casing, or length",
|
|
249
289
|
'',
|
|
250
290
|
'Examples:',
|
|
@@ -536,11 +576,42 @@ async function runApply(number: string, opts: ApplyOptions): Promise<number> {
|
|
|
536
576
|
return owed.length === 0 ? 0 : 2
|
|
537
577
|
}
|
|
538
578
|
|
|
579
|
+
/**
|
|
580
|
+
* Reads `--closes` as the issues the body must close. Undefined when the flag
|
|
581
|
+
* is absent, which skips the check, and a message when the value is neither
|
|
582
|
+
* `none` nor a comma list of numbers, since a guessed list would grade the
|
|
583
|
+
* body against the wrong issues.
|
|
584
|
+
*/
|
|
585
|
+
function parseCloses(value: string | undefined): number[] | string | undefined {
|
|
586
|
+
if (value === undefined) return undefined
|
|
587
|
+
if (value === 'none') return []
|
|
588
|
+
if (!/^\d+(,\d+)*$/.test(value)) {
|
|
589
|
+
return `--closes takes a comma list of issue numbers or none, not ${value}`
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return [...new Set(value.split(',').map(Number))]
|
|
593
|
+
}
|
|
594
|
+
|
|
539
595
|
async function runScan(opts: ScanOptions): Promise<number> {
|
|
540
596
|
const emitJson = opts.json ?? false
|
|
541
597
|
|
|
542
598
|
intro('canon labels scan')
|
|
543
599
|
|
|
600
|
+
const expected = parseCloses(opts.closes)
|
|
601
|
+
|
|
602
|
+
if (typeof expected === 'string') {
|
|
603
|
+
logStep('Refused')
|
|
604
|
+
logWarn(expected)
|
|
605
|
+
outro()
|
|
606
|
+
|
|
607
|
+
if (emitJson) {
|
|
608
|
+
process.stdout.write(
|
|
609
|
+
`${JSON.stringify({ reason: 'bad-input', message: expected })}\n`,
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
return 1
|
|
613
|
+
}
|
|
614
|
+
|
|
544
615
|
const resolved = resolveScanInput(opts)
|
|
545
616
|
|
|
546
617
|
if (resolved.kind === 'refused') {
|
|
@@ -647,6 +718,44 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
647
718
|
}
|
|
648
719
|
}
|
|
649
720
|
|
|
721
|
+
// Reported and never gated yet. New bodies come back clean under the
|
|
722
|
+
// shape in pr.md first, so an older open pull request fails no edit event.
|
|
723
|
+
// A post is never gated, since CI reads one only after it is published.
|
|
724
|
+
const post = !sectionsChecked && isPost(resolved.body)
|
|
725
|
+
const density = sectionsChecked
|
|
726
|
+
? bodyDensity(resolved.body)
|
|
727
|
+
: post
|
|
728
|
+
? postDensity(resolved.body)
|
|
729
|
+
: []
|
|
730
|
+
|
|
731
|
+
logStep(
|
|
732
|
+
!sectionsChecked && !post
|
|
733
|
+
? 'Density not checked'
|
|
734
|
+
: density.length === 0
|
|
735
|
+
? 'Clean'
|
|
736
|
+
: 'Density finding found',
|
|
737
|
+
)
|
|
738
|
+
if (!sectionsChecked && !post) {
|
|
739
|
+
logInfo(
|
|
740
|
+
'only an ordinary pull request body or a post opening on a post heading is graded for density',
|
|
741
|
+
)
|
|
742
|
+
} else if (density.length === 0) {
|
|
743
|
+
logInfo('every section is within its length, run, and heading limits')
|
|
744
|
+
} else {
|
|
745
|
+
logWarn(
|
|
746
|
+
`${plural(density.length, 'density finding')} in the ${post ? 'post' : 'body'}. Regroup the section under bold labels or split the long item rather than cutting a path.`,
|
|
747
|
+
)
|
|
748
|
+
for (const finding of density) {
|
|
749
|
+
const measured =
|
|
750
|
+
finding.value === null
|
|
751
|
+
? ''
|
|
752
|
+
: ` (${finding.value} over ${finding.limit})`
|
|
753
|
+
logWarn(
|
|
754
|
+
`line ${finding.line} in ${finding.section ?? 'the opening'}: ${finding.kind}${measured}`,
|
|
755
|
+
)
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
650
759
|
const banFindings = [
|
|
651
760
|
...scanBans(
|
|
652
761
|
[{ number: 1, text: resolved.title, fenced: false }],
|
|
@@ -693,6 +802,37 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
693
802
|
for (const issue of titleFormatIssues) logWarn(TITLE_FORMAT_MESSAGES[issue])
|
|
694
803
|
}
|
|
695
804
|
|
|
805
|
+
const closing =
|
|
806
|
+
expected === undefined
|
|
807
|
+
? { missing: [], stray: [] }
|
|
808
|
+
: checkClosing(resolved.body, expected)
|
|
809
|
+
|
|
810
|
+
logStep(
|
|
811
|
+
expected === undefined
|
|
812
|
+
? 'Closing lines not checked'
|
|
813
|
+
: closing.missing.length === 0 && closing.stray.length === 0
|
|
814
|
+
? 'Clean'
|
|
815
|
+
: 'Closing line issue found',
|
|
816
|
+
)
|
|
817
|
+
if (expected === undefined) {
|
|
818
|
+
logInfo('no --closes list, so the closing lines are not graded')
|
|
819
|
+
} else if (closing.missing.length === 0 && closing.stray.length === 0) {
|
|
820
|
+
logInfo('the closing lines match the issues the task closes')
|
|
821
|
+
} else {
|
|
822
|
+
if (closing.missing.length > 0) {
|
|
823
|
+
logWarn(
|
|
824
|
+
`${plural(closing.missing.length, 'issue')} with no closing line. Add one \`Closes #NNN\` per issue.`,
|
|
825
|
+
)
|
|
826
|
+
for (const number of closing.missing) logWarn(`#${number}`)
|
|
827
|
+
}
|
|
828
|
+
if (closing.stray.length > 0) {
|
|
829
|
+
logWarn(
|
|
830
|
+
`${plural(closing.stray.length, 'closing line')} for an issue this pull request does not close. Delete each.`,
|
|
831
|
+
)
|
|
832
|
+
for (const number of closing.stray) logWarn(`#${number}`)
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
696
836
|
outro()
|
|
697
837
|
|
|
698
838
|
if (emitJson) {
|
|
@@ -705,7 +845,10 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
705
845
|
sessionLinks: result.sessionLinks,
|
|
706
846
|
titleFormatIssues,
|
|
707
847
|
sections: { checked: sectionsChecked, ...sections },
|
|
848
|
+
density,
|
|
708
849
|
bannedCharacters,
|
|
850
|
+
missingClosures: closing.missing,
|
|
851
|
+
strayClosures: closing.stray,
|
|
709
852
|
})}\n`,
|
|
710
853
|
)
|
|
711
854
|
}
|
|
@@ -716,7 +859,9 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
716
859
|
titleFormatIssues.length === 0 &&
|
|
717
860
|
sections.outside.length === 0 &&
|
|
718
861
|
sections.missing.length === 0 &&
|
|
719
|
-
banFindings.length === 0
|
|
862
|
+
banFindings.length === 0 &&
|
|
863
|
+
closing.missing.length === 0 &&
|
|
864
|
+
closing.stray.length === 0
|
|
720
865
|
? 0
|
|
721
866
|
: 2
|
|
722
867
|
}
|
package/src/commands/markdown.ts
CHANGED
|
@@ -29,7 +29,16 @@ import {
|
|
|
29
29
|
bodyLines,
|
|
30
30
|
scanBans,
|
|
31
31
|
} from '@/markdown/scan'
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
appendGatedPrefix,
|
|
34
|
+
findingsUnder,
|
|
35
|
+
GATED_CONFIG_REL,
|
|
36
|
+
type GatedList,
|
|
37
|
+
prefixShapeError,
|
|
38
|
+
readGatedConfig,
|
|
39
|
+
resolveGatedList,
|
|
40
|
+
} from '@/markdown/gated'
|
|
41
|
+
import { densityGates, inDensityScope } from '@/markdown/scope'
|
|
33
42
|
import {
|
|
34
43
|
BASELINE,
|
|
35
44
|
CHECKPOINTS,
|
|
@@ -49,6 +58,9 @@ import {
|
|
|
49
58
|
} from '@/cli/ui'
|
|
50
59
|
import { isOwnCheckout } from '@/roots/project'
|
|
51
60
|
|
|
61
|
+
/** A config the audit cannot read refuses rather than gating nothing. */
|
|
62
|
+
type AuditRefusal = MarkdownAuditRefusal | 'malformed-config'
|
|
63
|
+
|
|
52
64
|
const EXIT_REFUSED = 1
|
|
53
65
|
const EXIT_GATE = 2
|
|
54
66
|
|
|
@@ -119,9 +131,12 @@ export function register(program: Command): void {
|
|
|
119
131
|
'a row, a 200-character bullet, 5 plain bullets in a row, no H4, and a',
|
|
120
132
|
'paragraph or table opening every H3. Only context, docs, skills, tooling',
|
|
121
133
|
'seeds and references, the wiki, README.md, and CONTRIBUTING.md take a',
|
|
122
|
-
'finding.
|
|
123
|
-
'and a bullet run gates in context, docs, and
|
|
124
|
-
'
|
|
134
|
+
'finding. In a canon checkout one gates under a prefix in GATED_PATHS in',
|
|
135
|
+
'src/markdown/scope.ts, and a bullet run gates in context, docs, and',
|
|
136
|
+
'skills from the start. Elsewhere one gates only under a prefix',
|
|
137
|
+
'canon/config/markdown.toml lists, which canon markdown gated add writes,',
|
|
138
|
+
'and with no such file every density finding reports and none gates.',
|
|
139
|
+
'A config that does not parse refuses with its path named.',
|
|
125
140
|
'',
|
|
126
141
|
'A page whose frontmatter carries kind: reference takes the reference',
|
|
127
142
|
'shape: no H2 beyond Output, Refusals, and See also, each at most once',
|
|
@@ -165,6 +180,131 @@ export function register(program: Command): void {
|
|
|
165
180
|
.action(async (paths: string[], opts: AuditCommandOptions) => {
|
|
166
181
|
process.exitCode = await runAudit(paths, opts)
|
|
167
182
|
})
|
|
183
|
+
|
|
184
|
+
const gated = markdown
|
|
185
|
+
.command('gated')
|
|
186
|
+
.description("Read and extend a project's gated density prefixes")
|
|
187
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
188
|
+
|
|
189
|
+
gated
|
|
190
|
+
.command('add')
|
|
191
|
+
.description(
|
|
192
|
+
'Gate a folder or file once it carries no density or reference shape finding',
|
|
193
|
+
)
|
|
194
|
+
.argument(
|
|
195
|
+
'<prefix>',
|
|
196
|
+
'A folder ending in a slash, or the path of one existing file',
|
|
197
|
+
)
|
|
198
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
199
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
200
|
+
.addHelpText(
|
|
201
|
+
'after',
|
|
202
|
+
[
|
|
203
|
+
'',
|
|
204
|
+
'Exit codes:',
|
|
205
|
+
' 0 the prefix is listed, appended now or already present',
|
|
206
|
+
' 1 refused, with the reason on stderr, and the config left untouched',
|
|
207
|
+
'',
|
|
208
|
+
'Appends the prefix to canon/config/markdown.toml only when every file',
|
|
209
|
+
'under it carries zero findings that would gate once it is listed, so a',
|
|
210
|
+
'gated folder never fails a push the moment it is added. A folder holding',
|
|
211
|
+
'no markdown file passes. A prefix without a trailing slash would also',
|
|
212
|
+
'match a sibling such as docs-old/, so one must name an existing file.',
|
|
213
|
+
"A skill's REQUIREMENT.md gates only on its own path, never a folder.",
|
|
214
|
+
'',
|
|
215
|
+
"Canon's own checkout lists its prefixes in GATED_PATHS in",
|
|
216
|
+
'src/markdown/scope.ts, so the verb refuses there.',
|
|
217
|
+
'',
|
|
218
|
+
'Examples:',
|
|
219
|
+
' canon markdown gated add docs/',
|
|
220
|
+
' canon markdown gated add .claude/skills/my-skill/REQUIREMENT.md --json',
|
|
221
|
+
'',
|
|
222
|
+
].join('\n'),
|
|
223
|
+
)
|
|
224
|
+
.action(async (prefix: string, opts: AuditCommandOptions) => {
|
|
225
|
+
process.exitCode = await runGatedAdd(prefix, opts)
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
type GatedAddRefusal =
|
|
230
|
+
| 'canon-checkout'
|
|
231
|
+
| 'malformed-config'
|
|
232
|
+
| 'bad-prefix'
|
|
233
|
+
| 'no-git'
|
|
234
|
+
| 'findings'
|
|
235
|
+
|
|
236
|
+
async function runGatedAdd(
|
|
237
|
+
prefix: string,
|
|
238
|
+
opts: AuditCommandOptions,
|
|
239
|
+
): Promise<number> {
|
|
240
|
+
const root = process.cwd()
|
|
241
|
+
const json = opts.json ?? false
|
|
242
|
+
const refuseAdd = (
|
|
243
|
+
reason: GatedAddRefusal,
|
|
244
|
+
message: string,
|
|
245
|
+
extra: Record<string, unknown> = {},
|
|
246
|
+
): number => {
|
|
247
|
+
intro('canon markdown gated add')
|
|
248
|
+
logStep('Refused')
|
|
249
|
+
logWarn(message)
|
|
250
|
+
outro()
|
|
251
|
+
if (json) {
|
|
252
|
+
process.stdout.write(
|
|
253
|
+
`${JSON.stringify({ root, prefix, reason, message, ...extra })}\n`,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
return EXIT_REFUSED
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (isOwnCheckout(root)) {
|
|
260
|
+
return refuseAdd(
|
|
261
|
+
'canon-checkout',
|
|
262
|
+
'This is a canon checkout, which gates through GATED_PATHS in src/markdown/scope.ts. Add the prefix there.',
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const config = readGatedConfig(root)
|
|
267
|
+
if (config.kind === 'malformed') {
|
|
268
|
+
return refuseAdd('malformed-config', config.message)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const shape = prefixShapeError(root, prefix)
|
|
272
|
+
if (shape !== undefined) return refuseAdd('bad-prefix', shape)
|
|
273
|
+
|
|
274
|
+
const findings = await findingsUnder(root, prefix)
|
|
275
|
+
if (!findings) {
|
|
276
|
+
return refuseAdd(
|
|
277
|
+
'no-git',
|
|
278
|
+
'git could not list the tree, so nothing under the prefix was measured. Run inside a git repository.',
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
if (findings.length > 0) {
|
|
282
|
+
return refuseAdd(
|
|
283
|
+
'findings',
|
|
284
|
+
`${plural(findings.length, 'finding')} under ${prefix} would gate once it is listed. Run 'canon markdown audit ${prefix}' and bring it to zero first.`,
|
|
285
|
+
{ findings },
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const current = config.kind === 'listed' ? config.prefixes : []
|
|
290
|
+
const action = appendGatedPrefix(root, current, prefix)
|
|
291
|
+
|
|
292
|
+
intro('canon markdown gated add')
|
|
293
|
+
logStep(action === 'appended' ? 'Appended' : 'Already listed')
|
|
294
|
+
logInfo(
|
|
295
|
+
action === 'appended'
|
|
296
|
+
? `${prefix} now gates in ${GATED_CONFIG_REL}.`
|
|
297
|
+
: `${prefix} was already listed in ${GATED_CONFIG_REL}, so nothing was written.`,
|
|
298
|
+
)
|
|
299
|
+
outro()
|
|
300
|
+
|
|
301
|
+
if (json) {
|
|
302
|
+
process.stdout.write(
|
|
303
|
+
`${JSON.stringify({ root, prefix, action, config: GATED_CONFIG_REL })}\n`,
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return 0
|
|
168
308
|
}
|
|
169
309
|
|
|
170
310
|
async function runAudit(
|
|
@@ -194,12 +334,20 @@ async function runAudit(
|
|
|
194
334
|
)
|
|
195
335
|
}
|
|
196
336
|
|
|
337
|
+
const resolution = resolveGatedList(root)
|
|
338
|
+
if (resolution.kind === 'malformed') {
|
|
339
|
+
return refuse(
|
|
340
|
+
'malformed-config',
|
|
341
|
+
resolution.message,
|
|
342
|
+
root,
|
|
343
|
+
opts.json ?? false,
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
const gated = resolution.list
|
|
347
|
+
|
|
197
348
|
const bans = BAN_SETS
|
|
198
349
|
const empty = emptyBanSets(bans)
|
|
199
350
|
const checkpoints = CHECKPOINTS
|
|
200
|
-
// `GATED_PATHS` describes this repository's rewrite progress, so a target
|
|
201
|
-
// reads every density finding as a report until its own corpus is gated.
|
|
202
|
-
const gates = isOwnCheckout(root)
|
|
203
351
|
|
|
204
352
|
const reports: FileReport[] = await Promise.all(
|
|
205
353
|
scope.files.map(async (rel) => {
|
|
@@ -216,12 +364,17 @@ async function runAudit(
|
|
|
216
364
|
density: inDensityScope(rel)
|
|
217
365
|
? densityFindings(lines).map((finding) => ({
|
|
218
366
|
...finding,
|
|
219
|
-
gating:
|
|
367
|
+
gating: densityGates(
|
|
368
|
+
rel,
|
|
369
|
+
finding.kind,
|
|
370
|
+
gated.prefixes,
|
|
371
|
+
gated.alwaysGated,
|
|
372
|
+
),
|
|
220
373
|
}))
|
|
221
374
|
: [],
|
|
222
375
|
referenceShape: referenceShapeFindings(source).map((finding) => ({
|
|
223
376
|
...finding,
|
|
224
|
-
gating:
|
|
377
|
+
gating: referenceShapeGates(rel, gated.prefixes),
|
|
225
378
|
})),
|
|
226
379
|
links: findBrokenLinks(lines, (path) =>
|
|
227
380
|
existsSync(resolve(dirname(abs), path)),
|
|
@@ -239,8 +392,8 @@ async function runAudit(
|
|
|
239
392
|
reportLinks(reports, root)
|
|
240
393
|
reportColons(reports)
|
|
241
394
|
reportHeadingCase(reports)
|
|
242
|
-
reportDensity(reports,
|
|
243
|
-
reportReferenceShape(reports,
|
|
395
|
+
reportDensity(reports, gated, resolution.source)
|
|
396
|
+
reportReferenceShape(reports, gated)
|
|
244
397
|
reportBullets(reports, checkpoints)
|
|
245
398
|
reportParagraphs(reports, checkpoints)
|
|
246
399
|
reportCadence(reports, checkpoints)
|
|
@@ -308,7 +461,7 @@ async function runAudit(
|
|
|
308
461
|
}
|
|
309
462
|
|
|
310
463
|
function refuse(
|
|
311
|
-
reason:
|
|
464
|
+
reason: AuditRefusal,
|
|
312
465
|
message: string,
|
|
313
466
|
root: string,
|
|
314
467
|
emitJson: boolean,
|
|
@@ -480,7 +633,11 @@ function describeDensity(finding: GatedDensityFinding): string {
|
|
|
480
633
|
* under no gated prefix reports the same finding a gated one fails on and a
|
|
481
634
|
* reader has to tell the two apart in one frame.
|
|
482
635
|
*/
|
|
483
|
-
function reportDensity(
|
|
636
|
+
function reportDensity(
|
|
637
|
+
reports: readonly FileReport[],
|
|
638
|
+
gated: GatedList,
|
|
639
|
+
source: 'canon' | 'config' | 'absent',
|
|
640
|
+
): void {
|
|
484
641
|
logStep('Density')
|
|
485
642
|
logInfo(
|
|
486
643
|
`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.`,
|
|
@@ -488,11 +645,7 @@ function reportDensity(reports: readonly FileReport[], gates: boolean): void {
|
|
|
488
645
|
logInfo(
|
|
489
646
|
'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
647
|
)
|
|
491
|
-
logInfo(
|
|
492
|
-
gates
|
|
493
|
-
? `A finding gates under a gated prefix (${GATED_PATHS.length === 0 ? 'none yet' : GATED_PATHS.join(', ')}), and a bullet run gates in context, docs, and skills already. Every other finding reports.`
|
|
494
|
-
: 'This tree is not a canon checkout, so every density finding reports and none gates.',
|
|
495
|
-
)
|
|
648
|
+
logInfo(densityGatingLine(gated, source))
|
|
496
649
|
|
|
497
650
|
const carrying = reports
|
|
498
651
|
.filter((report) => report.density.length > 0)
|
|
@@ -528,6 +681,20 @@ function reportDensity(reports: readonly FileReport[], gates: boolean): void {
|
|
|
528
681
|
)
|
|
529
682
|
}
|
|
530
683
|
|
|
684
|
+
function densityGatingLine(
|
|
685
|
+
gated: GatedList,
|
|
686
|
+
source: 'canon' | 'config' | 'absent',
|
|
687
|
+
): string {
|
|
688
|
+
if (source === 'absent') {
|
|
689
|
+
return `No ${GATED_CONFIG_REL} lists a gated prefix, so every density finding reports and none gates. Run 'canon markdown gated add <prefix>' once a folder reaches zero.`
|
|
690
|
+
}
|
|
691
|
+
const listed =
|
|
692
|
+
gated.prefixes.length === 0 ? 'none yet' : gated.prefixes.join(', ')
|
|
693
|
+
return source === 'canon'
|
|
694
|
+
? `A finding gates under a gated prefix (${listed}), and a bullet run gates in context, docs, and skills already. Every other finding reports.`
|
|
695
|
+
: `A finding gates under a prefix ${GATED_CONFIG_REL} lists (${listed}). Every other finding reports.`
|
|
696
|
+
}
|
|
697
|
+
|
|
531
698
|
const REFERENCE_SHAPE_LABELS: Record<ReferenceShapeKind, string> = {
|
|
532
699
|
'extra-section': 'section outside the shape',
|
|
533
700
|
'section-order': 'section out of order or repeated',
|
|
@@ -546,16 +713,16 @@ function describeReferenceShape(finding: GatedReferenceShapeFinding): string {
|
|
|
546
713
|
*/
|
|
547
714
|
function reportReferenceShape(
|
|
548
715
|
reports: readonly FileReport[],
|
|
549
|
-
|
|
716
|
+
gated: GatedList,
|
|
550
717
|
): void {
|
|
551
718
|
logStep('Reference shape')
|
|
552
719
|
logInfo(
|
|
553
720
|
`A page whose frontmatter carries kind: reference holds its opening, then ${REFERENCE_SECTIONS.join(', ')}, each at most once and in that order, with no H3 and no table of flags. An unmarked page is never measured.`,
|
|
554
721
|
)
|
|
555
722
|
logInfo(
|
|
556
|
-
|
|
723
|
+
gated.prefixes.length > 0
|
|
557
724
|
? 'A finding gates under the prefixes the density step names. Every other finding reports.'
|
|
558
|
-
: '
|
|
725
|
+
: 'No prefix is gated, so every reference shape finding reports and none gates.',
|
|
559
726
|
)
|
|
560
727
|
|
|
561
728
|
const carrying = reports.filter((report) => report.referenceShape.length > 0)
|