@erclx/canon 5.37.0 → 5.39.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 +1 -1
- 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 +36 -17
- 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 +7 -4
- 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-issue/references/issue.md +11 -10
- package/claude/skills/git-pr/SKILL.md +56 -44
- 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 -52
- 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/groundwork.md +53 -24
- 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-pr/references/post.md +1 -1
- package/claude/skills/review-ui/SKILL.md +62 -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-parked.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 +41 -30
- package/claude/skills/target-setup/references/verify.md +6 -5
- package/claude/skills/task-board/SKILL.md +68 -60
- package/claude/skills/task-board/references/create-report.md +36 -0
- package/claude/skills/task-board/references/tasks.md +3 -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 +22 -18
- 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/context-audit-checks.md +19 -1
- package/docs/agents/context-audit.md +2 -2
- package/docs/agents/driver.md +1 -1
- package/docs/agents/hooks.md +21 -1
- package/docs/agents/index.md +10 -14
- package/docs/agents/indexes.md +25 -29
- package/docs/agents/install-and-sync.md +124 -235
- package/docs/agents/markdown-audit-density.md +39 -0
- package/docs/agents/markdown-audit-weight.md +9 -7
- package/docs/agents/markdown-audit.md +18 -14
- 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/routing.md +1 -1
- package/docs/agents/rule-citations.md +1 -1
- package/docs/agents/skills-reach.md +1 -1
- package/docs/agents/targets.md +25 -77
- package/docs/target/migrations.md +1 -1
- package/docs/target/projects.md +3 -3
- package/docs/target/scenarios.md +1 -1
- package/docs/workflow/ai-workflow.md +2 -2
- package/docs/workflow/parallel-features.md +2 -2
- package/docs/workflow/skill-map.md +1 -1
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/governance/detect.toml +6 -1
- package/package.json +6 -3
- package/scripts/core/install-check.sh +1 -1
- package/src/commands/claude.ts +1 -2
- package/src/commands/context.ts +106 -7
- package/src/commands/hooks.ts +63 -0
- package/src/commands/markdown.ts +169 -32
- package/src/commands/pr.ts +1 -1
- package/src/commands/records.ts +0 -1
- package/src/context/audit.ts +256 -5
- package/src/context/classify/prompts.ts +1 -1
- package/src/context/gate.ts +23 -7
- package/src/context/shape-gated.ts +28 -0
- package/src/design/document.ts +1 -1
- package/src/design/tokens.ts +1 -1
- package/src/docs/read.ts +15 -21
- package/src/gate/measures.ts +15 -190
- package/src/gate/stages.ts +21 -23
- 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 +174 -0
- package/src/markdown/structure.ts +10 -60
- package/src/sandbox/equivalence.ts +7 -9
- package/src/shipped/references.ts +1 -1
- package/src/teach/browser/head.ts +2 -2
- package/src/teach/browser/sidebar.ts +3 -3
- package/src/teach/browser/theme.ts +2 -2
- package/src/tooling/inject.ts +2 -2
- package/src/tooling/package.ts +15 -18
- package/src/tooling/verify-stack.ts +6 -3
- package/tooling/astro/configs/.oxfmtrc.json +8 -0
- package/tooling/astro/configs/.prettierrc +13 -0
- package/tooling/astro/manifest.toml +13 -0
- package/tooling/astro/reference.md +15 -5
- package/tooling/base/configs/.oxfmtrc.json +7 -0
- package/tooling/base/configs/scripts/verify.ts +1 -1
- package/tooling/base/manifest.toml +6 -6
- package/tooling/base/reference.md +12 -9
- package/tooling/base/seeds/.lintstagedrc +1 -3
- package/tooling/base/seeds/canon/context/ci.md +3 -3
- package/tooling/base/seeds/canon/context/development.md +2 -2
- package/tooling/claude/manifest.toml +5 -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 +2 -2
- package/tooling/nextjs/configs/.oxlintrc.json +223 -0
- package/tooling/nextjs/manifest.toml +2 -5
- package/tooling/nextjs/reference.md +4 -4
- package/tooling/php/manifest.toml +6 -1
- package/tooling/php/reference.md +2 -2
- package/tooling/python/manifest.toml +6 -1
- package/tooling/python/reference.md +4 -4
- package/tooling/vite-react/manifest.toml +1 -4
- package/tooling/vite-react/reference.md +3 -3
- package/tooling/web/configs/.oxlintrc.json +213 -0
- package/tooling/web/manifest.toml +9 -13
- package/tooling/web/reference.md +14 -14
- package/docs/agents/commands.md +0 -154
- package/docs/agents/docs.md +0 -27
- package/docs/agents/drift-surfaces.md +0 -201
- package/docs/agents/gov-detect.md +0 -47
- package/docs/agents/scripting.md +0 -142
- package/src/markdown/growth.ts +0 -43
- package/tooling/base/configs/.prettierrc +0 -12
- package/tooling/base/seeds/.prettierignore +0 -0
- package/tooling/nextjs/configs/eslint.config.js +0 -91
- package/tooling/web/configs/eslint.config.js +0 -82
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erclx/canon",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.39.0",
|
|
5
5
|
"description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
"access": "public"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"format": "prettier --write --log-level warn
|
|
28
|
-
"check:format": "prettier --check --log-level warn
|
|
27
|
+
"format": "oxfmt . && prettier --write --log-level warn 'web/**/*.astro' && shfmt --write --indent 2 scripts/ tooling/ claude/",
|
|
28
|
+
"check:format": "oxfmt --check . && prettier --check --log-level warn 'web/**/*.astro' && shfmt --diff --indent 2 scripts/ tooling/ claude/",
|
|
29
29
|
"check:shell": "find scripts tooling claude .claude/hooks -name '*.sh' -exec shellcheck --severity=warning {} + && find .husky -maxdepth 1 -type f -exec shellcheck --severity=warning --shell=sh {} +",
|
|
30
|
+
"check:lint": "oxlint --disable-nested-config --max-warnings 0",
|
|
30
31
|
"check:types": "tsc --noEmit",
|
|
31
32
|
"check:install": "./scripts/core/install-check.sh",
|
|
32
33
|
"test": "bun --bun vitest run",
|
|
@@ -72,6 +73,8 @@
|
|
|
72
73
|
"happy-dom": "^20.14.5",
|
|
73
74
|
"husky": "^9.1.7",
|
|
74
75
|
"micromatch": "^4.0.8",
|
|
76
|
+
"oxfmt": "0.72.0",
|
|
77
|
+
"oxlint": "1.87.0",
|
|
75
78
|
"prettier": "^3.8.1",
|
|
76
79
|
"prettier-plugin-astro": "^0.14.1",
|
|
77
80
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
@@ -101,7 +101,7 @@ log_step "Assert scaffold"
|
|
|
101
101
|
# skills rather than installing into a target. The two
|
|
102
102
|
# rule paths stand for the `claude` and `canon` groups `base` takes whole.
|
|
103
103
|
for path in "CLAUDE.md" ".claude/wiki/index.md" ".claude" "canon/context/index.md" \
|
|
104
|
-
".
|
|
104
|
+
".oxfmtrc.json" ".lintstagedrc" ".husky/pre-commit" ".github/workflows/verify.yml" "scripts/verify.ts" \
|
|
105
105
|
".claude/rules/canon/claude/output.md" ".claude/rules/canon/canon/scratch.md"; do
|
|
106
106
|
if [ ! -e "$TARGET_DIR/$path" ]; then
|
|
107
107
|
log_error "Missing after canon init: $path"
|
package/src/commands/claude.ts
CHANGED
|
@@ -70,7 +70,6 @@ import {
|
|
|
70
70
|
writeSettings,
|
|
71
71
|
} from '@/claude/settings'
|
|
72
72
|
import { copyPreservingMode } from '@/sync/copy'
|
|
73
|
-
import { execScript } from '@/cli/exec'
|
|
74
73
|
import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
|
|
75
74
|
import { recordDir } from '@/roots/record'
|
|
76
75
|
import { SURFACE_ENTRIES, surfaceDir } from '@/roots/surface'
|
|
@@ -321,7 +320,7 @@ export function register(program: Command): void {
|
|
|
321
320
|
' 1 refused, with the reason on stderr',
|
|
322
321
|
'',
|
|
323
322
|
'Reports rather than gates. Whether a bullet belongs in a rule is the',
|
|
324
|
-
'judgment canon/context/context-model
|
|
323
|
+
'judgment canon/context/context-model.md states, and naming a path is evidence for it',
|
|
325
324
|
'rather than the answer.',
|
|
326
325
|
'',
|
|
327
326
|
'Examples:',
|
package/src/commands/context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
1
|
+
import { join, relative, resolve } from 'node:path'
|
|
2
2
|
import type { Command } from 'commander'
|
|
3
3
|
import {
|
|
4
4
|
type ContextAuditRefusal,
|
|
@@ -12,14 +12,22 @@ import {
|
|
|
12
12
|
matchesSiblings,
|
|
13
13
|
measureFolders,
|
|
14
14
|
extraSections,
|
|
15
|
+
GOTCHA_CAP,
|
|
16
|
+
LAYOUT_BULLET_CAP,
|
|
17
|
+
LAYOUT_PARAGRAPH_CAP,
|
|
15
18
|
missingSections,
|
|
19
|
+
OTHER_FOLDERS,
|
|
16
20
|
PROVENANCE_FOLDER,
|
|
17
21
|
ALLOWED_SECTIONS_BY_FOLDER,
|
|
18
22
|
REQUIRED_SECTIONS,
|
|
19
23
|
REQUIRED_SECTIONS_BY_FOLDER,
|
|
20
24
|
requiredSections,
|
|
25
|
+
RETIRED_SECTIONS_BY_FOLDER,
|
|
26
|
+
shapeFindings,
|
|
21
27
|
type ExtraSectionFinding,
|
|
22
28
|
type SectionFinding,
|
|
29
|
+
type ShapeFinding,
|
|
30
|
+
type ShapeIssue,
|
|
23
31
|
} from '@/context/audit'
|
|
24
32
|
import { auditCitations, type CitationReport } from '@/context/citations'
|
|
25
33
|
import {
|
|
@@ -49,6 +57,8 @@ import {
|
|
|
49
57
|
resolveFolders,
|
|
50
58
|
} from '@/context/folders'
|
|
51
59
|
import { isGating } from '@/context/gate'
|
|
60
|
+
import { isShapeGated, SHAPE_GATED_FOLDERS } from '@/context/shape-gated'
|
|
61
|
+
import { currentWorktreeRoot } from '@/git/worktree'
|
|
52
62
|
import {
|
|
53
63
|
isOverWordCap,
|
|
54
64
|
measureRequirements,
|
|
@@ -103,7 +113,7 @@ export function register(program: Command): void {
|
|
|
103
113
|
.option('--citations-only', 'Run the gating citation check alone')
|
|
104
114
|
.option(
|
|
105
115
|
'--gate',
|
|
106
|
-
'Also fail on a missing required section or index drift, the findings that are facts',
|
|
116
|
+
'Also fail on a missing required section, an H2 outside the set, a shape break in a gated folder, or index drift, the findings that are facts',
|
|
107
117
|
)
|
|
108
118
|
.addHelpText(
|
|
109
119
|
'after',
|
|
@@ -118,10 +128,13 @@ export function register(program: Command): void {
|
|
|
118
128
|
'stating a word cap gates past it, on any run except',
|
|
119
129
|
'--citations-only, which never measures it. A record stating no',
|
|
120
130
|
'cap is reported and never gated. --gate widens the gate to the',
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
'requires Overview and Layout
|
|
124
|
-
'
|
|
131
|
+
'findings that are facts rather than judgments: a missing',
|
|
132
|
+
'required section, an H2 outside the closed set, and index drift.',
|
|
133
|
+
'A context entry requires Overview and Layout and allows Gotchas.',
|
|
134
|
+
'A shape break, such as a Decisions section or a sixth gotcha, is',
|
|
135
|
+
'reported everywhere and gated only under a shape-gated folder.',
|
|
136
|
+
'Entry length, reference form, table, and provenance are',
|
|
137
|
+
'judgments under both.',
|
|
125
138
|
'',
|
|
126
139
|
'Depth and bullet weight are stated over every markdown file rather',
|
|
127
140
|
'than over a context entry, so `canon markdown audit` measures them.',
|
|
@@ -644,6 +657,15 @@ async function runAudit(
|
|
|
644
657
|
const drift = gateOnly ? [] : await auditIndexes(folders)
|
|
645
658
|
const sections = gateOnly ? [] : missingSections(root, folders, entries)
|
|
646
659
|
const extras = gateOnly ? [] : extraSections(entries)
|
|
660
|
+
// The gated prefixes name paths from the repository root, while an entry's
|
|
661
|
+
// path runs from the audit root, which is a seed tree's own root when one is
|
|
662
|
+
// passed. Resolving here keeps a seed tree listed under its toolkit path.
|
|
663
|
+
const top = gateOnly ? root : await currentWorktreeRoot(root)
|
|
664
|
+
const shape = gateOnly
|
|
665
|
+
? []
|
|
666
|
+
: shapeFindings(entries, (rel) =>
|
|
667
|
+
isShapeGated(relative(top, join(root, rel))),
|
|
668
|
+
)
|
|
647
669
|
const length = gateOnly ? undefined : lengthFindings(entries)
|
|
648
670
|
// Absent under `--citations-only` and null when the project carries no
|
|
649
671
|
// record. A run that never looked and a project with nothing to look at are
|
|
@@ -658,6 +680,7 @@ async function runAudit(
|
|
|
658
680
|
reportCitations(citations, cited)
|
|
659
681
|
reportReferenceForm(entries, folders)
|
|
660
682
|
reportSections(sections, extras, folders)
|
|
683
|
+
reportShape(shape)
|
|
661
684
|
reportLength(length ?? [])
|
|
662
685
|
reportTables(entries)
|
|
663
686
|
reportProvenance(entries, folders)
|
|
@@ -696,6 +719,7 @@ async function runAudit(
|
|
|
696
719
|
length,
|
|
697
720
|
missingSections: sections,
|
|
698
721
|
extraSections: extras,
|
|
722
|
+
shapeFindings: shape,
|
|
699
723
|
indexDrift: drift,
|
|
700
724
|
// Null says the run opened the project and found no record, which a
|
|
701
725
|
// target that never wrote one is entitled to. Absent says the run
|
|
@@ -711,6 +735,11 @@ async function runAudit(
|
|
|
711
735
|
// caller without an error.
|
|
712
736
|
requiredSectionsByFolder: REQUIRED_SECTIONS_BY_FOLDER,
|
|
713
737
|
allowedSectionsByFolder: ALLOWED_SECTIONS_BY_FOLDER,
|
|
738
|
+
retiredSectionsByFolder: RETIRED_SECTIONS_BY_FOLDER,
|
|
739
|
+
gotchaCap: GOTCHA_CAP,
|
|
740
|
+
layoutBulletCap: LAYOUT_BULLET_CAP,
|
|
741
|
+
layoutParagraphCap: LAYOUT_PARAGRAPH_CAP,
|
|
742
|
+
shapeGatedFolders: SHAPE_GATED_FOLDERS,
|
|
714
743
|
},
|
|
715
744
|
})}\n`,
|
|
716
745
|
)
|
|
@@ -722,6 +751,7 @@ async function runAudit(
|
|
|
722
751
|
requirements !== undefined && isOverWordCap(requirements),
|
|
723
752
|
sections,
|
|
724
753
|
extraSections: extras,
|
|
754
|
+
shape,
|
|
725
755
|
drift,
|
|
726
756
|
widened,
|
|
727
757
|
})
|
|
@@ -958,7 +988,7 @@ function reportSections(
|
|
|
958
988
|
logInfo(
|
|
959
989
|
`The set is closed: ${Object.entries(ALLOWED_SECTIONS_BY_FOLDER)
|
|
960
990
|
.map(([name, set]) => `canon/${name}/ allows ${set.join(', ')}`)
|
|
961
|
-
.join('. ')} as H2.
|
|
991
|
+
.join('. ')} as H2. A folder is an H3 under Layout.`,
|
|
962
992
|
)
|
|
963
993
|
|
|
964
994
|
if (extras.length === 0) {
|
|
@@ -974,6 +1004,75 @@ function reportSections(
|
|
|
974
1004
|
)
|
|
975
1005
|
}
|
|
976
1006
|
|
|
1007
|
+
/** Characters of a paragraph the report quotes, enough to find it by. */
|
|
1008
|
+
const QUOTE_WIDTH = 60
|
|
1009
|
+
|
|
1010
|
+
function clip(text: string): string {
|
|
1011
|
+
return text.length > QUOTE_WIDTH ? `${text.slice(0, QUOTE_WIDTH)}...` : text
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/** How each shape break reads in the report. */
|
|
1015
|
+
function readIssue(issue: ShapeIssue): string {
|
|
1016
|
+
switch (issue.kind) {
|
|
1017
|
+
case 'retired-section':
|
|
1018
|
+
return `retired section: ${issue.text}`
|
|
1019
|
+
case 'gotcha-count':
|
|
1020
|
+
return `${issue.count ?? 0} gotchas, past ${GOTCHA_CAP}`
|
|
1021
|
+
case 'gotcha-heading':
|
|
1022
|
+
return `heading inside Gotchas: ${issue.text}`
|
|
1023
|
+
case 'gotcha-paragraph':
|
|
1024
|
+
return `paragraph inside Gotchas: ${clip(issue.text)}`
|
|
1025
|
+
case 'layout-heading':
|
|
1026
|
+
return `Layout H3 titled neither a path nor ${OTHER_FOLDERS}: ${issue.text}`
|
|
1027
|
+
case 'layout-bullets':
|
|
1028
|
+
return `${issue.count ?? 0} bullets under ${issue.text}, past ${LAYOUT_BULLET_CAP}`
|
|
1029
|
+
case 'layout-paragraphs':
|
|
1030
|
+
return `${issue.count ?? 0} paragraphs under ${issue.text}, past ${LAYOUT_PARAGRAPH_CAP}`
|
|
1031
|
+
case 'layout-depth':
|
|
1032
|
+
return `heading below H3 under Layout: ${issue.text}`
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Names each entry breaking the section shape, gated ones first, and states
|
|
1038
|
+
* which prefixes gate on every run, since an empty gated list and a corpus that
|
|
1039
|
+
* conforms would otherwise print the same report.
|
|
1040
|
+
*/
|
|
1041
|
+
function reportShape(shape: readonly ShapeFinding[]): void {
|
|
1042
|
+
logStep('Shape')
|
|
1043
|
+
|
|
1044
|
+
logInfo(
|
|
1045
|
+
SHAPE_GATED_FOLDERS.length === 0
|
|
1046
|
+
? 'No folder is shape-gated yet, so every shape break reports and none gates.'
|
|
1047
|
+
: `Gated under --gate: ${SHAPE_GATED_FOLDERS.join(', ')}. A break elsewhere reports and never gates.`,
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
if (shape.length === 0) {
|
|
1051
|
+
logInfo('Every entry holds the section shape.')
|
|
1052
|
+
return
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const gated = shape.filter((finding) => finding.gated)
|
|
1056
|
+
if (gated.length > 0) {
|
|
1057
|
+
logWarn(
|
|
1058
|
+
`${gated.length} gated ${gated.length === 1 ? 'entry' : 'entries'} breaking the shape`,
|
|
1059
|
+
)
|
|
1060
|
+
}
|
|
1061
|
+
logInfo(
|
|
1062
|
+
`${shape.length} ${shape.length === 1 ? 'entry' : 'entries'} breaking the shape in all`,
|
|
1063
|
+
)
|
|
1064
|
+
pipeOutput(
|
|
1065
|
+
[...gated, ...shape.filter((finding) => !finding.gated)]
|
|
1066
|
+
.map(
|
|
1067
|
+
(finding) =>
|
|
1068
|
+
`${finding.rel}${finding.gated ? ' gated' : ''}\n${finding.issues
|
|
1069
|
+
.map((issue) => ` :${issue.line} ${readIssue(issue)}`)
|
|
1070
|
+
.join('\n')}`,
|
|
1071
|
+
)
|
|
1072
|
+
.join('\n'),
|
|
1073
|
+
)
|
|
1074
|
+
}
|
|
1075
|
+
|
|
977
1076
|
/** How each question reads in the report, in the standard's own order. */
|
|
978
1077
|
const QUESTION_LABEL: Record<LengthQuestion, string> = {
|
|
979
1078
|
domain: 'one domain',
|
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/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 {
|
package/src/commands/pr.ts
CHANGED
|
@@ -1891,7 +1891,7 @@ async function mapPool<T, R>(
|
|
|
1891
1891
|
limit: number,
|
|
1892
1892
|
task: (input: T) => Promise<R>,
|
|
1893
1893
|
): Promise<R[]> {
|
|
1894
|
-
const results: R[] =
|
|
1894
|
+
const results: R[] = Array.from({ length: inputs.length })
|
|
1895
1895
|
let next = 0
|
|
1896
1896
|
const lane = async (): Promise<void> => {
|
|
1897
1897
|
while (next < inputs.length) {
|