@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
package/src/gate/stages.ts
CHANGED
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
skillFamily,
|
|
26
26
|
skillPracticeShape,
|
|
27
27
|
skillProvenance,
|
|
28
|
-
standardCriteria,
|
|
29
28
|
unreferencedRules,
|
|
30
29
|
visualPathGlobs,
|
|
31
30
|
} from '@/gate/measures'
|
|
@@ -502,11 +501,6 @@ export const STAGES: readonly Stage[] = [
|
|
|
502
501
|
label: 'Skill family',
|
|
503
502
|
checks: [{ kind: 'measure', measure: skillFamily }],
|
|
504
503
|
},
|
|
505
|
-
{
|
|
506
|
-
id: 'standard-criteria',
|
|
507
|
-
label: 'Standard success criteria',
|
|
508
|
-
checks: [{ kind: 'measure', measure: standardCriteria }],
|
|
509
|
-
},
|
|
510
504
|
{
|
|
511
505
|
id: 'sandbox-coverage',
|
|
512
506
|
label: 'Sandbox coverage',
|
|
@@ -522,18 +516,6 @@ export const STAGES: readonly Stage[] = [
|
|
|
522
516
|
label: 'Plugin manifests',
|
|
523
517
|
checks: [{ kind: 'measure', measure: pluginManifests }],
|
|
524
518
|
},
|
|
525
|
-
{
|
|
526
|
-
id: 'spelling',
|
|
527
|
-
label: 'Spelling',
|
|
528
|
-
checks: [
|
|
529
|
-
{
|
|
530
|
-
kind: 'command',
|
|
531
|
-
argv: ['bun', 'run', 'check:spell'],
|
|
532
|
-
failure: 'Spell check failed',
|
|
533
|
-
},
|
|
534
|
-
],
|
|
535
|
-
success: 'Spell check passed',
|
|
536
|
-
},
|
|
537
519
|
{
|
|
538
520
|
id: 'shell',
|
|
539
521
|
label: 'Shell',
|
package/src/gov/citations.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import {
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
4
|
import { $ } from 'bun'
|
|
5
5
|
import { isMarked } from '@/sweep/exempt-marker'
|
|
6
6
|
import { gitEnv } from '@/git/env'
|
|
7
7
|
import { listRuleFiles } from '@/gov/payload'
|
|
8
8
|
import { parseFrontmatter } from '@/indexes/frontmatter'
|
|
9
|
-
import { movedStandardPath } from '@/standards/read'
|
|
10
9
|
import { findBacktickSpans } from '@/markdown/backticks'
|
|
11
10
|
|
|
12
11
|
/**
|
|
@@ -42,14 +41,11 @@ export const RULE_DIRS: readonly string[] = [
|
|
|
42
41
|
export const GLOB_CORPUS = join('internal', 'rules')
|
|
43
42
|
|
|
44
43
|
/**
|
|
45
|
-
* How the citation was written,
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* `standard` is the live form, carried by 20 rules. `path` is a backticked
|
|
49
|
-
* repository path. `sibling` is a rule naming another rule by filename alone,
|
|
50
|
-
* which resolves inside the folder the citing rule sits in.
|
|
44
|
+
* How the citation was written, being a backticked repository path. A bare
|
|
45
|
+
* filename is no form, since with no folder around it nothing tells a rule
|
|
46
|
+
* from a convention such as `index.md`.
|
|
51
47
|
*/
|
|
52
|
-
export type CitationForm = 'path'
|
|
48
|
+
export type CitationForm = 'path'
|
|
53
49
|
|
|
54
50
|
/**
|
|
55
51
|
* What the sweep decided about one citation.
|
|
@@ -116,16 +112,6 @@ const FENCE = /^\s*(?:```|~~~)/
|
|
|
116
112
|
|
|
117
113
|
const FRONTMATTER_DELIMITER = /^---\s*$/
|
|
118
114
|
|
|
119
|
-
/**
|
|
120
|
-
* The verb form, with the name captured. A leading letter or digit is required,
|
|
121
|
-
* which is what leaves `canon standards <name>` unmatched: that line teaches the
|
|
122
|
-
* form rather than citing a standard, and it is the only one in either corpus.
|
|
123
|
-
*/
|
|
124
|
-
const STANDARD_CALL = /canon standards ([A-Za-z0-9][A-Za-z0-9._-]*)/g
|
|
125
|
-
|
|
126
|
-
/** A rule filename, which is how a rule names a sibling with no folder around it. */
|
|
127
|
-
const SIBLING_RULE = /^\d{3}-[a-z0-9-]+\.md$/
|
|
128
|
-
|
|
129
115
|
/**
|
|
130
116
|
* A character that puts the span outside this repository, or outside paths
|
|
131
117
|
* altogether.
|
|
@@ -146,8 +132,8 @@ function isNotRepositoryPath(span: string): boolean {
|
|
|
146
132
|
* This is what separates a citation from the folder and module conventions the
|
|
147
133
|
* corpus is full of. `next/font`, `try/except`, `react-hooks/set-state-in-effect`,
|
|
148
134
|
* and `oven-sh/setup-bun@v2` all carry a slash and name no file, and a trailing
|
|
149
|
-
* slash is a folder rather than a document. The cost is that
|
|
150
|
-
*
|
|
135
|
+
* slash is a folder rather than a document. The cost is that a real folder path
|
|
136
|
+
* goes unchecked, which is the bound the report states.
|
|
151
137
|
*/
|
|
152
138
|
function namesAFile(span: string): boolean {
|
|
153
139
|
if (span.endsWith('/')) return false
|
|
@@ -160,46 +146,15 @@ function namesAFile(span: string): boolean {
|
|
|
160
146
|
* sweep can resolve.
|
|
161
147
|
*
|
|
162
148
|
* The whole span is classified rather than a trailing pattern inside it.
|
|
163
|
-
* Extracting
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
149
|
+
* Extracting a trailing `<folder>/<name>.md` out of a longer path and resolving
|
|
150
|
+
* that against another root manufactures a dead citation out of a file that
|
|
151
|
+
* exists, which a session measuring this corpus did before the check was
|
|
152
|
+
* written.
|
|
167
153
|
*/
|
|
168
154
|
export function classifySpan(span: string): CitationForm | undefined {
|
|
169
155
|
if (isNotRepositoryPath(span)) return undefined
|
|
170
156
|
if (!namesAFile(span)) return undefined
|
|
171
|
-
|
|
172
|
-
return SIBLING_RULE.test(span) ? 'sibling' : undefined
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Where a citation could answer, which is wherever the reader's own tools look
|
|
177
|
-
* and nowhere else.
|
|
178
|
-
*
|
|
179
|
-
* A standard name takes the authoring root alone, matching `standardRoots` in
|
|
180
|
-
* `@/standards/read`, which reads `standards/` at the working root and then the
|
|
181
|
-
* package corpus, plus the skill reference a retired name moved to. This verb
|
|
182
|
-
* refuses a tree holding no rule corpus, so it runs only where those two roots
|
|
183
|
-
* are one directory. `internal/standards/` is
|
|
184
|
-
* deliberately absent: `canon standards <name>` never reaches it, so admitting it
|
|
185
|
-
* here would pass a citation that refuses for the session opening it, which is a
|
|
186
|
-
* gate failing open.
|
|
187
|
-
*
|
|
188
|
-
* A sibling resolves inside the folder the citing rule sits in, since that is
|
|
189
|
-
* the only place a bare rule filename means anything.
|
|
190
|
-
*/
|
|
191
|
-
function candidatesFor(
|
|
192
|
-
form: CitationForm,
|
|
193
|
-
cited: string,
|
|
194
|
-
ruleFile: string,
|
|
195
|
-
): string[] {
|
|
196
|
-
if (form === 'standard') {
|
|
197
|
-
const moved = movedStandardPath(cited)
|
|
198
|
-
|
|
199
|
-
return [join('standards', `${cited}.md`), ...(moved ? [moved] : [])]
|
|
200
|
-
}
|
|
201
|
-
if (form === 'sibling') return [join(dirname(ruleFile), cited)]
|
|
202
|
-
return [cited]
|
|
157
|
+
return span.includes('/') ? 'path' : undefined
|
|
203
158
|
}
|
|
204
159
|
|
|
205
160
|
interface RawCitation {
|
|
@@ -244,16 +199,6 @@ export function collectCitations(text: string): RawCitation[] {
|
|
|
244
199
|
: trimmed
|
|
245
200
|
const marked = isMarked(lines, index, CITATION_MARKER)
|
|
246
201
|
|
|
247
|
-
for (const match of line.matchAll(STANDARD_CALL)) {
|
|
248
|
-
found.push({
|
|
249
|
-
line: index + 1,
|
|
250
|
-
form: 'standard',
|
|
251
|
-
cited: match[1] ?? '',
|
|
252
|
-
preview,
|
|
253
|
-
marked,
|
|
254
|
-
})
|
|
255
|
-
}
|
|
256
|
-
|
|
257
202
|
// A backticked span is the only carrier a rule writes a citation in. No
|
|
258
203
|
// rule in either corpus uses a markdown link, and matching running prose
|
|
259
204
|
// would report every sentence that happens to name a file.
|
|
@@ -300,7 +245,7 @@ function isGoverned(cited: string, declared: readonly string[]): boolean {
|
|
|
300
245
|
*
|
|
301
246
|
* Anchored on the entry shape rather than searched for as a substring. A bare
|
|
302
247
|
* scan for the glob text finds it in the `description:` line first wherever a
|
|
303
|
-
* rule names what it governs in prose, which is how `
|
|
248
|
+
* rule names what it governs in prose, which is how `seed-mirror.md` reported
|
|
304
249
|
* its `CLAUDE.md` glob against line 2 instead of line 4.
|
|
305
250
|
*/
|
|
306
251
|
const LIST_ENTRY = /^\s*-\s*(?:'([^']*)'|"([^"]*)"|(\S.*?))\s*$/
|
|
@@ -442,7 +387,7 @@ export async function readCitations(root: string): Promise<CitationReport> {
|
|
|
442
387
|
}
|
|
443
388
|
|
|
444
389
|
for (const raw of collectCitations(text)) {
|
|
445
|
-
const candidates =
|
|
390
|
+
const candidates = [raw.cited]
|
|
446
391
|
const resolved = candidates.find((path) =>
|
|
447
392
|
existsSync(resolve(root, path)),
|
|
448
393
|
)
|
|
@@ -471,7 +416,7 @@ function classifyStatus(
|
|
|
471
416
|
): CitationStatus {
|
|
472
417
|
if (resolved !== undefined) return 'resolved'
|
|
473
418
|
if (raw.marked) return 'exempt'
|
|
474
|
-
if (
|
|
419
|
+
if (isGoverned(raw.cited, declared)) return 'governed'
|
|
475
420
|
return 'dead'
|
|
476
421
|
}
|
|
477
422
|
|
|
@@ -488,7 +433,7 @@ async function applyIgnored(
|
|
|
488
433
|
measured: Extract<CitationReport, { kind: 'measured' }>,
|
|
489
434
|
): Promise<CitationReport> {
|
|
490
435
|
const pending = measured.citations.filter(
|
|
491
|
-
(citation) => citation.status === 'dead'
|
|
436
|
+
(citation) => citation.status === 'dead',
|
|
492
437
|
)
|
|
493
438
|
if (pending.length === 0) return measured
|
|
494
439
|
|
package/src/gov/stacks.ts
CHANGED
|
@@ -78,9 +78,9 @@ export function loadGovStack(
|
|
|
78
78
|
* entry resolves to itself, so a folder and a slug reach the caller as one
|
|
79
79
|
* shape rather than two the caller has to tell apart.
|
|
80
80
|
*
|
|
81
|
-
* The directory wins over a rule file of the same name
|
|
82
|
-
*
|
|
83
|
-
*
|
|
81
|
+
* The directory wins over a rule file of the same name, which would leave
|
|
82
|
+
* that rule unreachable from a stack entry. `src/gov/rule-layout.test.ts`
|
|
83
|
+
* fails on any rule basename equal to a folder name, so the two never meet.
|
|
84
84
|
*/
|
|
85
85
|
export function expandStackEntry(root: string, entry: string): string[] {
|
|
86
86
|
const dir = join(rulesSourceDir(root), entry)
|
|
@@ -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/a11y.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
|
package/src/markdown/bans.ts
CHANGED
|
@@ -16,7 +16,7 @@ const CHARACTERS = ['—', ';'] as const
|
|
|
16
16
|
* Parsing the prose was the alternative and it put a parser contract on a
|
|
17
17
|
* document authored for people. The set is closed rather than extensible,
|
|
18
18
|
* since a character is the one class a literal match settles. A word is left
|
|
19
|
-
* to `write-human` as guidance and a spelling to
|
|
19
|
+
* to `write-human` as guidance and a spelling to no check at all, because a word ban
|
|
20
20
|
* catches the token and misses the habit behind it.
|
|
21
21
|
*/
|
|
22
22
|
export const BAN_SETS: BanSets = {
|
package/src/markdown/ceiling.ts
CHANGED
|
@@ -34,9 +34,14 @@ export interface CeilingFinding {
|
|
|
34
34
|
readonly exempt: string | null
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Whether the release tool writes this document, which no measure reads. */
|
|
38
|
+
export function isChangelog(rel: string): boolean {
|
|
39
|
+
return basename(rel) === CHANGELOG
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
/** The reason a document is exempt from the ceiling, or null where it is not. */
|
|
38
43
|
export function lengthExemption(rel: string, source: string): string | null {
|
|
39
|
-
if (
|
|
44
|
+
if (isChangelog(rel)) return CHANGELOG_REASON
|
|
40
45
|
|
|
41
46
|
for (const line of bodyLines(source)) {
|
|
42
47
|
if (line.fenced) continue
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type BodyLine, LINK, maskDisplayed } from '@/markdown/scan'
|
|
2
|
+
|
|
3
|
+
export interface ColonFinding {
|
|
4
|
+
readonly line: number
|
|
5
|
+
/** Zero-based, the colon itself. */
|
|
6
|
+
readonly column: number
|
|
7
|
+
/** The whole source line, so a reader can match it against new text. */
|
|
8
|
+
readonly text: string
|
|
9
|
+
/**
|
|
10
|
+
* The run of non-space characters ending at the colon. A hook matches it
|
|
11
|
+
* against new text, since `column` is a UTF-16 offset that a reader counting
|
|
12
|
+
* code points would misplace after an emoji.
|
|
13
|
+
*/
|
|
14
|
+
readonly lead: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const HEADING = /^#{1,6}\s/
|
|
18
|
+
const TABLE_ROW = /^\s*\|/
|
|
19
|
+
const LIST_MARKER = /^\s*(?:[-*+]|\d+[.)])\s+/
|
|
20
|
+
const QUOTE_MARKER = /^(?:\s*>\s?)+/
|
|
21
|
+
|
|
22
|
+
/** A colon followed by whitespace and more text, ruling out `10:30` and a scheme. */
|
|
23
|
+
const COLON = /:(?=\s+\S)/g
|
|
24
|
+
|
|
25
|
+
const BOLD_LABEL = /^(\*\*|__)[^*_]+\1$/
|
|
26
|
+
|
|
27
|
+
/** Blanks a span while holding its width, so a column stays where it was. */
|
|
28
|
+
function blank(match: string): string {
|
|
29
|
+
return ' '.repeat(match.length)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether the text before a line's first colon is a label rather than a
|
|
34
|
+
* clause: the lead of a list item, or a prefix that is only emphasis and a
|
|
35
|
+
* masked code span.
|
|
36
|
+
*/
|
|
37
|
+
function isLabel(prefix: string, listItem: boolean): boolean {
|
|
38
|
+
if (BOLD_LABEL.test(prefix.trim())) return true
|
|
39
|
+
|
|
40
|
+
const words = prefix.replace(/[*_]/g, ' ').trim()
|
|
41
|
+
if (words === '') return true
|
|
42
|
+
return listItem && words.split(/\s+/).length <= 4
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Finds a colon joining two clauses in the middle of a prose sentence.
|
|
47
|
+
*
|
|
48
|
+
* A heading, a table row, a fence, a code span, a link, and a line-final colon
|
|
49
|
+
* are left alone, and so is the first colon of a list item or of a line opening
|
|
50
|
+
* on a bold or code label. The measure is advisory, because a colon is
|
|
51
|
+
* sometimes right and the corpus count shows a gate would be a sweep nobody
|
|
52
|
+
* asked for.
|
|
53
|
+
*/
|
|
54
|
+
export function findColons(lines: readonly BodyLine[]): ColonFinding[] {
|
|
55
|
+
const found: ColonFinding[] = []
|
|
56
|
+
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (line.fenced || HEADING.test(line.text) || TABLE_ROW.test(line.text)) {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const masked = maskDisplayed(line.text.replace(LINK, blank))
|
|
63
|
+
const quote = QUOTE_MARKER.exec(masked)?.[0] ?? ''
|
|
64
|
+
const marker = LIST_MARKER.exec(masked.slice(quote.length))?.[0] ?? ''
|
|
65
|
+
const start = quote.length + marker.length
|
|
66
|
+
let labelled = false
|
|
67
|
+
|
|
68
|
+
for (const match of masked.matchAll(COLON)) {
|
|
69
|
+
if (match.index < start) continue
|
|
70
|
+
|
|
71
|
+
if (!labelled) {
|
|
72
|
+
labelled = true
|
|
73
|
+
if (isLabel(masked.slice(start, match.index), marker !== '')) continue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
found.push({
|
|
77
|
+
line: line.number,
|
|
78
|
+
column: match.index,
|
|
79
|
+
text: line.text,
|
|
80
|
+
lead: line.text.slice(0, match.index).replace(/^.*\s/, ''),
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return found
|
|
86
|
+
}
|
package/src/markdown/gate.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ColonFinding } from '@/markdown/colons'
|
|
2
|
+
import type { HeadingCaseFinding } from '@/markdown/heading-case'
|
|
1
3
|
import type { LinkFinding } from '@/markdown/links'
|
|
2
4
|
import type { BanFinding } from '@/markdown/scan'
|
|
3
5
|
import type { StructureReport } from '@/markdown/structure'
|
|
@@ -16,6 +18,19 @@ export interface GateInput {
|
|
|
16
18
|
* alone states the same rule and leaves no place to assert it.
|
|
17
19
|
*/
|
|
18
20
|
readonly structure: readonly StructureReport[]
|
|
21
|
+
/**
|
|
22
|
+
* Every mid-sentence colon the run measured, read by nothing here.
|
|
23
|
+
*
|
|
24
|
+
* A colon is sometimes right and the corpus carries over a thousand
|
|
25
|
+
* candidates, so the measure warns and a push never fails on it.
|
|
26
|
+
*/
|
|
27
|
+
readonly colons: readonly ColonFinding[]
|
|
28
|
+
/**
|
|
29
|
+
* Every heading or title with a capital after its first word, read by
|
|
30
|
+
* nothing here. A proper noun is the one case it cannot tell from title
|
|
31
|
+
* case, and gating it would need a names list and an escape marker.
|
|
32
|
+
*/
|
|
33
|
+
readonly headingCase: readonly HeadingCaseFinding[]
|
|
19
34
|
}
|
|
20
35
|
|
|
21
36
|
/**
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { bodyLines, maskDisplayed } from '@/markdown/scan'
|
|
2
|
+
|
|
3
|
+
export interface HeadingCaseFinding {
|
|
4
|
+
readonly line: number
|
|
5
|
+
/** The heading or title as written, markers removed. */
|
|
6
|
+
readonly text: string
|
|
7
|
+
/** Every word after the first that opens on a capital. */
|
|
8
|
+
readonly words: readonly string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const HEADING = /^#{1,6}\s+(.*?)(?:\s+#+)?\s*$/
|
|
12
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---\n?/
|
|
13
|
+
const TITLE = /^title:\s*(["']?)(.*?)\1\s*$/
|
|
14
|
+
const CODE_SPAN = /(`+)(?:(?!\1).)*\1/g
|
|
15
|
+
const CAPITALIZED = /^[A-Z][a-z]/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The words of `text` that break sentence case.
|
|
19
|
+
*
|
|
20
|
+
* A code span stands for one lowercase word, so a heading opening on one
|
|
21
|
+
* counts that span as its first word. The first word with a letter in it is
|
|
22
|
+
* exempt, which keeps a leading numeral from promoting the next word.
|
|
23
|
+
*/
|
|
24
|
+
function capitalizedAfterFirst(text: string): string[] {
|
|
25
|
+
const masked = maskDisplayed(text.replace(CODE_SPAN, 'code'))
|
|
26
|
+
.replace(/\s+/g, ' ')
|
|
27
|
+
.trim()
|
|
28
|
+
const words = masked === '' ? [] : masked.split(' ')
|
|
29
|
+
const flagged: string[] = []
|
|
30
|
+
let seenFirst = false
|
|
31
|
+
|
|
32
|
+
for (const raw of words) {
|
|
33
|
+
const word = raw.replace(/^[^\p{L}]+/u, '')
|
|
34
|
+
if (!/\p{L}/u.test(raw)) continue
|
|
35
|
+
if (!seenFirst) {
|
|
36
|
+
seenFirst = true
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
if (CAPITALIZED.test(word)) flagged.push(word)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return flagged
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Finds a heading or a frontmatter `title` carrying a capitalized word after
|
|
47
|
+
* its first, outside code spans and all-caps tokens.
|
|
48
|
+
*
|
|
49
|
+
* A proper noun is the one case it cannot tell from title case, so the finding
|
|
50
|
+
* is a warning and no names list backs it.
|
|
51
|
+
*/
|
|
52
|
+
export function findHeadingCase(source: string): HeadingCaseFinding[] {
|
|
53
|
+
const found: HeadingCaseFinding[] = []
|
|
54
|
+
const check = (line: number, text: string): void => {
|
|
55
|
+
const words = capitalizedAfterFirst(text)
|
|
56
|
+
if (words.length > 0) found.push({ line, text, words })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const frontmatter = FRONTMATTER.exec(source)
|
|
60
|
+
if (frontmatter) {
|
|
61
|
+
for (const [index, text] of frontmatter[1].split('\n').entries()) {
|
|
62
|
+
const title = TITLE.exec(text)
|
|
63
|
+
if (title) check(index + 2, title[2])
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const line of bodyLines(source)) {
|
|
68
|
+
if (line.fenced) continue
|
|
69
|
+
const heading = HEADING.exec(line.text)
|
|
70
|
+
if (heading) check(line.number, heading[1])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return found
|
|
74
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* durable record naming a folder under it as its evidence is a citation into
|
|
7
7
|
* something the backup never covers. The nine promoted here are every folder
|
|
8
8
|
* under scratch that a live or archived record cites and no source file under
|
|
9
|
-
* `claude/`, `scripts/`, `src/`,
|
|
9
|
+
* `claude/`, `scripts/`, `src/`, or `governance/` names by path,
|
|
10
10
|
* which is what separates them from a folder a script still writes into on its
|
|
11
11
|
* own schedule.
|
|
12
12
|
*
|
|
@@ -42,7 +42,7 @@ import { recordDir, SCRATCH } from '@/roots/record'
|
|
|
42
42
|
* Derived by the two-clause test `canon migrate scratch-evidence` exists to
|
|
43
43
|
* apply mechanically: a durable record under a `BACKED_FOLDERS` entry, live
|
|
44
44
|
* or archived, names the folder as its evidence, and no file under `claude/`,
|
|
45
|
-
* `scripts/`, `src/`,
|
|
45
|
+
* `scripts/`, `src/`, or `governance/` names that path. Measured
|
|
46
46
|
* 2026-09-06 against nine folders holding thirteen files.
|
|
47
47
|
*
|
|
48
48
|
* `verify-astro` and `verify-vite-react` pass the same test and are excluded
|