@erclx/canon 5.44.0 → 5.46.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/REQUIREMENT.md +15 -31
- package/claude/skills/auto-ship/REQUIREMENT.md +14 -25
- package/claude/skills/backlog-triage/REQUIREMENT.md +12 -23
- package/claude/skills/build-in-slices/REQUIREMENT.md +15 -25
- package/claude/skills/canon-cli/REQUIREMENT.md +15 -27
- package/claude/skills/canon-cli/SKILL.md +0 -1
- package/claude/skills/canon-feedback/REQUIREMENT.md +10 -18
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +10 -15
- package/claude/skills/canvas-board/REQUIREMENT.md +14 -28
- package/claude/skills/ci-workflow/REQUIREMENT.md +12 -21
- package/claude/skills/code-craft/REQUIREMENT.md +14 -28
- package/claude/skills/codebase-layout/REQUIREMENT.md +14 -31
- package/claude/skills/context-fold/REQUIREMENT.md +15 -39
- package/claude/skills/context-fold/references/context.md +3 -3
- package/claude/skills/create-rule/REQUIREMENT.md +10 -24
- package/claude/skills/create-rule/SKILL.md +3 -0
- package/claude/skills/create-skill/REQUIREMENT.md +10 -21
- package/claude/skills/decision-escalate/REQUIREMENT.md +12 -24
- package/claude/skills/deploy-app/REQUIREMENT.md +12 -22
- package/claude/skills/deprecation-migration/REQUIREMENT.md +14 -42
- package/claude/skills/design-identity/REQUIREMENT.md +14 -20
- package/claude/skills/design-taste/REQUIREMENT.md +16 -57
- package/claude/skills/docs-sync/REQUIREMENT.md +14 -23
- package/claude/skills/document-health/REQUIREMENT.md +15 -29
- package/claude/skills/document-health/SKILL.md +2 -2
- package/claude/skills/document-repair/REQUIREMENT.md +32 -0
- package/claude/skills/document-repair/SKILL.md +76 -0
- package/claude/skills/document-repair/references/batches.md +44 -0
- package/claude/skills/draft-and-pick/REQUIREMENT.md +4 -3
- package/claude/skills/draft-doc/REQUIREMENT.md +4 -3
- package/claude/skills/draft-figure/REQUIREMENT.md +4 -3
- package/claude/skills/draft-identity/REQUIREMENT.md +4 -3
- package/claude/skills/draft-ready/REQUIREMENT.md +4 -3
- package/claude/skills/draft-screencast/REQUIREMENT.md +4 -3
- package/claude/skills/draft-slides/REQUIREMENT.md +4 -3
- package/claude/skills/draw-figure/REQUIREMENT.md +15 -39
- package/claude/skills/git-branch/REQUIREMENT.md +12 -26
- package/claude/skills/git-commit/REQUIREMENT.md +11 -15
- package/claude/skills/git-followup/REQUIREMENT.md +11 -22
- package/claude/skills/git-issue/REQUIREMENT.md +9 -15
- package/claude/skills/git-pr/REQUIREMENT.md +13 -32
- package/claude/skills/git-pr/SKILL.md +10 -2
- package/claude/skills/git-pr/references/closing-issues.md +47 -0
- package/claude/skills/git-pr/references/pr.md +4 -0
- package/claude/skills/git-ship/REQUIREMENT.md +13 -26
- package/claude/skills/git-split/REQUIREMENT.md +10 -17
- package/claude/skills/git-stage/REQUIREMENT.md +12 -18
- package/claude/skills/git-worktree/REQUIREMENT.md +15 -19
- package/claude/skills/make-deck/REQUIREMENT.md +13 -21
- package/claude/skills/markdown-craft/REQUIREMENT.md +9 -3
- package/claude/skills/pick-by-eye/REQUIREMENT.md +17 -67
- package/claude/skills/plan-feature/REQUIREMENT.md +15 -24
- package/claude/skills/plan-groundwork/REQUIREMENT.md +12 -28
- package/claude/skills/plan-groundwork/SKILL.md +5 -2
- package/claude/skills/plan-groundwork/references/output.md +47 -1
- package/claude/skills/plan-intake/REQUIREMENT.md +11 -26
- package/claude/skills/plan-intake/references/intake.md +2 -2
- package/claude/skills/plan-intake-answer/REQUIREMENT.md +14 -29
- package/claude/skills/plan-intake-answer/SKILL.md +7 -1
- package/claude/skills/plan-ready/REQUIREMENT.md +17 -36
- package/claude/skills/pr-evidence/REQUIREMENT.md +11 -17
- package/claude/skills/read-frames/REQUIREMENT.md +3 -8
- package/claude/skills/record-screencast/REQUIREMENT.md +14 -17
- package/claude/skills/review-address/REQUIREMENT.md +14 -38
- package/claude/skills/review-craft/REQUIREMENT.md +14 -32
- package/claude/skills/review-pr/REQUIREMENT.md +15 -34
- package/claude/skills/review-ui/REQUIREMENT.md +14 -39
- package/claude/skills/role-orchestrator/REQUIREMENT.md +16 -99
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +5 -0
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +9 -0
- package/claude/skills/role-planner/REQUIREMENT.md +15 -73
- package/claude/skills/role-reviewer/REQUIREMENT.md +15 -65
- package/claude/skills/role-reviewer/SKILL.md +5 -1
- package/claude/skills/role-worker/REQUIREMENT.md +13 -41
- package/claude/skills/role-worker/SKILL.md +2 -0
- package/claude/skills/script-screencast/REQUIREMENT.md +10 -16
- package/claude/skills/search-craft/REQUIREMENT.md +12 -36
- package/claude/skills/search-craft/SKILL.md +1 -1
- package/claude/skills/search-craft/references/adopted.md +2 -2
- package/claude/skills/session-compact/REQUIREMENT.md +13 -23
- package/claude/skills/session-map/REQUIREMENT.md +13 -29
- package/claude/skills/session-relay/REQUIREMENT.md +14 -58
- package/claude/skills/session-relay/SKILL.md +6 -0
- package/claude/skills/session-resume/REQUIREMENT.md +13 -27
- package/claude/skills/session-worktree/REQUIREMENT.md +14 -43
- package/claude/skills/systematic-debugging/REQUIREMENT.md +13 -34
- package/claude/skills/target-check/REQUIREMENT.md +16 -23
- package/claude/skills/target-setup/REQUIREMENT.md +11 -34
- package/claude/skills/target-setup/SKILL.md +2 -2
- package/claude/skills/target-setup/references/detect.md +0 -1
- package/claude/skills/target-setup/references/verify.md +2 -2
- package/claude/skills/task-board/REQUIREMENT.md +13 -22
- package/claude/skills/task-board/SKILL.md +7 -0
- package/claude/skills/task-board/references/tasks.md +1 -1
- package/claude/skills/task-complete/REQUIREMENT.md +11 -24
- package/claude/skills/task-complete/references/archive.md +1 -0
- package/claude/skills/teach-workspace/REQUIREMENT.md +14 -45
- package/claude/skills/teach-workspace/references/promotion.md +5 -5
- package/claude/skills/test-craft/REQUIREMENT.md +15 -46
- package/claude/skills/test-first/REQUIREMENT.md +14 -20
- package/claude/skills/ui-checklist/REQUIREMENT.md +14 -29
- package/claude/skills/ux-audit/REQUIREMENT.md +3 -7
- package/claude/skills/ux-measure/REQUIREMENT.md +14 -28
- package/claude/skills/video-craft/REQUIREMENT.md +14 -27
- package/claude/skills/write-doc/REQUIREMENT.md +14 -24
- package/claude/skills/write-human/REQUIREMENT.md +15 -27
- package/claude/skills/youtube-transcripts/REQUIREMENT.md +9 -15
- package/docs/agents/context-audit.md +19 -12
- package/docs/agents/install-and-sync.md +2 -2
- package/docs/agents/markdown-audit.md +15 -10
- package/docs/agents/output-shape.md +1 -2
- package/docs/target/migrations.md +18 -0
- package/docs/target/projects.md +3 -2
- package/docs/target/scenarios.md +4 -1
- package/docs/workflow/skill-map-on-request.md +1 -1
- package/docs/workflow/skill-map.md +6 -5
- package/governance/detect.toml +4 -0
- package/governance/ledger/lang/go.md +61 -0
- package/governance/ledger/lang/php.md +50 -0
- package/governance/ledger/lang/python.md +68 -0
- package/governance/ledger/lang/typescript.md +64 -0
- package/governance/ledger/lib/database.md +65 -0
- package/governance/ledger/lib/e2e-reliability.md +57 -0
- package/governance/ledger/lib/pydantic.md +61 -0
- package/governance/ledger/lib/security-server.md +62 -0
- package/governance/ledger/lib/security-web.md +51 -0
- package/governance/ledger/lib/tanstack-query.md +57 -0
- package/governance/ledger/lib/test-scope.md +43 -0
- package/governance/ledger/lib/testing-go.md +57 -0
- package/governance/ledger/lib/testing-php.md +51 -0
- package/governance/ledger/lib/testing-py.md +67 -0
- package/governance/ledger/lib/testing-ts.md +63 -0
- package/governance/ledger/lib/zod.md +47 -0
- package/governance/ledger/tooling/ci-workflow.md +33 -0
- package/governance/ledger/tooling/code-comments.md +48 -0
- package/governance/ledger/tooling/config-comments.md +27 -0
- package/governance/ledger/tooling/dependencies.md +38 -0
- package/governance/ledger/ui/a11y.md +55 -0
- package/governance/ledger/ui/copy.md +49 -0
- package/governance/ledger/ui/design-reference.md +42 -0
- package/governance/ledger/ui/design-taste.md +19 -0
- package/governance/ledger/ui/forms.md +71 -0
- package/governance/ledger/ui/interface-casing.md +47 -0
- package/governance/ledger/ui/keyboard.md +68 -0
- package/governance/ledger/ui/link-behavior.md +29 -0
- package/governance/ledger/ui/motion.md +33 -0
- package/governance/ledger/ui/surface-capture.md +69 -0
- package/governance/ledger/ui/ux-completeness.md +79 -0
- package/governance/renames.toml +0 -2
- package/governance/rules/claude/output.md +5 -1
- package/governance/rules/code/routing.md +5 -1
- package/governance/rules/lang/go.md +7 -16
- package/governance/rules/lang/php.md +3 -3
- package/governance/rules/lang/python.md +9 -18
- package/governance/rules/lang/typescript.md +9 -19
- package/governance/rules/lib/database.md +5 -5
- package/governance/rules/lib/e2e-reliability.md +4 -6
- package/governance/rules/lib/pydantic.md +14 -16
- package/governance/rules/lib/security-server.md +4 -4
- package/governance/rules/lib/security-web.md +6 -10
- package/governance/rules/lib/tanstack-query.md +10 -12
- package/governance/rules/lib/test-scope.md +4 -4
- package/governance/rules/lib/testing-go.md +1 -2
- package/governance/rules/lib/testing-php.md +4 -5
- package/governance/rules/lib/testing-py.md +11 -19
- package/governance/rules/lib/testing-ts.md +7 -16
- package/governance/rules/lib/zod.md +6 -8
- package/governance/rules/tooling/code-comments.md +2 -4
- package/governance/rules/tooling/config-comments.md +1 -4
- package/governance/rules/tooling/dependencies.md +1 -1
- package/governance/rules/ui/a11y.md +16 -45
- package/governance/rules/ui/copy.md +6 -9
- package/governance/rules/ui/forms.md +13 -19
- package/governance/rules/ui/interface-casing.md +5 -7
- package/governance/rules/ui/keyboard.md +38 -0
- package/governance/rules/ui/link-behavior.md +3 -3
- package/governance/rules/ui/motion.md +3 -3
- package/governance/rules/ui/surface-capture.md +15 -27
- package/governance/rules/ui/ux-completeness.md +16 -39
- package/governance/stacks/base.toml +0 -1
- package/package.json +1 -1
- package/scripts/core/install-check.sh +1 -1
- package/src/claude/cases/workflow.ts +12 -0
- package/src/cli/help.ts +0 -1
- package/src/cli.ts +0 -2
- package/src/commands/context.ts +57 -3
- package/src/commands/gov.ts +4 -0
- package/src/commands/init.ts +2 -2
- package/src/commands/labels.ts +83 -3
- package/src/commands/markdown.ts +188 -21
- package/src/commands/sessions.ts +36 -3
- package/src/commands/tasks.ts +180 -0
- package/src/commands/tooling.ts +18 -5
- package/src/commands/worktrees.ts +2 -0
- package/src/context/coverage.ts +210 -0
- package/src/context/gate.ts +12 -3
- package/src/design/components.ts +1 -1
- package/src/gate/stages.ts +1 -1
- package/src/gov/audit.ts +22 -2
- package/src/init/plan.ts +1 -8
- package/src/init/steps.ts +44 -7
- package/src/intake/items.ts +14 -0
- package/src/inventory/subjects.ts +1 -1
- package/src/labels/closing.ts +46 -0
- package/src/markdown/gated.ts +250 -0
- package/src/markdown/scope.ts +119 -2
- package/src/migrate/rename.ts +6 -0
- package/src/migrate/scratch-evidence.ts +12 -4
- package/src/records/backup.ts +3 -3
- package/src/records/validate.ts +3 -3
- package/src/roots/record.ts +17 -8
- package/src/sessions/claim.ts +7 -0
- package/src/sessions/live.ts +53 -6
- package/src/sessions/registry.ts +11 -0
- package/src/sessions/resolve.ts +32 -5
- package/src/tasks/closes.ts +136 -0
- package/src/tasks/record.ts +1 -1
- package/src/tooling/inject.ts +26 -7
- package/src/tooling/package.ts +32 -0
- package/src/tooling/read.ts +46 -3
- package/src/tooling/scan.ts +8 -2
- package/src/worktrees/reclaim.ts +16 -1
- package/tooling/astro/reference.md +64 -93
- package/tooling/base/configs/.github/workflows/phase-label-gate.yml +1 -1
- package/tooling/base/configs/scripts/verify.ts +2 -2
- package/tooling/base/reference.md +1 -9
- package/tooling/base/seeds/canon/context/{ci.md → development/ci.md} +1 -1
- package/tooling/base/seeds/canon/context/development/index.md +11 -0
- package/tooling/base/seeds/canon/context/{development.md → development/setup.md} +8 -12
- package/tooling/base/seeds/canon/context/index.md +1 -2
- package/tooling/claude/manifest.toml +1 -5
- package/tooling/claude/reference.md +36 -59
- package/tooling/claude/seeds/.claude/hooks/board-validate.sh +1 -1
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +2 -7
- package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +2 -7
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +3 -10
- package/tooling/claude/seeds/.claude/hooks/search-reminder.sh +2 -7
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +2 -4
- package/tooling/claude/seeds/CLAUDE.md +2 -2
- package/tooling/nextjs/reference.md +0 -6
- package/tooling/php/reference.md +0 -4
- package/tooling/web/configs/.github/workflows/verify.yml +28 -0
- package/tooling/web/configs/.oxlintrc.json +7 -0
- package/tooling/web/configs/e2e/capture-types.ts +35 -0
- package/tooling/web/configs/e2e/screenshot.ts +23 -72
- package/tooling/web/manifest.toml +1 -1
- package/tooling/web/reference.md +77 -124
- package/tooling/web/seeds/e2e/capture/cases.ts +47 -0
- package/governance/rules/canon/task-scope.md +0 -12
- package/src/commands/wiki.ts +0 -100
- package/src/wiki/init.ts +0 -85
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
2
|
+
import { join, relative } from 'node:path'
|
|
3
|
+
import type { AuditedFolder } from '@/context/folders'
|
|
4
|
+
|
|
5
|
+
/** Where a project declares the code roots its context entries must cover. */
|
|
6
|
+
export const CONTEXT_CONFIG_REL = join('canon', 'config', 'context.toml')
|
|
7
|
+
|
|
8
|
+
/** The root assumed when a project declares none and carries the folder. */
|
|
9
|
+
export const DEFAULT_ROOT = 'src'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The roots a project asked the coverage check to measure.
|
|
13
|
+
*
|
|
14
|
+
* `none` says the project carries no declaration and no `src/`, which reports
|
|
15
|
+
* and never gates so a seed tree audited at its own root keeps passing.
|
|
16
|
+
* `opted-out` is an explicit `roots = []`. `invalid` is a declaration that
|
|
17
|
+
* exists and cannot be used, which gates rather than falling back to `src`,
|
|
18
|
+
* since a project that wrote one meant it.
|
|
19
|
+
*/
|
|
20
|
+
export type RootDeclaration =
|
|
21
|
+
| { readonly kind: 'roots'; readonly patterns: readonly string[] }
|
|
22
|
+
| { readonly kind: 'opted-out' }
|
|
23
|
+
| { readonly kind: 'none' }
|
|
24
|
+
| { readonly kind: 'invalid'; readonly reason: string }
|
|
25
|
+
|
|
26
|
+
export interface DoubleClaim {
|
|
27
|
+
readonly folder: string
|
|
28
|
+
readonly rels: readonly string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CoverageFinding {
|
|
32
|
+
/** Folders under a declared root that no entry's Layout names. */
|
|
33
|
+
readonly unnamed: readonly string[]
|
|
34
|
+
/** Folders taking a Layout H3 in more than one entry. */
|
|
35
|
+
readonly doubled: readonly DoubleClaim[]
|
|
36
|
+
/** Declared roots or globs that matched no folder on disk. */
|
|
37
|
+
readonly missingRoots: readonly string[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type CoverageReport =
|
|
41
|
+
| {
|
|
42
|
+
readonly kind: 'measured'
|
|
43
|
+
readonly roots: readonly string[]
|
|
44
|
+
readonly findings: CoverageFinding
|
|
45
|
+
}
|
|
46
|
+
| { readonly kind: 'opted-out' }
|
|
47
|
+
| { readonly kind: 'no-root' }
|
|
48
|
+
| { readonly kind: 'invalid'; readonly reason: string }
|
|
49
|
+
|
|
50
|
+
/** Reads the declaration, falling back to `src` when that folder exists. */
|
|
51
|
+
export function readRoots(root: string): RootDeclaration {
|
|
52
|
+
const path = join(root, CONTEXT_CONFIG_REL)
|
|
53
|
+
|
|
54
|
+
if (existsSync(path)) {
|
|
55
|
+
let parsed: Record<string, unknown>
|
|
56
|
+
try {
|
|
57
|
+
parsed = Bun.TOML.parse(readFileSync(path, 'utf8')) as Record<
|
|
58
|
+
string,
|
|
59
|
+
unknown
|
|
60
|
+
>
|
|
61
|
+
} catch {
|
|
62
|
+
return {
|
|
63
|
+
kind: 'invalid',
|
|
64
|
+
reason: `${CONTEXT_CONFIG_REL} is not valid TOML`,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const declared = parsed.roots
|
|
69
|
+
if (
|
|
70
|
+
!Array.isArray(declared) ||
|
|
71
|
+
!declared.every((entry) => typeof entry === 'string' && entry !== '')
|
|
72
|
+
) {
|
|
73
|
+
return {
|
|
74
|
+
kind: 'invalid',
|
|
75
|
+
reason: `${CONTEXT_CONFIG_REL} needs roots as a list of folder or glob strings`,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return declared.length === 0
|
|
80
|
+
? { kind: 'opted-out' }
|
|
81
|
+
: { kind: 'roots', patterns: declared as string[] }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return isDirectory(join(root, DEFAULT_ROOT))
|
|
85
|
+
? { kind: 'roots', patterns: [DEFAULT_ROOT] }
|
|
86
|
+
: { kind: 'none' }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isDirectory(path: string): boolean {
|
|
90
|
+
try {
|
|
91
|
+
return statSync(path).isDirectory()
|
|
92
|
+
} catch {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Expands each pattern to the folders it matches, relative to `root`. */
|
|
98
|
+
function expandRoots(root: string, patterns: readonly string[]) {
|
|
99
|
+
const roots: string[] = []
|
|
100
|
+
const missing: string[] = []
|
|
101
|
+
|
|
102
|
+
for (const pattern of patterns) {
|
|
103
|
+
const matched = [
|
|
104
|
+
...new Bun.Glob(pattern).scanSync({ cwd: root, onlyFiles: false }),
|
|
105
|
+
]
|
|
106
|
+
.map((path) => path.replace(/\/$/, ''))
|
|
107
|
+
.filter((path) => isDirectory(join(root, path)))
|
|
108
|
+
.sort()
|
|
109
|
+
|
|
110
|
+
if (matched.length === 0) missing.push(pattern)
|
|
111
|
+
roots.push(...matched)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { roots: [...new Set(roots)], missing }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Lists folders directly under a root, skipping hidden ones and `node_modules`. */
|
|
118
|
+
function listFolders(root: string, rel: string): string[] {
|
|
119
|
+
return readdirSync(join(root, rel), { withFileTypes: true })
|
|
120
|
+
.filter(
|
|
121
|
+
(entry) =>
|
|
122
|
+
entry.isDirectory() &&
|
|
123
|
+
!entry.name.startsWith('.') &&
|
|
124
|
+
entry.name !== 'node_modules',
|
|
125
|
+
)
|
|
126
|
+
.map((entry) => `${rel}/${entry.name}`)
|
|
127
|
+
.sort()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function layoutSection(source: string): string {
|
|
131
|
+
const lines = source.split('\n')
|
|
132
|
+
const start = lines.findIndex((line) => /^##\s+Layout\s*$/.test(line))
|
|
133
|
+
if (start === -1) return ''
|
|
134
|
+
|
|
135
|
+
const end = lines.findIndex(
|
|
136
|
+
(line, index) => index > start && /^##\s/.test(line),
|
|
137
|
+
)
|
|
138
|
+
return lines.slice(start + 1, end === -1 ? undefined : end).join('\n')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function backtickedPaths(text: string): string[] {
|
|
142
|
+
return [...text.matchAll(/`([^`\s]+)`/g)].map((match) =>
|
|
143
|
+
match[1].replace(/\/$/, ''),
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Measures the folders under the declared roots against the Layout sections.
|
|
149
|
+
*
|
|
150
|
+
* A folder is named when any Layout text cites it in backticks, a bullet
|
|
151
|
+
* included, and claimed when it is an H3 title. Only the H3 counts toward
|
|
152
|
+
* uniqueness, since a bullet legitimately cites another domain's folder and a
|
|
153
|
+
* grep cannot tell a citation from a claim.
|
|
154
|
+
*/
|
|
155
|
+
export function measureCoverage(
|
|
156
|
+
root: string,
|
|
157
|
+
declaration: RootDeclaration,
|
|
158
|
+
folders: readonly AuditedFolder[],
|
|
159
|
+
): CoverageReport {
|
|
160
|
+
if (declaration.kind === 'opted-out') return { kind: 'opted-out' }
|
|
161
|
+
if (declaration.kind === 'none') return { kind: 'no-root' }
|
|
162
|
+
if (declaration.kind === 'invalid') {
|
|
163
|
+
return { kind: 'invalid', reason: declaration.reason }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const named = new Set<string>()
|
|
167
|
+
const claims = new Map<string, Set<string>>()
|
|
168
|
+
|
|
169
|
+
for (const folder of folders) {
|
|
170
|
+
for (const entry of folder.entries) {
|
|
171
|
+
const rel = relative(root, entry)
|
|
172
|
+
const layout = layoutSection(readFileSync(entry, 'utf8'))
|
|
173
|
+
|
|
174
|
+
for (const path of backtickedPaths(layout)) named.add(path)
|
|
175
|
+
|
|
176
|
+
for (const line of layout.split('\n')) {
|
|
177
|
+
const heading = line.match(/^###\s+`([^`\s]+)`\s*$/)
|
|
178
|
+
if (!heading) continue
|
|
179
|
+
const path = heading[1].replace(/\/$/, '')
|
|
180
|
+
claims.set(path, (claims.get(path) ?? new Set()).add(rel))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { roots, missing } = expandRoots(root, declaration.patterns)
|
|
186
|
+
const onDisk = roots.flatMap((rel) => listFolders(root, rel))
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
kind: 'measured',
|
|
190
|
+
roots,
|
|
191
|
+
findings: {
|
|
192
|
+
unnamed: onDisk.filter((path) => !named.has(path)),
|
|
193
|
+
doubled: onDisk
|
|
194
|
+
.filter((path) => (claims.get(path)?.size ?? 0) > 1)
|
|
195
|
+
.map((folder) => ({
|
|
196
|
+
folder,
|
|
197
|
+
rels: [...(claims.get(folder) ?? [])].sort(),
|
|
198
|
+
})),
|
|
199
|
+
missingRoots: missing,
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Whether the report holds a finding that should fail a widened gate. */
|
|
205
|
+
export function hasCoverageFindings(report: CoverageReport): boolean {
|
|
206
|
+
if (report.kind === 'invalid') return true
|
|
207
|
+
if (report.kind !== 'measured') return false
|
|
208
|
+
const { unnamed, doubled, missingRoots } = report.findings
|
|
209
|
+
return unnamed.length > 0 || doubled.length > 0 || missingRoots.length > 0
|
|
210
|
+
}
|
package/src/context/gate.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
SectionFinding,
|
|
4
4
|
ShapeFinding,
|
|
5
5
|
} from '@/context/audit'
|
|
6
|
+
import { type CoverageReport, hasCoverageFindings } from '@/context/coverage'
|
|
6
7
|
import type { FolderDrift } from '@/context/index-drift'
|
|
7
8
|
|
|
8
9
|
export interface GateInput {
|
|
@@ -25,6 +26,12 @@ export interface GateInput {
|
|
|
25
26
|
*/
|
|
26
27
|
readonly shape: readonly ShapeFinding[]
|
|
27
28
|
readonly drift: readonly FolderDrift[]
|
|
29
|
+
/**
|
|
30
|
+
* Folders under the declared code roots that no Layout names or two entries
|
|
31
|
+
* claim, gating under `--gate` alone. A project with no code root reads as
|
|
32
|
+
* not measured and never gates.
|
|
33
|
+
*/
|
|
34
|
+
readonly coverage: CoverageReport
|
|
28
35
|
/**
|
|
29
36
|
* Whether the caller asked for the widened gate. False leaves a missing
|
|
30
37
|
* section and a drifted index advisory, which is what the project-root stage
|
|
@@ -48,8 +55,8 @@ export function hasDrift(drift: readonly FolderDrift[]): boolean {
|
|
|
48
55
|
* for itself, which makes it a fact rather than a threshold a reader weighs.
|
|
49
56
|
* The findings `--gate` adds are the ones answerable from the file itself: a
|
|
50
57
|
* required section it does not declare, an H2 outside the closed set, a shape
|
|
51
|
-
* break in a folder rebuilt to the shape,
|
|
52
|
-
* folder. Entry length, depth, bullet, table, provenance, and the record's
|
|
58
|
+
* break in a folder rebuilt to the shape, an index disagreeing with its
|
|
59
|
+
* folder, and a code folder no entry names or two entries claim. Entry length, depth, bullet, table, provenance, and the record's
|
|
53
60
|
* claim coverage are judgments, so they stay out under both modes.
|
|
54
61
|
*/
|
|
55
62
|
export function isGating({
|
|
@@ -59,6 +66,7 @@ export function isGating({
|
|
|
59
66
|
extraSections,
|
|
60
67
|
shape,
|
|
61
68
|
drift,
|
|
69
|
+
coverage,
|
|
62
70
|
widened,
|
|
63
71
|
}: GateInput): boolean {
|
|
64
72
|
if (unresolvedCitations > 0) return true
|
|
@@ -69,6 +77,7 @@ export function isGating({
|
|
|
69
77
|
sections.length > 0 ||
|
|
70
78
|
extraSections.length > 0 ||
|
|
71
79
|
shape.some((finding) => finding.gated) ||
|
|
72
|
-
hasDrift(drift)
|
|
80
|
+
hasDrift(drift) ||
|
|
81
|
+
hasCoverageFindings(coverage)
|
|
73
82
|
)
|
|
74
83
|
}
|
package/src/design/components.ts
CHANGED
|
@@ -520,7 +520,7 @@ html.sb-shut .pane { border-left: 0; }
|
|
|
520
520
|
}
|
|
521
521
|
|
|
522
522
|
/* A bar link is its own tap target rather than a line of text, so it carries
|
|
523
|
-
the 24 pixel minimum the
|
|
523
|
+
the 24 pixel minimum the keyboard rule sets. */
|
|
524
524
|
.mast a,
|
|
525
525
|
.mast summary {
|
|
526
526
|
min-height: 1.6rem;
|
package/src/gate/stages.ts
CHANGED
|
@@ -334,7 +334,7 @@ export const STAGES: readonly Stage[] = [
|
|
|
334
334
|
kind: 'cli',
|
|
335
335
|
argv: ['context', 'audit', '--gate'],
|
|
336
336
|
failure:
|
|
337
|
-
'A cited context path does not resolve, an entry lacks a required section or carries an H2 outside Overview, Layout, and Gotchas, an entry under a shape-gated folder breaks the section shape,
|
|
337
|
+
'A cited context path does not resolve, an entry lacks a required section or carries an H2 outside Overview, Layout, and Gotchas, an entry under a shape-gated folder breaks the section shape, an index disagrees with its folder, or a code folder is named in no Layout or claimed by two entries. Run bun src/cli.ts context audit --gate.',
|
|
338
338
|
},
|
|
339
339
|
],
|
|
340
340
|
success: 'Context citations resolve',
|
package/src/gov/audit.ts
CHANGED
|
@@ -36,6 +36,21 @@ export const RULE_GATED_FOLDERS: readonly string[] = [
|
|
|
36
36
|
join('governance', 'rules', 'framework') + sep,
|
|
37
37
|
join('governance', 'rules', 'writing') + sep,
|
|
38
38
|
join('internal', 'rules') + sep,
|
|
39
|
+
join('governance', 'rules', 'lang') + sep,
|
|
40
|
+
join('governance', 'rules', 'lib') + sep,
|
|
41
|
+
join('governance', 'rules', 'ui') + sep,
|
|
42
|
+
join('governance', 'rules', 'tooling') + sep,
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Rule files in a gated folder whose findings `--gate` still passes, one path
|
|
47
|
+
* each and never a folder, so a new rule beside one is gated the day it lands.
|
|
48
|
+
*
|
|
49
|
+
* `ui/canvas.md` carries a shape finding and no ledger, and v149.2 removes the
|
|
50
|
+
* rule, so this entry goes with it.
|
|
51
|
+
*/
|
|
52
|
+
export const RULE_GATE_EXEMPT: readonly string[] = [
|
|
53
|
+
join('governance', 'rules', 'ui', 'canvas.md'),
|
|
39
54
|
]
|
|
40
55
|
|
|
41
56
|
/** The folders whose rules claim outside practice, so each carries a ledger. */
|
|
@@ -429,13 +444,18 @@ export function hasFinding(reading: RuleReading): boolean {
|
|
|
429
444
|
*/
|
|
430
445
|
export function auditExit(
|
|
431
446
|
report: Extract<RuleAuditReport, { kind: 'measured' }>,
|
|
432
|
-
options: {
|
|
447
|
+
options: {
|
|
448
|
+
readonly gate: boolean
|
|
449
|
+
readonly gated: readonly string[]
|
|
450
|
+
readonly exempt?: readonly string[]
|
|
451
|
+
},
|
|
433
452
|
): number {
|
|
434
453
|
const failing = report.rules.filter(
|
|
435
454
|
(reading) =>
|
|
436
455
|
hasFinding(reading) &&
|
|
437
456
|
(!options.gate ||
|
|
438
|
-
options.gated.some((prefix) => reading.file.startsWith(prefix))
|
|
457
|
+
(options.gated.some((prefix) => reading.file.startsWith(prefix)) &&
|
|
458
|
+
!options.exempt?.includes(reading.file))),
|
|
439
459
|
)
|
|
440
460
|
return failing.length > 0 ? 2 : 0
|
|
441
461
|
}
|
package/src/init/plan.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SKIPPABLE_DOMAINS = ['
|
|
1
|
+
export const SKIPPABLE_DOMAINS = ['governance', 'records'] as const
|
|
2
2
|
|
|
3
3
|
export type SkippableDomain = (typeof SKIPPABLE_DOMAINS)[number]
|
|
4
4
|
|
|
@@ -97,13 +97,6 @@ export function planInit(flags: InitFlags): InitPlan {
|
|
|
97
97
|
})
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
if (!flags.skip.skipped.has('wiki')) {
|
|
101
|
-
preview.push({
|
|
102
|
-
level: 'info',
|
|
103
|
-
text: 'wiki (.claude/wiki/ with a stub index)',
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
|
|
107
100
|
if (!flags.skip.skipped.has('records')) {
|
|
108
101
|
preview.push({
|
|
109
102
|
level: 'info',
|
package/src/init/steps.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { logInfo, logWarn, plural } from '@/cli/ui'
|
|
1
2
|
import { type InitFlags, resolveStack } from '@/init/plan'
|
|
2
3
|
import type { DomainStep } from '@/init/run'
|
|
4
|
+
import { GATED_CONFIG_REL, gateScopeRoots } from '@/markdown/gated'
|
|
3
5
|
|
|
4
6
|
/** Builds the child-process invocation for one domain. */
|
|
5
7
|
export type RunFactory = (args: readonly string[]) => () => Promise<boolean>
|
|
@@ -55,13 +57,15 @@ export function buildSteps(
|
|
|
55
57
|
})
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
// In-process rather than a child verb, since the audit reads the working
|
|
61
|
+
// directory and every child here is handed the target as an argument. It
|
|
62
|
+
// runs after every seeding domain so the roots it measures hold what they
|
|
63
|
+
// will ship with.
|
|
64
|
+
steps.push({
|
|
65
|
+
kind: 'run',
|
|
66
|
+
label: 'Markdown gating',
|
|
67
|
+
run: () => gateMarkdown(resolved),
|
|
68
|
+
})
|
|
65
69
|
|
|
66
70
|
if (!flags.skip.skipped.has('records')) {
|
|
67
71
|
// `skip` here means there is nothing to run non-interactively, not that
|
|
@@ -76,6 +80,39 @@ export function buildSteps(
|
|
|
76
80
|
return steps
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Starts a new project with every density root gated that passes now. A root
|
|
85
|
+
* with a finding is named and left out, since gating it would fail the first
|
|
86
|
+
* push on prose a scaffolder wrote.
|
|
87
|
+
*/
|
|
88
|
+
async function gateMarkdown(root: string): Promise<boolean> {
|
|
89
|
+
const result = await gateScopeRoots(root)
|
|
90
|
+
|
|
91
|
+
if (result.kind === 'exists') {
|
|
92
|
+
logInfo(`${GATED_CONFIG_REL} already exists, so its list is kept.`)
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A scaffold such as `npm create vite` makes no repository, and nothing else
|
|
97
|
+
// in init needs one, so this is a skip rather than a failed domain.
|
|
98
|
+
if (result.kind === 'unavailable') {
|
|
99
|
+
logWarn(
|
|
100
|
+
"Skipped: not a git repository, so no root was measured or gated. Run 'canon markdown gated add <prefix>' once it is one.",
|
|
101
|
+
)
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
logInfo(
|
|
106
|
+
`Gated ${result.gated.length === 0 ? 'nothing' : result.gated.join(', ')} in ${GATED_CONFIG_REL}.`,
|
|
107
|
+
)
|
|
108
|
+
for (const { prefix, findings } of result.left) {
|
|
109
|
+
logWarn(
|
|
110
|
+
`Left ${prefix} ungated with ${plural(findings, 'finding')}. Repair it, then run 'canon markdown gated add ${prefix}'.`,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
|
|
79
116
|
/**
|
|
80
117
|
* Builds the `gov install` argv. The run and the recovery command a skip prints
|
|
81
118
|
* come from here both, so the command a caller is told to paste installs what
|
package/src/intake/items.ts
CHANGED
|
@@ -44,6 +44,10 @@ export interface IntakeItem {
|
|
|
44
44
|
readonly answerLine: number | undefined
|
|
45
45
|
/** Text in the slot, absent when the slot is empty and the item is unread. */
|
|
46
46
|
readonly answer: string | undefined
|
|
47
|
+
/** What is wrong today, absent when the item carries no `Problem:` line. */
|
|
48
|
+
readonly problem: string | undefined
|
|
49
|
+
/** The one change proposed, absent when the item carries no `Fix:` line. */
|
|
50
|
+
readonly fix: string | undefined
|
|
47
51
|
readonly open: string | undefined
|
|
48
52
|
readonly suggested: string | undefined
|
|
49
53
|
readonly worth: string | undefined
|
|
@@ -55,6 +59,8 @@ interface Draft {
|
|
|
55
59
|
line: number
|
|
56
60
|
answerLine: number | undefined
|
|
57
61
|
answer: string | undefined
|
|
62
|
+
problem: string | undefined
|
|
63
|
+
fix: string | undefined
|
|
58
64
|
open: string | undefined
|
|
59
65
|
suggested: string | undefined
|
|
60
66
|
worth: string | undefined
|
|
@@ -89,6 +95,8 @@ export function readItems(text: string): IntakeItem[] {
|
|
|
89
95
|
line: line.number,
|
|
90
96
|
answerLine: undefined,
|
|
91
97
|
answer: undefined,
|
|
98
|
+
problem: undefined,
|
|
99
|
+
fix: undefined,
|
|
92
100
|
open: undefined,
|
|
93
101
|
suggested: undefined,
|
|
94
102
|
worth: undefined,
|
|
@@ -114,6 +122,12 @@ export function readItems(text: string): IntakeItem[] {
|
|
|
114
122
|
draft.answerLine = line.number
|
|
115
123
|
draft.answer = value === '' ? undefined : value
|
|
116
124
|
break
|
|
125
|
+
case 'problem':
|
|
126
|
+
draft.problem = value
|
|
127
|
+
break
|
|
128
|
+
case 'fix':
|
|
129
|
+
draft.fix = value
|
|
130
|
+
break
|
|
117
131
|
case 'open':
|
|
118
132
|
draft.open = value
|
|
119
133
|
break
|
|
@@ -27,7 +27,7 @@ export interface Subject {
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Reports what each element changes about itself when it takes focus, which is
|
|
30
|
-
* the reading `governance/rules/ui/
|
|
30
|
+
* the reading `governance/rules/ui/keyboard.md` states three rules against and
|
|
31
31
|
* nothing measures.
|
|
32
32
|
*
|
|
33
33
|
* The difference between rest and focus is the treatment, rather than the
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { linesOutsideFences, maskCodeSpans } from '@/markdown/scan'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The keywords GitHub reads as closing an issue on merge, each with its
|
|
5
|
+
* tenses. The keyword must start a word, so `disclosed #12` closes nothing,
|
|
6
|
+
* and GitHub reads only the one number that follows it, so `Closes #12, #13`
|
|
7
|
+
* closes #12 alone.
|
|
8
|
+
*/
|
|
9
|
+
const CLOSING =
|
|
10
|
+
/(?<!\w)(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?[ \t]+#(\d+)/gi
|
|
11
|
+
|
|
12
|
+
export interface ClosingCheck {
|
|
13
|
+
/** Expected issues the body carries no closing reference for. */
|
|
14
|
+
readonly missing: readonly number[]
|
|
15
|
+
/** Issues the body closes that the expectation did not name. */
|
|
16
|
+
readonly stray: readonly number[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reads the issue numbers a body closes, in order and deduped. A code span and
|
|
21
|
+
* a fence quote a keyword rather than use it, and GitHub ignores both.
|
|
22
|
+
*/
|
|
23
|
+
export function readClosingReferences(body: string): readonly number[] {
|
|
24
|
+
const numbers = new Set<number>()
|
|
25
|
+
|
|
26
|
+
for (const line of linesOutsideFences(body)) {
|
|
27
|
+
for (const match of maskCodeSpans(line).matchAll(CLOSING)) {
|
|
28
|
+
numbers.add(Number(match[1]))
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return [...numbers]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Compares the issues a body closes to the issues it should. */
|
|
36
|
+
export function checkClosing(
|
|
37
|
+
body: string,
|
|
38
|
+
expected: readonly number[],
|
|
39
|
+
): ClosingCheck {
|
|
40
|
+
const found = readClosingReferences(body)
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
missing: expected.filter((number) => !found.includes(number)),
|
|
44
|
+
stray: found.filter((number) => !expected.includes(number)),
|
|
45
|
+
}
|
|
46
|
+
}
|