@erclx/canon 5.34.0 → 5.36.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 +15 -17
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/REQUIREMENT.md +1 -1
- package/claude/skills/auto-ship/references/draft-mark.md +1 -1
- package/claude/skills/canon-cli/REQUIREMENT.md +2 -2
- package/claude/skills/canon-cli/SKILL.md +12 -12
- package/claude/skills/canvas-board/REQUIREMENT.md +1 -1
- package/claude/skills/canvas-board/SKILL.md +2 -2
- package/claude/skills/canvas-board/references/design.md +53 -36
- package/claude/skills/codebase-layout/REQUIREMENT.md +1 -1
- package/claude/skills/codebase-layout/SKILL.md +2 -2
- package/claude/skills/create-rule/REQUIREMENT.md +6 -8
- package/claude/skills/create-rule/SKILL.md +8 -8
- package/claude/skills/create-rule/references/rule.md +11 -23
- package/claude/skills/create-skill/SKILL.md +2 -1
- package/claude/skills/create-skill/references/skill-paths.md +12 -15
- package/{standards/standard.md → claude/skills/create-skill/references/skill-reference.md} +50 -45
- package/claude/skills/create-skill/references/skill.md +9 -3
- package/claude/skills/design-taste/REQUIREMENT.md +6 -6
- package/claude/skills/design-taste/SKILL.md +2 -2
- package/claude/skills/design-taste/references/craft.md +1 -1
- package/claude/skills/design-taste/references/preflight.md +2 -2
- package/claude/skills/design-taste/references/tells.md +1 -1
- package/claude/skills/draft-and-pick/SKILL.md +1 -1
- package/claude/skills/draft-identity/SKILL.md +3 -2
- package/claude/skills/git-commit/references/versioning.md +13 -13
- package/claude/skills/git-pr/SKILL.md +1 -1
- package/claude/skills/git-pr/references/publish.md +1 -1
- package/claude/skills/markdown-craft/references/markdown.md +3 -2
- package/claude/skills/plan-feature/SKILL.md +1 -1
- package/claude/skills/review-address/REQUIREMENT.md +1 -1
- package/claude/skills/review-address/SKILL.md +2 -2
- package/claude/skills/review-craft/references/ui.md +1 -1
- package/claude/skills/review-pr/SKILL.md +3 -3
- package/claude/skills/review-pr/references/post.md +1 -1
- package/claude/skills/review-ui/REQUIREMENT.md +1 -1
- package/claude/skills/review-ui/SKILL.md +1 -1
- package/claude/skills/role-orchestrator/REQUIREMENT.md +2 -2
- package/claude/skills/role-orchestrator/SKILL.md +11 -12
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +3 -1
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +11 -8
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +10 -4
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +11 -12
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +12 -15
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +6 -4
- package/claude/skills/role-orchestrator/scripts/poll.ts +50 -1
- package/claude/skills/role-reviewer/REQUIREMENT.md +17 -7
- package/claude/skills/role-reviewer/SKILL.md +17 -10
- package/claude/skills/role-worker/REQUIREMENT.md +1 -1
- package/claude/skills/role-worker/SKILL.md +6 -6
- package/claude/skills/session-compact/references/handoff-note.md +1 -1
- package/claude/skills/target-setup/SKILL.md +2 -2
- package/claude/skills/target-setup/references/detect.md +3 -3
- package/claude/skills/test-craft/SKILL.md +2 -2
- package/claude/skills/ux-audit/REQUIREMENT.md +1 -1
- package/claude/skills/ux-audit/SKILL.md +2 -2
- package/claude/skills/ux-measure/REQUIREMENT.md +1 -1
- package/claude/skills/write-human/references/machine-tells.md +12 -0
- package/docs/agents/audits.md +1 -1
- package/docs/agents/commands.md +14 -13
- package/docs/agents/comments.md +1 -1
- package/docs/agents/counts.md +5 -6
- package/docs/agents/drift-surfaces.md +15 -19
- package/docs/agents/gov-detect.md +1 -1
- package/docs/agents/index.md +2 -3
- package/docs/agents/install-and-sync.md +10 -34
- package/docs/agents/markdown-audit.md +22 -12
- package/docs/agents/migrate.md +1 -1
- package/docs/agents/output-shape.md +1 -1
- package/docs/agents/overview.md +2 -2
- package/docs/agents/records-stale.md +1 -1
- package/docs/agents/records-validate.md +11 -23
- package/docs/agents/records.md +1 -1
- package/docs/agents/routing.md +1 -1
- package/docs/agents/rule-citations.md +9 -17
- package/docs/agents/scripting.md +2 -20
- package/docs/agents/state-scoped-risk.md +1 -1
- package/docs/agents/tasks-validate.md +1 -1
- package/docs/agents/teach.md +1 -1
- package/docs/target/index.md +1 -0
- package/docs/target/migrations.md +4 -0
- package/docs/target/projects.md +10 -10
- package/docs/target/scenarios.md +5 -6
- package/docs/target/standards-moved.md +32 -0
- package/docs/target/sync.md +5 -5
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/operating-model.md +16 -18
- package/docs/workflow/skill-map.md +1 -1
- package/docs/workflow/visual-design-workflow.md +3 -3
- package/governance/detect.toml +32 -36
- package/governance/renames.toml +44 -1
- package/governance/rules/lib/{305-e2e-reliability.md → e2e-reliability.md} +1 -1
- package/governance/rules/lib/{306-test-scope.md → test-scope.md} +2 -2
- package/governance/rules/ui/{410-a11y.md → a11y.md} +1 -1
- package/governance/rules/ui/{440-surface-capture.md → surface-capture.md} +3 -3
- package/governance/stacks/base.toml +15 -8
- package/governance/stacks/go.toml +2 -0
- package/governance/stacks/node.toml +1 -1
- package/governance/stacks/php.toml +2 -0
- package/governance/stacks/python.toml +1 -1
- package/package.json +1 -4
- package/scripts/core/install-check.sh +4 -10
- package/src/audits/catalog.ts +0 -9
- package/src/capture/frame-values.ts +3 -6
- package/src/claude/routing.ts +1 -1
- package/src/claude/skills-reach.ts +4 -6
- package/src/cli/help-all.ts +96 -0
- package/src/cli/help.ts +29 -29
- package/src/cli.ts +8 -3
- package/src/commands/claude.ts +29 -0
- package/src/commands/design.ts +22 -1
- package/src/commands/gov.ts +29 -11
- package/src/commands/init.ts +10 -1
- package/src/commands/labels.ts +4 -44
- package/src/commands/markdown.ts +96 -3
- package/src/commands/migrate.ts +1 -1
- package/src/commands/records.ts +5 -21
- package/src/commands/sync.ts +11 -0
- package/src/commands/tooling.ts +10 -4
- package/src/counts/catalogs.ts +0 -6
- package/src/design/budget.ts +14 -10
- package/src/design/document.ts +5 -5
- package/src/design/parse.ts +21 -4
- package/src/design/render.ts +3 -3
- package/src/gate/boundaries.ts +2 -2
- package/src/gate/measures.ts +7 -29
- package/src/gate/sequencer.ts +51 -2
- package/src/gate/stages.ts +0 -18
- package/src/gov/citations.ts +16 -71
- package/src/gov/stacks.ts +3 -3
- package/src/inventory/subjects.ts +1 -1
- package/src/markdown/bans.ts +1 -1
- package/src/markdown/ceiling.ts +6 -1
- package/src/markdown/colons.ts +86 -0
- package/src/markdown/gate.ts +15 -0
- package/src/markdown/heading-case.ts +74 -0
- package/src/migrate/scratch-evidence.ts +2 -2
- package/src/records/validate.ts +6 -194
- package/src/sandbox/provision.ts +0 -5
- package/src/secrets/shipped.ts +1 -2
- package/src/serve/static.ts +1 -2
- package/src/shipped/references.ts +42 -93
- package/src/sync/check.ts +18 -14
- package/src/sync/copy.ts +4 -5
- package/src/tooling/inject.ts +6 -37
- package/src/tooling/read.ts +2 -3
- package/src/tooling/scan.ts +4 -17
- package/src/tooling/subfolder.ts +2 -66
- package/tooling/astro/manifest.toml +0 -1
- package/tooling/astro/reference.md +0 -8
- package/tooling/base/configs/scripts/verify.json +1 -1
- package/tooling/base/configs/scripts/verify.ts +22 -17
- package/tooling/base/manifest.toml +7 -7
- package/tooling/base/reference.md +19 -32
- package/tooling/base/seeds/.github/workflows/verify.yml +0 -9
- package/tooling/base/seeds/.lintstagedrc +2 -4
- package/tooling/base/seeds/canon/context/ci.md +4 -6
- package/tooling/base/seeds/canon/context/development.md +2 -2
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +13 -1
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +1 -1
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +27 -2
- package/tooling/claude/seeds/CLAUDE.md +1 -1
- package/tooling/claude/seeds/canon/DESIGN.md +24 -37
- package/tooling/go/reference.md +1 -1
- package/tooling/php/reference.md +1 -1
- package/tooling/python/reference.md +2 -2
- package/tooling/shell/configs/.github/workflows/shell.yml +30 -0
- package/tooling/shell/manifest.toml +12 -0
- package/tooling/shell/reference.md +46 -0
- package/tooling/vite-react/manifest.toml +0 -1
- package/tooling/vite-react/reference.md +0 -8
- package/tooling/web/configs/.github/workflows/readme-screenshot.yml +3 -5
- package/tooling/web/configs/.github/workflows/verify.yml +0 -9
- package/tooling/web/manifest.toml +1 -2
- package/tooling/web/reference.md +4 -6
- package/docs/agents/standards-audit.md +0 -40
- package/governance/rules/tooling/765-spelling.md +0 -19
- package/governance/stacks/astro.toml +0 -2
- package/governance/stacks/nextjs.toml +0 -2
- package/governance/stacks/node-server.toml +0 -2
- package/governance/stacks/python-fastapi.toml +0 -2
- package/governance/stacks/react.toml +0 -2
- package/src/commands/standards.ts +0 -278
- package/src/labels/spelling.ts +0 -120
- package/src/standards/audit.ts +0 -146
- package/src/standards/list.ts +0 -33
- package/src/standards/read.ts +0 -177
- package/src/standards/scope.ts +0 -56
- package/standards/index.md +0 -10
- package/tooling/base/configs/.vscode/extensions.json +0 -9
- package/tooling/base/configs/.vscode/settings.json +0 -3
- package/tooling/base/configs/scripts/verify.sh +0 -2
- package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
- package/tooling/base/seeds/.cspell/tech-stack.txt +0 -25
- package/tooling/base/seeds/cspell.json +0 -20
- package/tooling/go/seeds/.cspell/tech-stack.txt +0 -16
- package/tooling/nextjs/seeds/.cspell/tech-stack.txt +0 -3
- package/tooling/php/seeds/.cspell/tech-stack.txt +0 -5
- package/tooling/python/seeds/.cspell/tech-stack.txt +0 -18
- package/tooling/vite-react/seeds/.cspell/project-terms.txt +0 -1
- package/tooling/vite-react/seeds/.cspell/tech-stack.txt +0 -2
- package/tooling/web/configs/.vscode/extensions.json +0 -13
- package/tooling/web/configs/.vscode/settings.json +0 -10
- package/tooling/web/seeds/.cspell/tech-stack.txt +0 -21
- /package/governance/rules/canon/{606-git.md → git.md} +0 -0
- /package/governance/rules/canon/{611-indexes.md → indexes.md} +0 -0
- /package/governance/rules/canon/{604-scratch.md → scratch.md} +0 -0
- /package/governance/rules/canon/{602-tasks.md → task-scope.md} +0 -0
- /package/governance/rules/canon/{605-worktrees.md → worktrees.md} +0 -0
- /package/governance/rules/claude/{566-output.md → output.md} +0 -0
- /package/governance/rules/code/{000-code.md → routing.md} +0 -0
- /package/governance/rules/code/{010-testing.md → testing.md} +0 -0
- /package/governance/rules/framework/{210-astro.md → astro.md} +0 -0
- /package/governance/rules/framework/{220-fastapi.md → fastapi.md} +0 -0
- /package/governance/rules/framework/{230-nextjs.md → nextjs.md} +0 -0
- /package/governance/rules/framework/{200-react.md → react.md} +0 -0
- /package/governance/rules/framework/{260-shadcn.md → shadcn.md} +0 -0
- /package/governance/rules/framework/{250-tailwind.md → tailwind.md} +0 -0
- /package/governance/rules/lang/{130-go.md → go.md} +0 -0
- /package/governance/rules/lang/{140-php.md → php.md} +0 -0
- /package/governance/rules/lang/{110-python.md → python.md} +0 -0
- /package/governance/rules/lang/{100-typescript.md → typescript.md} +0 -0
- /package/governance/rules/lib/{370-database.md → database.md} +0 -0
- /package/governance/rules/lib/{340-pydantic.md → pydantic.md} +0 -0
- /package/governance/rules/lib/{360-security-server.md → security-server.md} +0 -0
- /package/governance/rules/lib/{350-security-web.md → security-web.md} +0 -0
- /package/governance/rules/lib/{320-tanstack-query.md → tanstack-query.md} +0 -0
- /package/governance/rules/lib/{335-testing-go.md → testing-go.md} +0 -0
- /package/governance/rules/lib/{336-testing-php.md → testing-php.md} +0 -0
- /package/governance/rules/lib/{330-testing-py.md → testing-py.md} +0 -0
- /package/governance/rules/lib/{300-testing-ts.md → testing-ts.md} +0 -0
- /package/governance/rules/lib/{310-zod.md → zod.md} +0 -0
- /package/governance/rules/tooling/{700-ci-workflow.md → ci-workflow.md} +0 -0
- /package/governance/rules/tooling/{790-code-comments.md → code-comments.md} +0 -0
- /package/governance/rules/tooling/{780-config-comments.md → config-comments.md} +0 -0
- /package/governance/rules/tooling/{775-dependencies.md → dependencies.md} +0 -0
- /package/governance/rules/ui/{480-canvas.md → canvas.md} +0 -0
- /package/governance/rules/ui/{400-ui.md → copy.md} +0 -0
- /package/governance/rules/ui/{460-design-taste.md → design-taste.md} +0 -0
- /package/governance/rules/ui/{420-forms.md → forms.md} +0 -0
- /package/governance/rules/ui/{465-interface-casing.md → interface-casing.md} +0 -0
- /package/governance/rules/ui/{450-link-behavior.md → link-behavior.md} +0 -0
- /package/governance/rules/ui/{470-motion.md → motion.md} +0 -0
- /package/governance/rules/ui/{430-ux-completeness.md → ux-completeness.md} +0 -0
- /package/tooling/{base → shell}/configs/.editorconfig +0 -0
- /package/tooling/{base → shell}/configs/.shellcheckrc +0 -0
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
2
|
-
import type { Command } from 'commander'
|
|
3
|
-
import {
|
|
4
|
-
auditExitCode,
|
|
5
|
-
auditStandards,
|
|
6
|
-
type StandardsAudit,
|
|
7
|
-
} from '@/standards/audit'
|
|
8
|
-
import { listStandardEntries } from '@/standards/list'
|
|
9
|
-
import { listStandards, readStandard, resolveStandard } from '@/standards/read'
|
|
10
|
-
import {
|
|
11
|
-
frameError,
|
|
12
|
-
intro,
|
|
13
|
-
logError,
|
|
14
|
-
logInfo,
|
|
15
|
-
logStep,
|
|
16
|
-
logWarn,
|
|
17
|
-
outro,
|
|
18
|
-
pipeOutput,
|
|
19
|
-
plural,
|
|
20
|
-
} from '@/cli/ui'
|
|
21
|
-
|
|
22
|
-
interface StandardsListOptions {
|
|
23
|
-
readonly json?: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface StandardsAuditOptions {
|
|
27
|
-
readonly json?: boolean
|
|
28
|
-
readonly arrivalsOnly?: boolean
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function register(program: Command): void {
|
|
32
|
-
const standards = program
|
|
33
|
-
.command('standards')
|
|
34
|
-
.description('Standards commands (list, audit, <name>)')
|
|
35
|
-
.argument('[name]', 'Standard to print, by name with or without .md')
|
|
36
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
37
|
-
.addHelpText(
|
|
38
|
-
'after',
|
|
39
|
-
[
|
|
40
|
-
'',
|
|
41
|
-
'A name resolves under standards/ at the working root, then the corpus',
|
|
42
|
-
'inside the canon package. No standard installs into a project, so the',
|
|
43
|
-
'package corpus is what answers there. The frame names the copy it read.',
|
|
44
|
-
'',
|
|
45
|
-
'Examples:',
|
|
46
|
-
' canon standards markdown',
|
|
47
|
-
' canon standards markdown.md',
|
|
48
|
-
'',
|
|
49
|
-
].join('\n'),
|
|
50
|
-
)
|
|
51
|
-
.action((name: string | undefined, _options: unknown, cmd: Command) => {
|
|
52
|
-
if (name === undefined) {
|
|
53
|
-
// Registering an action replaces commander's own no-action fallback,
|
|
54
|
-
// which writes this help to stderr. Help is UI rather than data, and
|
|
55
|
-
// the default here is stdout.
|
|
56
|
-
cmd.outputHelp({ error: true })
|
|
57
|
-
process.exitCode = 1
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
process.exitCode = print(name)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
standards
|
|
65
|
-
.command('list')
|
|
66
|
-
.description('List the standards with their descriptions and jurisdiction')
|
|
67
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
68
|
-
.option('--json', 'Emit machine-readable JSON')
|
|
69
|
-
.action((opts: StandardsListOptions) => {
|
|
70
|
-
process.exitCode = runStandardsList(opts)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
standards
|
|
74
|
-
.command('audit')
|
|
75
|
-
.description(
|
|
76
|
-
'Report the corpus against the success-criterion gate standards/standard.md states',
|
|
77
|
-
)
|
|
78
|
-
.argument('[path]', 'Project root, defaulting to the current directory')
|
|
79
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
80
|
-
.option('--json', 'Add a machine-readable record on stdout')
|
|
81
|
-
.option(
|
|
82
|
-
'--arrivals-only',
|
|
83
|
-
'Run the gating check for standards new on this branch alone',
|
|
84
|
-
)
|
|
85
|
-
.addHelpText(
|
|
86
|
-
'after',
|
|
87
|
-
[
|
|
88
|
-
'',
|
|
89
|
-
'Exit codes:',
|
|
90
|
-
' 0 the audit completed with every arriving standard carrying the section',
|
|
91
|
-
' 1 refused, with the reason on stderr',
|
|
92
|
-
' 2 a standard new to this branch carries no success-criterion section',
|
|
93
|
-
'',
|
|
94
|
-
'A standard already in the corpus without the section is a known gap',
|
|
95
|
-
'standards/standard.md names, not a violation, so only an arrival fails.',
|
|
96
|
-
'',
|
|
97
|
-
'Examples:',
|
|
98
|
-
' canon standards audit',
|
|
99
|
-
' canon standards audit --json',
|
|
100
|
-
' canon standards audit --arrivals-only',
|
|
101
|
-
'',
|
|
102
|
-
].join('\n'),
|
|
103
|
-
)
|
|
104
|
-
.action(async (path: string | undefined, opts: StandardsAuditOptions) => {
|
|
105
|
-
process.exitCode = await runStandardsAudit(path, opts)
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Reads the working root first, the way `canon standards <name>` does, so a
|
|
111
|
-
* project's own standards list beside the package corpus. The `--json` record
|
|
112
|
-
* is the one thing on stdout and the frame opens only on the text path.
|
|
113
|
-
*/
|
|
114
|
-
function runStandardsList(opts: StandardsListOptions): number {
|
|
115
|
-
const standards = listStandardEntries(process.cwd())
|
|
116
|
-
|
|
117
|
-
if (opts.json) {
|
|
118
|
-
process.stdout.write(`${JSON.stringify({ standards })}\n`)
|
|
119
|
-
return 0
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
intro('canon standards')
|
|
123
|
-
logStep('Standards')
|
|
124
|
-
for (const each of standards) logInfo(`${each.name} : ${each.description}`)
|
|
125
|
-
outro()
|
|
126
|
-
return 0
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Writes the standard to stdout and every frame line to stderr, so a caller
|
|
131
|
-
* capturing the output with `$(...)` receives the document alone.
|
|
132
|
-
*
|
|
133
|
-
* The root is the caller's directory rather than the toolkit's, since a
|
|
134
|
-
* repository that authors standards governs its own copy and the package copy
|
|
135
|
-
* answers everywhere else.
|
|
136
|
-
*/
|
|
137
|
-
function print(name: string): number {
|
|
138
|
-
intro('canon standards')
|
|
139
|
-
|
|
140
|
-
const root = process.cwd()
|
|
141
|
-
const resolved = resolveStandard(root, name)
|
|
142
|
-
|
|
143
|
-
if (!resolved) {
|
|
144
|
-
logWarn(`Unknown standard: ${name}`)
|
|
145
|
-
logStep('Available standards')
|
|
146
|
-
for (const each of listStandards(root)) logInfo(each)
|
|
147
|
-
logError("Run 'canon standards list' for descriptions.")
|
|
148
|
-
outro()
|
|
149
|
-
return 1
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
logStep(resolved.source)
|
|
153
|
-
process.stdout.write(readStandard(resolved))
|
|
154
|
-
outro()
|
|
155
|
-
return 0
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Measures the corpus at the cwd rather than the toolkit root the catalog
|
|
160
|
-
* reads, so a linked worktree audits its own branch instead of `main`.
|
|
161
|
-
*/
|
|
162
|
-
async function runStandardsAudit(
|
|
163
|
-
path: string | undefined,
|
|
164
|
-
opts: StandardsAuditOptions,
|
|
165
|
-
): Promise<number> {
|
|
166
|
-
const root = resolve(path ?? process.cwd())
|
|
167
|
-
const gateOnly = opts.arrivalsOnly ?? false
|
|
168
|
-
const audit = await auditStandards(root)
|
|
169
|
-
|
|
170
|
-
if (audit.kind === 'refused') {
|
|
171
|
-
const message =
|
|
172
|
-
audit.reason === 'no-corpus'
|
|
173
|
-
? `No standards/ under ${root}.`
|
|
174
|
-
: audit.reason === 'no-base'
|
|
175
|
-
? 'No merge base against main resolved.'
|
|
176
|
-
: 'Could not read which standards arrived on this branch.'
|
|
177
|
-
|
|
178
|
-
if (gateOnly) {
|
|
179
|
-
frameError(message)
|
|
180
|
-
} else {
|
|
181
|
-
intro('canon standards audit')
|
|
182
|
-
logStep('Refused')
|
|
183
|
-
logWarn(message)
|
|
184
|
-
outro()
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (opts.json) {
|
|
188
|
-
process.stdout.write(
|
|
189
|
-
`${JSON.stringify({ root, reason: audit.reason, message })}\n`,
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return auditExitCode(audit)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (gateOnly) {
|
|
197
|
-
reportArrivalGate(audit)
|
|
198
|
-
} else {
|
|
199
|
-
intro('canon standards audit')
|
|
200
|
-
reportCorpus(audit)
|
|
201
|
-
outro()
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (opts.json) {
|
|
205
|
-
process.stdout.write(
|
|
206
|
-
`${JSON.stringify({
|
|
207
|
-
root,
|
|
208
|
-
base: audit.base,
|
|
209
|
-
standards: audit.standards,
|
|
210
|
-
withCriterion: audit.withCriterion,
|
|
211
|
-
withoutCriterion: audit.withoutCriterion,
|
|
212
|
-
arrivals: audit.arrivals,
|
|
213
|
-
arrivalsWithoutCriterion: audit.arrivalsWithoutCriterion,
|
|
214
|
-
})}\n`,
|
|
215
|
-
)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return auditExitCode(audit)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Prints nothing when every arriving standard carries the section.
|
|
223
|
-
*
|
|
224
|
-
* `--arrivals-only` is what `verify.sh` runs on every push, and that script
|
|
225
|
-
* pipes a stage's whole output into its own frame. A passing gate that
|
|
226
|
-
* printed its frame would nest one inside the other on every contributor's
|
|
227
|
-
* push.
|
|
228
|
-
*/
|
|
229
|
-
function reportArrivalGate(
|
|
230
|
-
audit: Extract<StandardsAudit, { kind: 'measured' }>,
|
|
231
|
-
): void {
|
|
232
|
-
const missing = audit.arrivalsWithoutCriterion
|
|
233
|
-
if (missing.length === 0) return
|
|
234
|
-
|
|
235
|
-
intro('canon standards audit')
|
|
236
|
-
logError(
|
|
237
|
-
missing.length === 1
|
|
238
|
-
? '1 standard new to this branch carries no success-criterion section'
|
|
239
|
-
: `${missing.length} standards new to this branch carry no success-criterion section`,
|
|
240
|
-
)
|
|
241
|
-
pipeOutput(missing.join('\n'))
|
|
242
|
-
outro()
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function reportCorpus(
|
|
246
|
-
audit: Extract<StandardsAudit, { kind: 'measured' }>,
|
|
247
|
-
): void {
|
|
248
|
-
logStep('Corpus')
|
|
249
|
-
logInfo(`${plural(audit.standards.length, 'standard')} at standards/`)
|
|
250
|
-
logInfo(
|
|
251
|
-
`${plural(audit.withCriterion.length, 'standard')} carrying a success criterion`,
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
logStep('Known gaps')
|
|
255
|
-
if (audit.withoutCriterion.length === 0) {
|
|
256
|
-
logInfo('None. Every standard carries the section.')
|
|
257
|
-
} else {
|
|
258
|
-
pipeOutput(audit.withoutCriterion.join('\n'))
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
logStep('Arrivals since main')
|
|
262
|
-
if (audit.arrivals.length === 0) {
|
|
263
|
-
logInfo('No standard new to this branch.')
|
|
264
|
-
return
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (audit.arrivalsWithoutCriterion.length === 0) {
|
|
268
|
-
logInfo(
|
|
269
|
-
`${plural(audit.arrivals.length, 'standard')} arrived, every one carrying the section.`,
|
|
270
|
-
)
|
|
271
|
-
return
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
logError(
|
|
275
|
-
`${plural(audit.arrivalsWithoutCriterion.length, 'standard')} arrived carrying no success-criterion section`,
|
|
276
|
-
)
|
|
277
|
-
pipeOutput(audit.arrivalsWithoutCriterion.join('\n'))
|
|
278
|
-
}
|
package/src/labels/spelling.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { dirname, join, resolve } from 'node:path'
|
|
3
|
-
import { execa } from 'execa'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Why no unknown-word list was produced, which is never the same as a clean
|
|
7
|
-
* one.
|
|
8
|
-
*
|
|
9
|
-
* `no-binary` is the binary being absent anywhere from the root upward.
|
|
10
|
-
* `cspell` is a devDependency of this repository alone, per
|
|
11
|
-
* `775-dependencies.md`'s ban on importing a transitive-only package, so a
|
|
12
|
-
* target project that never adopted it gets no coverage from this check
|
|
13
|
-
* rather than a network fetch or a forced new dependency.
|
|
14
|
-
*
|
|
15
|
-
* `check-failed` is the binary running and returning neither of the two
|
|
16
|
-
* codes it defines, clean or issues found. `reject: false` is right, because
|
|
17
|
-
* a findings exit is the ordinary outcome for half of this check's runs, but
|
|
18
|
-
* discarding the code outright folded a crash, a bad config, or an
|
|
19
|
-
* unreadable dictionary into the same empty-stdout shape a clean title
|
|
20
|
-
* produces. Reading it the way `spawnAudit` in `src/audits/run.ts` reads its
|
|
21
|
-
* own child processes is what tells the two apart.
|
|
22
|
-
*/
|
|
23
|
-
export type SpellingRefusal = 'no-binary' | 'check-failed'
|
|
24
|
-
|
|
25
|
-
export type SpellingScan =
|
|
26
|
-
| { readonly kind: 'checked'; readonly unknownWords: readonly string[] }
|
|
27
|
-
| {
|
|
28
|
-
readonly kind: 'unavailable'
|
|
29
|
-
readonly reason: 'no-binary'
|
|
30
|
-
readonly probedFrom: string
|
|
31
|
-
}
|
|
32
|
-
| {
|
|
33
|
-
readonly kind: 'unavailable'
|
|
34
|
-
readonly reason: 'check-failed'
|
|
35
|
-
readonly message: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const CSPELL_EXIT_CLEAN = 0
|
|
39
|
-
const CSPELL_EXIT_ISSUES_FOUND = 1
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Reads `cspell stdin --words-only --unique`'s stdout into a clean list.
|
|
43
|
-
*
|
|
44
|
-
* `--unique` already dedupes on cspell's side, so this exists for the same
|
|
45
|
-
* reason `parseAdvisories` exists beside `auditDependencies`: a pure function
|
|
46
|
-
* over a fixture string is a unit test that needs no binary on the machine
|
|
47
|
-
* running it, where the trailing blank line an empty scan's stdout carries
|
|
48
|
-
* is what actually needs the trim-and-filter below.
|
|
49
|
-
*/
|
|
50
|
-
export function parseUnknownWords(stdout: string): string[] {
|
|
51
|
-
return stdout
|
|
52
|
-
.split(/\r?\n/)
|
|
53
|
-
.map((word) => word.trim())
|
|
54
|
-
.filter((word) => word.length > 0)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Walks from `root` up to the filesystem root looking for a resolved
|
|
59
|
-
* `cspell` binary, stopping at the first `node_modules/.bin/cspell` found.
|
|
60
|
-
*
|
|
61
|
-
* A caller running `canon labels scan` from a subdirectory of the checkout,
|
|
62
|
-
* rather than from its own root, is what a single `join(root, …)` check
|
|
63
|
-
* missed: `node_modules` sits at the checkout root, so the check found
|
|
64
|
-
* nothing, the title went unchecked, and the command exited 0 with no path
|
|
65
|
-
* naming what was probed or where it stopped looking.
|
|
66
|
-
*/
|
|
67
|
-
function resolveCspellBinary(root: string): string | undefined {
|
|
68
|
-
let dir = resolve(root)
|
|
69
|
-
|
|
70
|
-
while (true) {
|
|
71
|
-
const candidate = join(dir, 'node_modules', '.bin', 'cspell')
|
|
72
|
-
if (existsSync(candidate)) return candidate
|
|
73
|
-
|
|
74
|
-
const parent = dirname(dir)
|
|
75
|
-
if (parent === dir) return undefined
|
|
76
|
-
dir = parent
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Shells this repository's own resolved `cspell` binary against a title over
|
|
82
|
-
* stdin, rather than a bare `execa('cspell', …)`.
|
|
83
|
-
*
|
|
84
|
-
* `bun src/cli.ts labels scan` is the exact invocation `phase-label-gate.yml`
|
|
85
|
-
* uses, and it runs outside `bun run`, so `node_modules/.bin` is not on
|
|
86
|
-
* `PATH` and a bare spawn throws `ENOENT`. Resolving the binary path directly
|
|
87
|
-
* under `root`, or an ancestor of it, is what a caller running from this
|
|
88
|
-
* checkout needs, and it is also what keeps the check from reaching a
|
|
89
|
-
* `bunx` fallback that could fetch `cspell` from the network on a machine
|
|
90
|
-
* that never asked for it.
|
|
91
|
-
*/
|
|
92
|
-
export async function scanTitleSpelling(
|
|
93
|
-
title: string,
|
|
94
|
-
root: string,
|
|
95
|
-
): Promise<SpellingScan> {
|
|
96
|
-
const binary = resolveCspellBinary(root)
|
|
97
|
-
|
|
98
|
-
if (binary === undefined) {
|
|
99
|
-
return { kind: 'unavailable', reason: 'no-binary', probedFrom: root }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const result = await execa(
|
|
103
|
-
binary,
|
|
104
|
-
['stdin', '--words-only', '--unique', '--no-progress', '--no-summary'],
|
|
105
|
-
{ cwd: root, input: title, reject: false },
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
result.exitCode !== CSPELL_EXIT_CLEAN &&
|
|
110
|
-
result.exitCode !== CSPELL_EXIT_ISSUES_FOUND
|
|
111
|
-
) {
|
|
112
|
-
return {
|
|
113
|
-
kind: 'unavailable',
|
|
114
|
-
reason: 'check-failed',
|
|
115
|
-
message: result.stderr.trim().split('\n').pop() || 'no output on stderr',
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return { kind: 'checked', unknownWords: parseUnknownWords(result.stdout) }
|
|
120
|
-
}
|
package/src/standards/audit.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
2
|
-
import { basename, join } from 'node:path'
|
|
3
|
-
import { $ } from 'bun'
|
|
4
|
-
import { gitEnv } from '@/git/env'
|
|
5
|
-
import { resolveBaseRef } from '@/git/files'
|
|
6
|
-
import { INDEX_FILE, standardsSourceDir } from '@/standards/read'
|
|
7
|
-
|
|
8
|
-
/** Returned when a standard new to this branch states no success criterion under either heading `CRITERION_HEADING` accepts, the gating check. */
|
|
9
|
-
export const EXIT_MISSING_CRITERION = 2
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Matched at any casing, level-2 only, in either heading the corpus writes a
|
|
13
|
-
* criterion under.
|
|
14
|
-
*
|
|
15
|
-
* `standards/standard.md` states the rule under `## Success criterion` and
|
|
16
|
-
* templates the section itself as `## What a working <document type> looks
|
|
17
|
-
* like`, and the template wins where the two disagree, which that file says in
|
|
18
|
-
* as many words. Reading the rule group's own heading as the one to match is
|
|
19
|
-
* what the first spelling here did, and no standard in the corpus carries a
|
|
20
|
-
* criterion under it: 12 of 28 use the templated form and `standard.md` alone
|
|
21
|
-
* carries the literal words, over its rules about criteria rather than over its
|
|
22
|
-
* own. Nothing caught that, because the gate reads a standard new since the
|
|
23
|
-
* merge base and none had arrived since it shipped.
|
|
24
|
-
*/
|
|
25
|
-
const CRITERION_HEADING =
|
|
26
|
-
/^##\s+(?:success criterion|what a working .+ looks like)\s*$/im
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The reasons an audit produces no reading. `no-corpus` is the ordinary state
|
|
30
|
-
* of a target that authors no standards of its own, the same absence the
|
|
31
|
-
* skills audit reads as its own `no-corpus`. The other two are a broken git
|
|
32
|
-
* invocation rather than a project stating nothing.
|
|
33
|
-
*/
|
|
34
|
-
export type StandardsAuditRefusal =
|
|
35
|
-
| 'no-corpus'
|
|
36
|
-
| 'no-base'
|
|
37
|
-
| 'unreadable-arrivals'
|
|
38
|
-
|
|
39
|
-
export type StandardsAudit =
|
|
40
|
-
| {
|
|
41
|
-
readonly kind: 'measured'
|
|
42
|
-
readonly base: string
|
|
43
|
-
readonly standards: readonly string[]
|
|
44
|
-
readonly withCriterion: readonly string[]
|
|
45
|
-
readonly withoutCriterion: readonly string[]
|
|
46
|
-
readonly arrivals: readonly string[]
|
|
47
|
-
readonly arrivalsWithoutCriterion: readonly string[]
|
|
48
|
-
}
|
|
49
|
-
| { readonly kind: 'refused'; readonly reason: StandardsAuditRefusal }
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Measures the corpus authored at `standards/` under `root` against the
|
|
53
|
-
* `## Success criterion` gate `standards/standard.md` states, and names which
|
|
54
|
-
* of those files are new since the branch's merge base.
|
|
55
|
-
*
|
|
56
|
-
* Reads the working-root corpus alone, never the packaged fallback
|
|
57
|
-
* `src/standards/read.ts` falls through to for a name lookup, since a target
|
|
58
|
-
* with no authored standards of its own has nothing here to gate.
|
|
59
|
-
*/
|
|
60
|
-
export async function auditStandards(root: string): Promise<StandardsAudit> {
|
|
61
|
-
const dir = standardsSourceDir(root)
|
|
62
|
-
if (!existsSync(dir)) return { kind: 'refused', reason: 'no-corpus' }
|
|
63
|
-
|
|
64
|
-
const standards = readdirSync(dir, { withFileTypes: true })
|
|
65
|
-
.filter(
|
|
66
|
-
(entry) =>
|
|
67
|
-
entry.isFile() &&
|
|
68
|
-
entry.name.endsWith('.md') &&
|
|
69
|
-
entry.name !== INDEX_FILE,
|
|
70
|
-
)
|
|
71
|
-
.map((entry) => entry.name)
|
|
72
|
-
.sort()
|
|
73
|
-
|
|
74
|
-
const withCriterion: string[] = []
|
|
75
|
-
const withoutCriterion: string[] = []
|
|
76
|
-
for (const name of standards) {
|
|
77
|
-
const body = readFileSync(join(dir, name), 'utf8')
|
|
78
|
-
;(CRITERION_HEADING.test(body) ? withCriterion : withoutCriterion).push(
|
|
79
|
-
name,
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const base = await resolveBaseRef(root)
|
|
84
|
-
if (base === undefined) return { kind: 'refused', reason: 'no-base' }
|
|
85
|
-
|
|
86
|
-
const arrived = await arrivedStandards(root, base)
|
|
87
|
-
if (arrived === undefined) {
|
|
88
|
-
return { kind: 'refused', reason: 'unreadable-arrivals' }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const arrivals = standards.filter((name) => arrived.has(name))
|
|
92
|
-
const arrivalsWithoutCriterion = arrivals.filter((name) =>
|
|
93
|
-
withoutCriterion.includes(name),
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
kind: 'measured',
|
|
98
|
-
base,
|
|
99
|
-
standards,
|
|
100
|
-
withCriterion,
|
|
101
|
-
withoutCriterion,
|
|
102
|
-
arrivals,
|
|
103
|
-
arrivalsWithoutCriterion,
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Only an arrival missing the section sets a failing code. Every other
|
|
109
|
-
* standard without one is a known gap `standards/standard.md` names rather
|
|
110
|
-
* than a violation, so failing the push on the 26 already there teaches
|
|
111
|
-
* contributors to route around the stage.
|
|
112
|
-
*/
|
|
113
|
-
export function auditExitCode(audit: StandardsAudit): number {
|
|
114
|
-
if (audit.kind === 'refused') return 1
|
|
115
|
-
return audit.arrivalsWithoutCriterion.length > 0 ? EXIT_MISSING_CRITERION : 0
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Filenames under `standards/` present in the working tree and absent at
|
|
120
|
-
* `base`: a plain add, with rename detection forced off so a standard moved
|
|
121
|
-
* into the corpus from elsewhere counts the same as one authored fresh.
|
|
122
|
-
*/
|
|
123
|
-
async function arrivedStandards(
|
|
124
|
-
root: string,
|
|
125
|
-
base: string,
|
|
126
|
-
): Promise<Set<string> | undefined> {
|
|
127
|
-
const [added, untracked] = await Promise.all([
|
|
128
|
-
$`git -C ${root} diff --no-renames --name-only --diff-filter=A ${base} -- standards`
|
|
129
|
-
.env(gitEnv())
|
|
130
|
-
.quiet()
|
|
131
|
-
.nothrow(),
|
|
132
|
-
$`git -C ${root} ls-files --others --exclude-standard -- standards`
|
|
133
|
-
.env(gitEnv())
|
|
134
|
-
.quiet()
|
|
135
|
-
.nothrow(),
|
|
136
|
-
])
|
|
137
|
-
|
|
138
|
-
if (added.exitCode !== 0 || untracked.exitCode !== 0) return undefined
|
|
139
|
-
|
|
140
|
-
const paths = [
|
|
141
|
-
...added.text().split('\n'),
|
|
142
|
-
...untracked.text().split('\n'),
|
|
143
|
-
].filter(Boolean)
|
|
144
|
-
|
|
145
|
-
return new Set(paths.map((path) => basename(path)))
|
|
146
|
-
}
|
package/src/standards/list.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
3
|
-
import { resolveAllStandards } from '@/standards/read'
|
|
4
|
-
import { appliesTo } from '@/standards/scope'
|
|
5
|
-
|
|
6
|
-
export interface StandardEntry {
|
|
7
|
-
readonly name: string
|
|
8
|
-
readonly description: string
|
|
9
|
-
readonly appliesTo: string[]
|
|
10
|
-
/** The whole file, frontmatter included. */
|
|
11
|
-
readonly content: string
|
|
12
|
-
/** The root-labeled path `canon standards <name>` would read. */
|
|
13
|
-
readonly source: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Lists every standard a resolve reaches, the first root winning, so a project
|
|
18
|
-
* standard shows beside the toolkit's and a local override of a package name
|
|
19
|
-
* shows once, as the local copy.
|
|
20
|
-
*/
|
|
21
|
-
export function listStandardEntries(root: string): StandardEntry[] {
|
|
22
|
-
return resolveAllStandards(root).map(({ name, path, source }) => {
|
|
23
|
-
const content = readFileSync(path, 'utf8')
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
name,
|
|
27
|
-
description: readField(parseFrontmatter(content), 'description') ?? '',
|
|
28
|
-
appliesTo: appliesTo(content),
|
|
29
|
-
content,
|
|
30
|
-
source,
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
}
|