@erclx/canon 5.38.0 → 5.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +12 -9
- package/claude/skills/auto-ship/SKILL.md +43 -23
- package/claude/skills/backlog-triage/SKILL.md +62 -53
- package/claude/skills/backlog-triage/references/apply.md +69 -0
- package/claude/skills/build-in-slices/SKILL.md +8 -4
- package/claude/skills/canon-cli/SKILL.md +5 -1
- package/claude/skills/canon-feedback/SKILL.md +18 -8
- package/claude/skills/canon-feedback-triage/SKILL.md +10 -3
- package/claude/skills/canvas-board/SKILL.md +37 -18
- package/claude/skills/ci-workflow/SKILL.md +5 -4
- package/claude/skills/code-craft/SKILL.md +10 -6
- package/claude/skills/codebase-layout/SKILL.md +3 -5
- package/claude/skills/context-fold/REQUIREMENT.md +4 -4
- package/claude/skills/context-fold/SKILL.md +42 -23
- package/claude/skills/context-fold/references/context-refresh.md +4 -3
- package/claude/skills/context-fold/references/context.md +79 -64
- package/claude/skills/create-rule/SKILL.md +7 -3
- package/claude/skills/create-skill/SKILL.md +27 -7
- package/claude/skills/create-skill/references/skill-reference.md +41 -17
- package/claude/skills/decision-escalate/SKILL.md +8 -5
- package/claude/skills/deploy-app/SKILL.md +1 -1
- package/claude/skills/deprecation-migration/SKILL.md +12 -4
- package/claude/skills/design-taste/SKILL.md +17 -11
- package/claude/skills/docs-sync/REQUIREMENT.md +1 -0
- package/claude/skills/docs-sync/SKILL.md +17 -6
- package/claude/skills/docs-sync/references/docs.md +54 -21
- package/claude/skills/docs-sync/references/readme.md +32 -8
- package/claude/skills/document-health/SKILL.md +3 -4
- package/claude/skills/draft-and-pick/SKILL.md +73 -47
- package/claude/skills/draft-doc/SKILL.md +9 -4
- package/claude/skills/draft-doc/references/context.md +4 -4
- package/claude/skills/draft-doc/references/docs.md +1 -0
- package/claude/skills/draft-doc/references/wiki.md +3 -2
- package/claude/skills/draft-figure/SKILL.md +9 -7
- package/claude/skills/draft-identity/SKILL.md +24 -52
- package/claude/skills/draft-identity/references/card-route.md +46 -0
- package/claude/skills/draft-ready/SKILL.md +19 -7
- package/claude/skills/draft-screencast/SKILL.md +6 -112
- package/claude/skills/draft-screencast/references/beats-template.md +119 -0
- package/claude/skills/draft-slides/SKILL.md +9 -7
- package/claude/skills/draft-slides/references/deck.md +183 -0
- package/claude/skills/git-branch/SKILL.md +3 -3
- package/claude/skills/git-commit/SKILL.md +5 -3
- package/claude/skills/git-followup/SKILL.md +16 -9
- package/claude/skills/git-issue/SKILL.md +6 -5
- package/claude/skills/git-pr/SKILL.md +56 -44
- package/claude/skills/git-pr/references/evidence.md +2 -2
- package/claude/skills/git-pr/references/final-command.md +1 -1
- package/claude/skills/git-pr/references/labels.md +1 -1
- package/claude/skills/git-pr/references/publish.md +1 -1
- package/claude/skills/git-ship/SKILL.md +43 -34
- package/claude/skills/git-ship/references/ci-watch.md +31 -0
- package/claude/skills/git-split/SKILL.md +28 -100
- package/claude/skills/git-split/references/independent.md +40 -0
- package/claude/skills/git-split/references/stacked.md +73 -0
- package/claude/skills/git-stage/SKILL.md +9 -7
- package/claude/skills/git-worktree/SKILL.md +27 -49
- package/claude/skills/git-worktree/references/cleanup.md +55 -0
- package/claude/skills/markdown-craft/SKILL.md +2 -2
- package/claude/skills/markdown-craft/references/markdown.md +82 -53
- package/claude/skills/plan-feature/SKILL.md +52 -46
- package/claude/skills/plan-feature/references/batches.md +46 -0
- package/claude/skills/plan-groundwork/SKILL.md +60 -74
- package/claude/skills/plan-groundwork/references/output.md +48 -0
- package/claude/skills/plan-intake/SKILL.md +28 -21
- package/claude/skills/plan-intake-answer/SKILL.md +14 -13
- package/claude/skills/read-frames/SKILL.md +2 -0
- package/claude/skills/record-screencast/SKILL.md +31 -11
- package/claude/skills/review-address/SKILL.md +76 -158
- package/claude/skills/review-address/references/rebase-only.md +34 -0
- package/claude/skills/review-craft/SKILL.md +27 -11
- package/claude/skills/review-pr/SKILL.md +91 -40
- package/claude/skills/review-ui/SKILL.md +63 -37
- package/claude/skills/review-ui/references/hosted-preview.md +30 -0
- package/claude/skills/role-orchestrator/SKILL.md +105 -74
- package/claude/skills/role-orchestrator/references/orchestrator-filing.md +26 -0
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
- package/claude/skills/role-planner/SKILL.md +66 -64
- package/claude/skills/role-reviewer/SKILL.md +57 -46
- package/claude/skills/role-worker/SKILL.md +68 -63
- package/claude/skills/search-craft/SKILL.md +7 -5
- package/claude/skills/session-compact/SKILL.md +16 -11
- package/claude/skills/session-map/SKILL.md +31 -10
- package/claude/skills/session-relay/SKILL.md +32 -65
- package/claude/skills/session-resume/SKILL.md +18 -13
- package/claude/skills/session-worktree/SKILL.md +70 -42
- package/claude/skills/systematic-debugging/SKILL.md +6 -1
- package/claude/skills/target-check/SKILL.md +13 -6
- package/claude/skills/target-setup/SKILL.md +39 -27
- package/claude/skills/task-board/SKILL.md +68 -60
- package/claude/skills/task-board/references/board.md +1 -1
- package/claude/skills/task-board/references/create-report.md +36 -0
- package/claude/skills/task-board/references/tasks.md +1 -1
- package/claude/skills/task-complete/SKILL.md +20 -18
- package/claude/skills/teach-workspace/SKILL.md +61 -33
- package/claude/skills/test-craft/SKILL.md +2 -1
- package/claude/skills/test-first/SKILL.md +15 -9
- package/claude/skills/ui-checklist/SKILL.md +18 -16
- package/claude/skills/ux-audit/SKILL.md +9 -10
- package/claude/skills/ux-measure/SKILL.md +23 -19
- package/claude/skills/video-craft/SKILL.md +15 -7
- package/claude/skills/write-human/SKILL.md +9 -1
- package/claude/skills/youtube-transcripts/SKILL.md +2 -1
- package/docs/agents/audits.md +61 -60
- package/docs/agents/context-audit.md +40 -58
- package/docs/agents/index.md +11 -61
- package/docs/agents/indexes.md +25 -29
- package/docs/agents/install-and-sync.md +125 -235
- package/docs/agents/markdown-audit.md +54 -96
- package/docs/agents/migrate.md +41 -16
- package/docs/agents/output-shape.md +60 -45
- package/docs/agents/overview.md +36 -12
- package/docs/agents/records.md +37 -79
- package/docs/agents/sessions-transfer.md +26 -70
- package/docs/agents/targets.md +25 -77
- package/docs/target/index.md +0 -1
- package/docs/target/migrations.md +57 -66
- package/docs/target/projects.md +36 -78
- package/docs/target/scenarios.md +9 -19
- package/docs/target/sync.md +27 -68
- package/docs/workflow/ai-workflow.md +22 -68
- package/docs/workflow/operating-model.md +32 -186
- package/docs/workflow/parallel-features.md +29 -66
- package/docs/workflow/skill-map-on-request.md +22 -24
- package/docs/workflow/skill-map.md +72 -76
- package/docs/workflow/visual-design-workflow.md +25 -118
- package/docs/workflow/zshrc-aliases.md +18 -37
- package/package.json +1 -1
- package/src/canvas/client/inspector/fields.css +4 -0
- package/src/canvas/index.html +6 -0
- package/src/claude/skills-reach.ts +2 -2
- package/src/cli/ui.ts +3 -1
- package/src/commands/canvas.ts +23 -0
- package/src/commands/capture.ts +14 -0
- package/src/commands/claude.ts +14 -1
- package/src/commands/context.ts +106 -7
- package/src/commands/demo.ts +22 -0
- package/src/commands/deps.ts +6 -0
- package/src/commands/driver.ts +28 -2
- package/src/commands/gov.ts +52 -18
- package/src/commands/hooks.ts +63 -0
- package/src/commands/intake.ts +17 -0
- package/src/commands/labels.ts +38 -5
- package/src/commands/markdown.ts +169 -32
- package/src/commands/pr.ts +160 -43
- package/src/commands/records.ts +8 -0
- package/src/commands/secrets.ts +6 -0
- package/src/commands/sessions.ts +8 -0
- package/src/commands/tasks.ts +52 -0
- package/src/commands/teach.ts +13 -0
- package/src/commands/upstream.ts +25 -1
- package/src/commands/worktrees.ts +5 -0
- package/src/context/audit.ts +274 -5
- package/src/context/classify/prompts.ts +1 -1
- package/src/context/folders.ts +1 -1
- package/src/context/gate.ts +23 -7
- package/src/context/shape-gated.ts +30 -0
- package/src/counts/scan.ts +1 -1
- package/src/design/components.ts +1 -1
- package/src/design/document.ts +1 -1
- package/src/design/tokens.ts +1 -1
- package/src/gate/boundaries.ts +4 -1
- package/src/gate/measures.ts +16 -191
- package/src/gate/stages.ts +14 -30
- package/src/git/env.ts +6 -0
- package/src/hooks/reply-links.ts +110 -0
- package/src/markdown/density.ts +258 -0
- package/src/markdown/gate.ts +57 -18
- package/src/markdown/reference-shape.ts +103 -0
- package/src/markdown/scope.ts +162 -0
- package/src/markdown/structure.ts +9 -59
- package/src/pr/lift.ts +153 -19
- package/src/pr/painted.ts +87 -0
- package/src/roots/record.ts +18 -1
- package/src/sandbox/census.ts +1 -1
- package/src/sandbox/headless/run.ts +3 -2
- package/src/sandbox/headless/watch.ts +3 -4
- package/src/shipped/references.ts +1 -1
- package/src/slides/convert/master.ts +3 -0
- package/src/sync/format.ts +46 -0
- package/src/sync/stamp.ts +6 -0
- package/tooling/astro/reference.md +2 -2
- package/tooling/base/reference.md +1 -1
- package/tooling/claude/seeds/.claude/hooks/reply-links.sh +72 -0
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +22 -1
- package/tooling/claude/seeds/.claude/settings.json +4 -0
- package/tooling/go/reference.md +1 -1
- package/tooling/nextjs/reference.md +1 -1
- package/tooling/php/reference.md +1 -1
- package/tooling/python/reference.md +1 -1
- package/tooling/vite-react/reference.md +1 -1
- package/tooling/web/reference.md +1 -1
- package/docs/agents/canvas.md +0 -86
- package/docs/agents/capture.md +0 -65
- package/docs/agents/census.md +0 -23
- package/docs/agents/commands.md +0 -154
- package/docs/agents/comments.md +0 -34
- package/docs/agents/context-audit-checks.md +0 -90
- package/docs/agents/context-classify.md +0 -101
- package/docs/agents/counts.md +0 -74
- package/docs/agents/demo.md +0 -140
- package/docs/agents/docs.md +0 -27
- package/docs/agents/drift-surfaces.md +0 -201
- package/docs/agents/driver.md +0 -103
- package/docs/agents/gate.md +0 -90
- package/docs/agents/gov-detect.md +0 -47
- package/docs/agents/hooks.md +0 -58
- package/docs/agents/intake.md +0 -81
- package/docs/agents/key-changes.md +0 -116
- package/docs/agents/label-coverage.md +0 -86
- package/docs/agents/markdown-audit-weight.md +0 -81
- package/docs/agents/pr-evidence-layout.md +0 -70
- package/docs/agents/pr-evidence.md +0 -273
- package/docs/agents/pr-frames.md +0 -131
- package/docs/agents/pr-lift.md +0 -132
- package/docs/agents/pr-local.md +0 -111
- package/docs/agents/pr-preview.md +0 -155
- package/docs/agents/pr-reads.md +0 -111
- package/docs/agents/pr-tick.md +0 -72
- package/docs/agents/records-push.md +0 -61
- package/docs/agents/records-stale.md +0 -40
- package/docs/agents/records-validate.md +0 -57
- package/docs/agents/restated.md +0 -83
- package/docs/agents/routing.md +0 -61
- package/docs/agents/rule-citations.md +0 -90
- package/docs/agents/sandbox.md +0 -126
- package/docs/agents/scripting.md +0 -142
- package/docs/agents/sessions.md +0 -128
- package/docs/agents/skills-audit.md +0 -120
- package/docs/agents/skills-reach.md +0 -64
- package/docs/agents/slides.md +0 -176
- package/docs/agents/state-scoped-risk.md +0 -105
- package/docs/agents/superseded.md +0 -85
- package/docs/agents/tasks-archive.md +0 -83
- package/docs/agents/tasks-plans.md +0 -152
- package/docs/agents/tasks-validate.md +0 -133
- package/docs/agents/tasks.md +0 -123
- package/docs/agents/teach-authoring.md +0 -118
- package/docs/agents/teach.md +0 -157
- package/docs/agents/test-order.md +0 -58
- package/docs/agents/upstream.md +0 -94
- package/docs/agents/worktrees.md +0 -118
- package/docs/target/standards-moved.md +0 -34
- package/src/markdown/growth.ts +0 -43
package/src/commands/gov.ts
CHANGED
|
@@ -84,6 +84,7 @@ const CANON_RULES_REL = join('.claude', 'rules', 'canon')
|
|
|
84
84
|
|
|
85
85
|
interface InstallOptions {
|
|
86
86
|
readonly add?: string
|
|
87
|
+
readonly yes?: boolean
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
interface RegenOptions {
|
|
@@ -147,6 +148,7 @@ export function register(program: Command): void {
|
|
|
147
148
|
.argument('[target]', 'Target directory', '.')
|
|
148
149
|
.helpOption('-h, --help', 'Show this help message')
|
|
149
150
|
.option('--add <rules>', 'Comma-separated rules to layer on the stack')
|
|
151
|
+
.option('--yes', 'Answer the install confirm without a prompt')
|
|
150
152
|
.addHelpText(
|
|
151
153
|
'after',
|
|
152
154
|
[
|
|
@@ -156,6 +158,7 @@ export function register(program: Command): void {
|
|
|
156
158
|
' canon gov install node ../my-app',
|
|
157
159
|
' canon gov install node --add astro,shadcn ../my-app',
|
|
158
160
|
' canon gov install --add tasks,plan ../my-app',
|
|
161
|
+
' canon gov install node --yes ../my-app',
|
|
159
162
|
'',
|
|
160
163
|
'With --add and no stack, the named rules install alone. A first',
|
|
161
164
|
'positional naming a stack is read as the stack, so pass . or a path',
|
|
@@ -164,6 +167,10 @@ export function register(program: Command): void {
|
|
|
164
167
|
'Copies each rule into .claude/rules/canon/, replacing a copy already',
|
|
165
168
|
'there. Rules under .claude/rules/project/ are never read.',
|
|
166
169
|
'',
|
|
170
|
+
'The install confirm needs a TTY. --yes answers it, so a headless call',
|
|
171
|
+
'installs without setting CANON_NON_INTERACTIVE=1. It does not pick a',
|
|
172
|
+
'stack, so a headless call still names one.',
|
|
173
|
+
'',
|
|
167
174
|
'Exit codes:',
|
|
168
175
|
' 0 installed, the stack held no rules, or the prompt was cancelled',
|
|
169
176
|
' 1 refused: no stack named headlessly, an unknown stack, no --add',
|
|
@@ -349,6 +356,10 @@ export function register(program: Command): void {
|
|
|
349
356
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
350
357
|
' 2 at least one stated figure disagrees with the tree',
|
|
351
358
|
'',
|
|
359
|
+
'Reasons:',
|
|
360
|
+
' no-git git could not list the tree',
|
|
361
|
+
' no-markdown the tree holds no markdown file',
|
|
362
|
+
'',
|
|
352
363
|
'It reports and never gates. The false-positive rate is read off the',
|
|
353
364
|
'first run rather than assumed, and a push failing on a stated figure',
|
|
354
365
|
'that reads correctly to a person would teach a contributor to route',
|
|
@@ -415,6 +426,9 @@ export function register(program: Command): void {
|
|
|
415
426
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
416
427
|
' 2 at least one declaration asserts either',
|
|
417
428
|
'',
|
|
429
|
+
'Refuses an empty superseded value, a superseded value equal to its',
|
|
430
|
+
'replacement, and a tree git cannot list. An empty replacement is allowed.',
|
|
431
|
+
'',
|
|
418
432
|
'Examples:',
|
|
419
433
|
' canon gov superseded feature-feat- feature-',
|
|
420
434
|
' canon gov superseded feature-feat- feature- --json',
|
|
@@ -496,6 +510,9 @@ export function register(program: Command): void {
|
|
|
496
510
|
' 2 at least one cited path resolves to nothing, or one glob matches',
|
|
497
511
|
' nothing in this tree',
|
|
498
512
|
'',
|
|
513
|
+
'Refuses a tree holding neither rule corpus, and a `git check-ignore`',
|
|
514
|
+
'read that fails.',
|
|
515
|
+
'',
|
|
499
516
|
'Examples:',
|
|
500
517
|
' canon gov citations',
|
|
501
518
|
' canon gov citations --json',
|
|
@@ -543,6 +560,10 @@ export function register(program: Command): void {
|
|
|
543
560
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
544
561
|
' 2 at least one instruction is restated outside a declared mirror',
|
|
545
562
|
'',
|
|
563
|
+
'Reasons:',
|
|
564
|
+
' no-instructions the tree holds no always-loaded file or seed to read',
|
|
565
|
+
' no-surfaces the tree holds no skills tree or rules tree to match against',
|
|
566
|
+
'',
|
|
546
567
|
'Examples:',
|
|
547
568
|
' canon gov restated',
|
|
548
569
|
' canon gov restated --json',
|
|
@@ -1268,8 +1289,9 @@ async function runInstall(
|
|
|
1268
1289
|
intro('canon gov install')
|
|
1269
1290
|
|
|
1270
1291
|
const add = opts.add ?? ''
|
|
1292
|
+
const yes = opts.yes === true
|
|
1271
1293
|
if (add !== '' && isAddWithoutStack(stack, target)) {
|
|
1272
|
-
return installExtras(add, stack ?? target)
|
|
1294
|
+
return installExtras(add, stack ?? target, yes)
|
|
1273
1295
|
}
|
|
1274
1296
|
|
|
1275
1297
|
const resolved = resolveTarget(target, PROJECT_ROOT)
|
|
@@ -1314,14 +1336,7 @@ async function runInstall(
|
|
|
1314
1336
|
for (const rule of missing) logWarn(`${rule} (source not found, skipping)`)
|
|
1315
1337
|
for (const source of found) logInfo(source.rule)
|
|
1316
1338
|
|
|
1317
|
-
const shouldInstall = await
|
|
1318
|
-
message: `Install ${found.length} rules to ${displayPath(target, CANON_RULES_REL)}?`,
|
|
1319
|
-
options: [
|
|
1320
|
-
{ value: true, label: 'Yes' },
|
|
1321
|
-
{ value: false, label: 'No' },
|
|
1322
|
-
],
|
|
1323
|
-
nonInteractiveDefault: true,
|
|
1324
|
-
})
|
|
1339
|
+
const shouldInstall = await confirmInstall(found.length, target, yes)
|
|
1325
1340
|
|
|
1326
1341
|
if (!shouldInstall) {
|
|
1327
1342
|
logWarn('Cancelled')
|
|
@@ -1347,6 +1362,28 @@ async function runInstall(
|
|
|
1347
1362
|
return 0
|
|
1348
1363
|
}
|
|
1349
1364
|
|
|
1365
|
+
/**
|
|
1366
|
+
* The install confirm. `--yes` answers it without a prompt, so the line it
|
|
1367
|
+
* would have printed is skipped rather than duplicated under
|
|
1368
|
+
* `CANON_NON_INTERACTIVE=1`.
|
|
1369
|
+
*/
|
|
1370
|
+
async function confirmInstall(
|
|
1371
|
+
count: number,
|
|
1372
|
+
target: string,
|
|
1373
|
+
yes: boolean,
|
|
1374
|
+
): Promise<boolean> {
|
|
1375
|
+
if (yes) return true
|
|
1376
|
+
return select({
|
|
1377
|
+
message: `Install ${count} rules to ${displayPath(target, CANON_RULES_REL)}?`,
|
|
1378
|
+
options: [
|
|
1379
|
+
{ value: true, label: 'Yes' },
|
|
1380
|
+
{ value: false, label: 'No' },
|
|
1381
|
+
],
|
|
1382
|
+
nonInteractiveDefault: true,
|
|
1383
|
+
ttyHint: 'Pass --yes',
|
|
1384
|
+
})
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1350
1387
|
/**
|
|
1351
1388
|
* Whether `--add` runs with no stack under it. Commander fills the first
|
|
1352
1389
|
* positional before the second, so `--add a,b ../my-app` arrives with the
|
|
@@ -1366,7 +1403,11 @@ function isAddWithoutStack(stack: string | undefined, target: string): boolean {
|
|
|
1366
1403
|
* extras are not a stack, and overwriting the chain would cost a target the
|
|
1367
1404
|
* missing-rule report its stack install recorded.
|
|
1368
1405
|
*/
|
|
1369
|
-
async function installExtras(
|
|
1406
|
+
async function installExtras(
|
|
1407
|
+
add: string,
|
|
1408
|
+
target: string,
|
|
1409
|
+
yes: boolean,
|
|
1410
|
+
): Promise<number> {
|
|
1370
1411
|
const resolved = resolveTarget(target, PROJECT_ROOT)
|
|
1371
1412
|
if (typeof resolved === 'number') return resolved
|
|
1372
1413
|
|
|
@@ -1382,14 +1423,7 @@ async function installExtras(add: string, target: string): Promise<number> {
|
|
|
1382
1423
|
return 1
|
|
1383
1424
|
}
|
|
1384
1425
|
|
|
1385
|
-
const shouldInstall = await
|
|
1386
|
-
message: `Install ${found.length} rules to ${displayPath(target, CANON_RULES_REL)}?`,
|
|
1387
|
-
options: [
|
|
1388
|
-
{ value: true, label: 'Yes' },
|
|
1389
|
-
{ value: false, label: 'No' },
|
|
1390
|
-
],
|
|
1391
|
-
nonInteractiveDefault: true,
|
|
1392
|
-
})
|
|
1426
|
+
const shouldInstall = await confirmInstall(found.length, target, yes)
|
|
1393
1427
|
|
|
1394
1428
|
if (!shouldInstall) {
|
|
1395
1429
|
logWarn('Cancelled')
|
package/src/commands/hooks.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { findMissingReplyLinks, formatMissing } from '@/hooks/reply-links'
|
|
2
4
|
import {
|
|
3
5
|
readMergedSubjects,
|
|
4
6
|
runCanon,
|
|
@@ -11,6 +13,11 @@ interface PostMergeOptions {
|
|
|
11
13
|
readonly root?: string
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
interface ReplyLinksOptions {
|
|
17
|
+
readonly root?: string
|
|
18
|
+
readonly json?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
export function register(program: Command): void {
|
|
15
22
|
const hooks = program
|
|
16
23
|
.command('hooks')
|
|
@@ -64,4 +71,60 @@ export function register(program: Command): void {
|
|
|
64
71
|
})
|
|
65
72
|
process.exitCode = 0
|
|
66
73
|
})
|
|
74
|
+
|
|
75
|
+
hooks
|
|
76
|
+
.command('reply-links')
|
|
77
|
+
.description(
|
|
78
|
+
'Check that every markdown link in a reply on stdin names a file on disk',
|
|
79
|
+
)
|
|
80
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
81
|
+
.option(
|
|
82
|
+
'--root <path>',
|
|
83
|
+
'Root a relative link resolves against, default the main worktree root',
|
|
84
|
+
)
|
|
85
|
+
.option('--json', 'Write the findings as a record on stdout')
|
|
86
|
+
.addHelpText(
|
|
87
|
+
'after',
|
|
88
|
+
[
|
|
89
|
+
'',
|
|
90
|
+
'Reads the reply from stdin and resolves each link target on disk.',
|
|
91
|
+
'Placeholders, globs, URLs, anchors, and links inside code are skipped.',
|
|
92
|
+
'A :42 or #L42 tail is stripped before the check.',
|
|
93
|
+
'',
|
|
94
|
+
'Missing paths go to stderr, and --json adds a record on stdout.',
|
|
95
|
+
'',
|
|
96
|
+
'Exit codes:',
|
|
97
|
+
' 0 every link resolves, or the reply holds none',
|
|
98
|
+
' 1 at least one link names nothing on disk, or stdin is a terminal',
|
|
99
|
+
'',
|
|
100
|
+
'Examples:',
|
|
101
|
+
' printf "%s" "$reply" | canon hooks reply-links',
|
|
102
|
+
' canon hooks reply-links --json < reply.md',
|
|
103
|
+
'',
|
|
104
|
+
].join('\n'),
|
|
105
|
+
)
|
|
106
|
+
.action(async (opts: ReplyLinksOptions) => {
|
|
107
|
+
if (process.stdin.isTTY) {
|
|
108
|
+
process.stderr.write('Pipe the reply on stdin.\n')
|
|
109
|
+
process.exitCode = 1
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const found = findMissingReplyLinks(
|
|
114
|
+
readFileSync(0, 'utf8'),
|
|
115
|
+
opts.root ?? (await mainWorktreeRoot()),
|
|
116
|
+
existsSync,
|
|
117
|
+
)
|
|
118
|
+
if (opts.json) {
|
|
119
|
+
process.stdout.write(
|
|
120
|
+
`${JSON.stringify({
|
|
121
|
+
message: found.length > 0 ? formatMissing(found) : undefined,
|
|
122
|
+
missing: found,
|
|
123
|
+
ok: found.length === 0,
|
|
124
|
+
})}\n`,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
if (found.length > 0) process.stderr.write(`${formatMissing(found)}\n`)
|
|
128
|
+
process.exitCode = found.length > 0 ? 1 : 0
|
|
129
|
+
})
|
|
67
130
|
}
|
package/src/commands/intake.ts
CHANGED
|
@@ -64,6 +64,12 @@ export function register(program: Command): void {
|
|
|
64
64
|
.addHelpText(
|
|
65
65
|
'after',
|
|
66
66
|
[
|
|
67
|
+
'',
|
|
68
|
+
'Refusals, read off `reason`:',
|
|
69
|
+
' no-intake the project holds no intake folder',
|
|
70
|
+
' no-folder no folder matches the slug',
|
|
71
|
+
' ambiguous-slug a bare slug matches more than one ordinal-prefixed',
|
|
72
|
+
' folder, all named in `detail`',
|
|
67
73
|
'',
|
|
68
74
|
'Exit codes:',
|
|
69
75
|
' 0 the folders or items were listed',
|
|
@@ -105,6 +111,17 @@ export function register(program: Command): void {
|
|
|
105
111
|
.addHelpText(
|
|
106
112
|
'after',
|
|
107
113
|
[
|
|
114
|
+
'',
|
|
115
|
+
'Refusals, read off `reason`:',
|
|
116
|
+
' no-intake the project holds no intake folder',
|
|
117
|
+
' no-folder no folder matches the slug',
|
|
118
|
+
' ambiguous-slug a bare slug matches more than one ordinal-prefixed',
|
|
119
|
+
' folder, all named in `detail`',
|
|
120
|
+
' no-cluster the cluster file named does not exist',
|
|
121
|
+
' no-item the item number named does not exist in the cluster',
|
|
122
|
+
' answered the item already carries an answer',
|
|
123
|
+
' bad-input a malformed command line, such as no selection, an empty',
|
|
124
|
+
' answer, or two answers for one item',
|
|
108
125
|
'',
|
|
109
126
|
'Exit codes:',
|
|
110
127
|
' 0 every named item now carries its answer',
|
package/src/commands/labels.ts
CHANGED
|
@@ -111,6 +111,10 @@ export function register(program: Command): void {
|
|
|
111
111
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
112
112
|
' 2 at least one changed path is reached by no row',
|
|
113
113
|
'',
|
|
114
|
+
'Reasons:',
|
|
115
|
+
' no-map the project declares no label map, so it labels nothing. An',
|
|
116
|
+
' expected absence for `canon audits run`, not a fault',
|
|
117
|
+
'',
|
|
114
118
|
'Examples:',
|
|
115
119
|
' canon labels audit',
|
|
116
120
|
' canon labels audit --json',
|
|
@@ -130,7 +134,10 @@ export function register(program: Command): void {
|
|
|
130
134
|
)
|
|
131
135
|
.argument('<number>', "This branch's open pull request")
|
|
132
136
|
.helpOption('-h, --help', 'Show this help message')
|
|
133
|
-
.option(
|
|
137
|
+
.option(
|
|
138
|
+
'--base <ref>',
|
|
139
|
+
"Read the git range back to this ref instead of the pull request's files",
|
|
140
|
+
)
|
|
134
141
|
.option('--root <path>', 'Repository to read, defaulting to the cwd')
|
|
135
142
|
.option('--check', 'Report the labels owed and write nothing')
|
|
136
143
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
@@ -138,9 +145,10 @@ export function register(program: Command): void {
|
|
|
138
145
|
'after',
|
|
139
146
|
[
|
|
140
147
|
'',
|
|
141
|
-
|
|
142
|
-
'
|
|
143
|
-
'
|
|
148
|
+
"Resolves the earned set from the pull request's own files listing, or",
|
|
149
|
+
'from the git range when --base is passed, binds to the pull request only',
|
|
150
|
+
'when its head is the current branch and it is open, and adds each earned',
|
|
151
|
+
'label the pull request lacks over REST.',
|
|
144
152
|
'',
|
|
145
153
|
'It adds and never removes, so a label a person applied by hand stays.',
|
|
146
154
|
'A label the remote does not carry is reported under missingOnRemote and',
|
|
@@ -155,6 +163,12 @@ export function register(program: Command): void {
|
|
|
155
163
|
' 2 labels are still owed, being any under --check, or a label the',
|
|
156
164
|
' remote lacks or a failed add otherwise',
|
|
157
165
|
'',
|
|
166
|
+
'Reasons:',
|
|
167
|
+
' head-mismatch the pull request is not open on the current branch, under',
|
|
168
|
+
' --check as well',
|
|
169
|
+
' gh-failed the files listing or the labels could not be read, and the',
|
|
170
|
+
' run does not fall back to git',
|
|
171
|
+
'',
|
|
158
172
|
'Examples:',
|
|
159
173
|
' canon labels apply 123 --json',
|
|
160
174
|
' canon labels apply 123 --check',
|
|
@@ -396,7 +410,26 @@ async function runApply(number: string, opts: ApplyOptions): Promise<number> {
|
|
|
396
410
|
if (target === null) return refuse('gh-failed')
|
|
397
411
|
if (target.trim() !== `${head} open`) return refuse('head-mismatch')
|
|
398
412
|
|
|
399
|
-
|
|
413
|
+
// An explicit --base is an explicit request for the git route. Otherwise the
|
|
414
|
+
// pull request's own file listing is the diff the labels describe, and a
|
|
415
|
+
// failed read refuses rather than falling back, since binding needed gh too.
|
|
416
|
+
let paths: string[] | undefined
|
|
417
|
+
if (opts.base === undefined) {
|
|
418
|
+
const listing = await gh(root, [
|
|
419
|
+
'api',
|
|
420
|
+
'--paginate',
|
|
421
|
+
`repos/{owner}/{repo}/pulls/${number}/files`,
|
|
422
|
+
'--jq',
|
|
423
|
+
'.[].filename',
|
|
424
|
+
])
|
|
425
|
+
if (listing === null) return refuse('gh-failed')
|
|
426
|
+
paths = nonEmptyLines(listing)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const report = await auditLabels(root, {
|
|
430
|
+
base: opts.base,
|
|
431
|
+
...(paths !== undefined && { paths }),
|
|
432
|
+
})
|
|
400
433
|
|
|
401
434
|
// A project declaring no map earns nothing and is labelled silently, which
|
|
402
435
|
// is an answer and exits clean rather than reading as a break.
|
package/src/commands/markdown.ts
CHANGED
|
@@ -6,19 +6,30 @@ import { BAN_SETS, emptyBanSets } from '@/markdown/bans'
|
|
|
6
6
|
import { isChangelog, lengthExemption } from '@/markdown/ceiling'
|
|
7
7
|
import { type ColonFinding, findColons } from '@/markdown/colons'
|
|
8
8
|
import { type MarkdownAuditRefusal, resolveMarkdown } from '@/markdown/files'
|
|
9
|
-
import {
|
|
9
|
+
import { densityFindings, type DensityKind } from '@/markdown/density'
|
|
10
|
+
import {
|
|
11
|
+
type GatedDensityFinding,
|
|
12
|
+
type GatedReferenceShapeFinding,
|
|
13
|
+
isGating,
|
|
14
|
+
referenceShapeGates,
|
|
15
|
+
} from '@/markdown/gate'
|
|
10
16
|
import {
|
|
11
17
|
findHeadingCase,
|
|
12
18
|
type HeadingCaseFinding,
|
|
13
19
|
} from '@/markdown/heading-case'
|
|
14
20
|
import { decodePath, findBrokenLinks, type LinkFinding } from '@/markdown/links'
|
|
21
|
+
import {
|
|
22
|
+
REFERENCE_SECTIONS,
|
|
23
|
+
type ReferenceShapeKind,
|
|
24
|
+
referenceShapeFindings,
|
|
25
|
+
} from '@/markdown/reference-shape'
|
|
15
26
|
import {
|
|
16
27
|
type BanFinding,
|
|
17
28
|
type BanSets,
|
|
18
29
|
bodyLines,
|
|
19
30
|
scanBans,
|
|
20
31
|
} from '@/markdown/scan'
|
|
21
|
-
import {
|
|
32
|
+
import { densityGates, GATED_PATHS, inDensityScope } from '@/markdown/scope'
|
|
22
33
|
import {
|
|
23
34
|
BASELINE,
|
|
24
35
|
CHECKPOINTS,
|
|
@@ -36,6 +47,7 @@ import {
|
|
|
36
47
|
pipeOutput,
|
|
37
48
|
plural,
|
|
38
49
|
} from '@/cli/ui'
|
|
50
|
+
import { isOwnCheckout } from '@/roots/project'
|
|
39
51
|
|
|
40
52
|
const EXIT_REFUSED = 1
|
|
41
53
|
const EXIT_GATE = 2
|
|
@@ -60,7 +72,10 @@ interface FileReport {
|
|
|
60
72
|
readonly links: readonly LinkFinding[]
|
|
61
73
|
readonly colons: readonly ColonFinding[]
|
|
62
74
|
readonly headingCase: readonly HeadingCaseFinding[]
|
|
63
|
-
|
|
75
|
+
/** Empty on a path outside the density scope, so a plan never carries one. */
|
|
76
|
+
readonly density: readonly GatedDensityFinding[]
|
|
77
|
+
/** Empty on a page whose frontmatter does not carry `kind: reference`. */
|
|
78
|
+
readonly referenceShape: readonly GatedReferenceShapeFinding[]
|
|
64
79
|
readonly structure: StructureReport
|
|
65
80
|
readonly renderedLines: number
|
|
66
81
|
/** The stated reason, or null where the document is not exempt. */
|
|
@@ -76,7 +91,7 @@ export function register(program: Command): void {
|
|
|
76
91
|
markdown
|
|
77
92
|
.command('audit')
|
|
78
93
|
.description(
|
|
79
|
-
'Fail on a banned character, and report
|
|
94
|
+
'Fail on a banned character, a dead link, or a gated density finding, and report weight, cadence, depth, and length',
|
|
80
95
|
)
|
|
81
96
|
.argument(
|
|
82
97
|
'[path...]',
|
|
@@ -91,22 +106,33 @@ export function register(program: Command): void {
|
|
|
91
106
|
'Exit codes:',
|
|
92
107
|
' 0 the audit completed with no gating finding',
|
|
93
108
|
' 1 refused, with the reason on stderr',
|
|
94
|
-
' 2 a banned character is present,
|
|
95
|
-
'
|
|
109
|
+
' 2 a banned character is present, a relative link resolves to',
|
|
110
|
+
' nothing on disk, or a density or reference shape finding sits',
|
|
111
|
+
' on a gated path',
|
|
96
112
|
' 3 a shipped ban set is empty, so the run measured nothing',
|
|
97
113
|
'',
|
|
98
114
|
'A ban hit and a dead link are each a fact and gate unconditionally.',
|
|
99
|
-
'
|
|
100
|
-
'
|
|
115
|
+
'Depth weight is a judgment a reader settles, so it reports and never',
|
|
116
|
+
'fails a run.',
|
|
117
|
+
'',
|
|
118
|
+
'Density holds six limits: a 350-character paragraph, 2 paragraphs in',
|
|
119
|
+
'a row, a 200-character bullet, 5 plain bullets in a row, no H4, and a',
|
|
120
|
+
'paragraph or table opening every H3. Only context, docs, skills, tooling',
|
|
121
|
+
'seeds and references, the wiki, README.md, and CONTRIBUTING.md take a',
|
|
122
|
+
'finding. One gates under a prefix in GATED_PATHS in src/markdown/scope.ts,',
|
|
123
|
+
'and a bullet run gates in context, docs, and skills from the start.',
|
|
124
|
+
'Outside a canon checkout every density finding reports and none gates.',
|
|
125
|
+
'',
|
|
126
|
+
'A page whose frontmatter carries kind: reference takes the reference',
|
|
127
|
+
'shape: no H2 beyond Output, Refusals, and See also, each at most once',
|
|
128
|
+
'and in that order, no H3, and no table whose first header cell names',
|
|
129
|
+
'flags or options. It gates under the same prefixes density does, and',
|
|
130
|
+
'an unmarked page is never measured.',
|
|
101
131
|
'',
|
|
102
132
|
'A mid-sentence colon and a heading with a capital after its first word',
|
|
103
133
|
'report the same way. A proper noun is the one case the second cannot',
|
|
104
134
|
'tell from title case, and the first is sometimes right.',
|
|
105
135
|
'',
|
|
106
|
-
'Bullet stacks report a run of more than 5 plain bullets under one',
|
|
107
|
-
'heading and never exit 2. The Bullet stacks gate stage owns that',
|
|
108
|
-
'verdict, for the reason the length line below states.',
|
|
109
|
-
'',
|
|
110
136
|
'Length reports a whole document past the ceiling and never exits 2.',
|
|
111
137
|
'The Document ceiling gate stage owns that verdict, since this audit also',
|
|
112
138
|
'runs on a record kept out of version control, where a long plan is not',
|
|
@@ -131,7 +157,7 @@ export function register(program: Command): void {
|
|
|
131
157
|
' canon markdown audit',
|
|
132
158
|
' canon markdown audit --json',
|
|
133
159
|
' canon markdown audit .claude/rules governance',
|
|
134
|
-
' canon markdown audit docs/agents/
|
|
160
|
+
' canon markdown audit docs/agents/install-and-sync.md',
|
|
135
161
|
" canon markdown audit 'standards/**/*.md'",
|
|
136
162
|
'',
|
|
137
163
|
].join('\n'),
|
|
@@ -171,6 +197,9 @@ async function runAudit(
|
|
|
171
197
|
const bans = BAN_SETS
|
|
172
198
|
const empty = emptyBanSets(bans)
|
|
173
199
|
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)
|
|
174
203
|
|
|
175
204
|
const reports: FileReport[] = await Promise.all(
|
|
176
205
|
scope.files.map(async (rel) => {
|
|
@@ -184,7 +213,16 @@ async function runAudit(
|
|
|
184
213
|
bans: scanBans(lines, bans),
|
|
185
214
|
colons: advisory ? findColons(lines) : [],
|
|
186
215
|
headingCase: advisory ? findHeadingCase(source) : [],
|
|
187
|
-
|
|
216
|
+
density: inDensityScope(rel)
|
|
217
|
+
? densityFindings(lines).map((finding) => ({
|
|
218
|
+
...finding,
|
|
219
|
+
gating: gates && densityGates(rel, finding.kind),
|
|
220
|
+
}))
|
|
221
|
+
: [],
|
|
222
|
+
referenceShape: referenceShapeFindings(source).map((finding) => ({
|
|
223
|
+
...finding,
|
|
224
|
+
gating: gates && referenceShapeGates(rel),
|
|
225
|
+
})),
|
|
188
226
|
links: findBrokenLinks(lines, (path) =>
|
|
189
227
|
existsSync(resolve(dirname(abs), path)),
|
|
190
228
|
),
|
|
@@ -201,7 +239,8 @@ async function runAudit(
|
|
|
201
239
|
reportLinks(reports, root)
|
|
202
240
|
reportColons(reports)
|
|
203
241
|
reportHeadingCase(reports)
|
|
204
|
-
|
|
242
|
+
reportDensity(reports, gates)
|
|
243
|
+
reportReferenceShape(reports, gates)
|
|
205
244
|
reportBullets(reports, checkpoints)
|
|
206
245
|
reportParagraphs(reports, checkpoints)
|
|
207
246
|
reportCadence(reports, checkpoints)
|
|
@@ -237,7 +276,8 @@ async function runAudit(
|
|
|
237
276
|
links: report.links,
|
|
238
277
|
colons: report.colons,
|
|
239
278
|
headingCase: report.headingCase,
|
|
240
|
-
|
|
279
|
+
density: report.density,
|
|
280
|
+
referenceShape: report.referenceShape,
|
|
241
281
|
longestRun: report.structure.longestRun,
|
|
242
282
|
longestRunLine: report.structure.longestRunLine,
|
|
243
283
|
heavyBullets: report.structure.heavyBullets,
|
|
@@ -260,7 +300,8 @@ async function runAudit(
|
|
|
260
300
|
structure: reports.map((report) => report.structure),
|
|
261
301
|
colons: reports.flatMap((report) => report.colons),
|
|
262
302
|
headingCase: reports.flatMap((report) => report.headingCase),
|
|
263
|
-
|
|
303
|
+
density: reports.flatMap((report) => report.density),
|
|
304
|
+
referenceShape: reports.flatMap((report) => report.referenceShape),
|
|
264
305
|
})
|
|
265
306
|
|
|
266
307
|
return gating ? EXIT_GATE : 0
|
|
@@ -414,46 +455,142 @@ function reportLinks(reports: readonly FileReport[], root: string): void {
|
|
|
414
455
|
)
|
|
415
456
|
}
|
|
416
457
|
|
|
417
|
-
|
|
418
|
-
|
|
458
|
+
const DENSITY_LABELS: Record<DensityKind, string> = {
|
|
459
|
+
'paragraph-length': 'paragraph length',
|
|
460
|
+
'paragraph-run': 'paragraphs in a row',
|
|
461
|
+
'bullet-length': 'bullet length',
|
|
462
|
+
'bullet-run': 'bullets in a row',
|
|
463
|
+
'heading-depth': 'heading depth',
|
|
464
|
+
'h3-opening': 'H3 opening',
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function describeDensity(finding: GatedDensityFinding): string {
|
|
468
|
+
const measured =
|
|
469
|
+
finding.kind === 'h3-opening'
|
|
470
|
+
? 'opens with neither a paragraph nor a table'
|
|
471
|
+
: finding.kind === 'heading-depth'
|
|
472
|
+
? `H${finding.value} past H${finding.limit}`
|
|
473
|
+
: `${finding.value} past ${finding.limit}`
|
|
474
|
+
const gate = finding.gating ? ' gating' : ''
|
|
475
|
+
return ` :${finding.line} ${DENSITY_LABELS[finding.kind]} ${measured}${gate}`
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Lists every density finding with whether it gates, since an in-scope path
|
|
480
|
+
* under no gated prefix reports the same finding a gated one fails on and a
|
|
481
|
+
* reader has to tell the two apart in one frame.
|
|
482
|
+
*/
|
|
483
|
+
function reportDensity(reports: readonly FileReport[], gates: boolean): void {
|
|
484
|
+
logStep('Density')
|
|
485
|
+
logInfo(
|
|
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
|
+
)
|
|
419
488
|
logInfo(
|
|
420
|
-
|
|
489
|
+
'Measured on context entries, docs, skill bodies and references, tooling seeds and stack references, the wiki, README.md, and CONTRIBUTING.md. Every other path takes no density finding.',
|
|
421
490
|
)
|
|
422
491
|
logInfo(
|
|
423
|
-
|
|
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.',
|
|
424
495
|
)
|
|
425
496
|
|
|
426
497
|
const carrying = reports
|
|
427
|
-
.filter((report) => report.
|
|
428
|
-
.sort((a, b) =>
|
|
498
|
+
.filter((report) => report.density.length > 0)
|
|
499
|
+
.sort((a, b) => b.density.length - a.density.length)
|
|
429
500
|
|
|
430
501
|
if (carrying.length === 0) {
|
|
431
|
-
logInfo(
|
|
502
|
+
logInfo('No density finding.')
|
|
432
503
|
return
|
|
433
504
|
}
|
|
434
505
|
|
|
435
|
-
const total = carrying.reduce(
|
|
436
|
-
|
|
506
|
+
const total = carrying.reduce((sum, report) => sum + report.density.length, 0)
|
|
507
|
+
const gating = carrying.reduce(
|
|
508
|
+
(sum, report) =>
|
|
509
|
+
sum + report.density.filter((finding) => finding.gating).length,
|
|
437
510
|
0,
|
|
438
511
|
)
|
|
439
|
-
logWarn(
|
|
512
|
+
logWarn(
|
|
513
|
+
`${plural(total, 'finding')} across ${plural(carrying.length, 'file')}, ${gating} gating`,
|
|
514
|
+
)
|
|
515
|
+
if (gating > 0) logWarn('A gating finding fails the run.')
|
|
440
516
|
logInfo(
|
|
441
|
-
'
|
|
517
|
+
'Split a long block at its seam, put a heading or a table between paragraphs, give a long list H3s, and open each H3 with a sentence or a table.',
|
|
442
518
|
)
|
|
443
519
|
pipeOutput(
|
|
444
520
|
carrying
|
|
445
521
|
.map(
|
|
446
522
|
(report) =>
|
|
447
|
-
`${report.rel} ${plural(report.
|
|
448
|
-
.map(
|
|
523
|
+
`${report.rel} ${plural(report.density.length, 'finding')}\n${report.density
|
|
524
|
+
.map(describeDensity)
|
|
449
525
|
.join('\n')}`,
|
|
450
526
|
)
|
|
451
527
|
.join('\n'),
|
|
452
528
|
)
|
|
453
529
|
}
|
|
454
530
|
|
|
455
|
-
|
|
456
|
-
|
|
531
|
+
const REFERENCE_SHAPE_LABELS: Record<ReferenceShapeKind, string> = {
|
|
532
|
+
'extra-section': 'section outside the shape',
|
|
533
|
+
'section-order': 'section out of order or repeated',
|
|
534
|
+
subsection: 'H3',
|
|
535
|
+
'flag-table': 'flag table',
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function describeReferenceShape(finding: GatedReferenceShapeFinding): string {
|
|
539
|
+
const gate = finding.gating ? ' gating' : ''
|
|
540
|
+
return ` :${finding.line} ${REFERENCE_SHAPE_LABELS[finding.kind]} ${finding.text}${gate}`
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Lists every reference shape finding with whether it gates, under the same
|
|
545
|
+
* prefixes the density step names.
|
|
546
|
+
*/
|
|
547
|
+
function reportReferenceShape(
|
|
548
|
+
reports: readonly FileReport[],
|
|
549
|
+
gates: boolean,
|
|
550
|
+
): void {
|
|
551
|
+
logStep('Reference shape')
|
|
552
|
+
logInfo(
|
|
553
|
+
`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
|
+
)
|
|
555
|
+
logInfo(
|
|
556
|
+
gates
|
|
557
|
+
? 'A finding gates under the prefixes the density step names. Every other finding reports.'
|
|
558
|
+
: 'This tree is not a canon checkout, so every reference shape finding reports and none gates.',
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
const carrying = reports.filter((report) => report.referenceShape.length > 0)
|
|
562
|
+
|
|
563
|
+
if (carrying.length === 0) {
|
|
564
|
+
logInfo('No reference shape finding.')
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const total = carrying.reduce(
|
|
569
|
+
(sum, report) => sum + report.referenceShape.length,
|
|
570
|
+
0,
|
|
571
|
+
)
|
|
572
|
+
const gating = carrying.reduce(
|
|
573
|
+
(sum, report) =>
|
|
574
|
+
sum + report.referenceShape.filter((finding) => finding.gating).length,
|
|
575
|
+
0,
|
|
576
|
+
)
|
|
577
|
+
logWarn(
|
|
578
|
+
`${plural(total, 'finding')} across ${plural(carrying.length, 'file')}, ${gating} gating`,
|
|
579
|
+
)
|
|
580
|
+
if (gating > 0) logWarn('A gating finding fails the run.')
|
|
581
|
+
logInfo(
|
|
582
|
+
"Fold an H3 into its section, move a section the shape lacks into the opening or a linked page, and point at the command's own --help for flags.",
|
|
583
|
+
)
|
|
584
|
+
pipeOutput(
|
|
585
|
+
carrying
|
|
586
|
+
.map(
|
|
587
|
+
(report) =>
|
|
588
|
+
`${report.rel} ${plural(report.referenceShape.length, 'finding')}\n${report.referenceShape
|
|
589
|
+
.map(describeReferenceShape)
|
|
590
|
+
.join('\n')}`,
|
|
591
|
+
)
|
|
592
|
+
.join('\n'),
|
|
593
|
+
)
|
|
457
594
|
}
|
|
458
595
|
|
|
459
596
|
function reportColons(reports: readonly FileReport[]): void {
|