@erclx/canon 5.34.0 → 5.35.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.
Files changed (143) hide show
  1. package/README.md +3 -5
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/REQUIREMENT.md +1 -1
  4. package/claude/skills/auto-ship/references/draft-mark.md +1 -1
  5. package/claude/skills/canon-cli/SKILL.md +10 -11
  6. package/claude/skills/create-skill/SKILL.md +2 -1
  7. package/claude/skills/create-skill/references/skill-paths.md +12 -15
  8. package/{standards/standard.md → claude/skills/create-skill/references/skill-reference.md} +50 -45
  9. package/claude/skills/create-skill/references/skill.md +9 -3
  10. package/claude/skills/draft-and-pick/SKILL.md +1 -1
  11. package/claude/skills/git-commit/references/versioning.md +13 -13
  12. package/claude/skills/git-pr/SKILL.md +1 -1
  13. package/claude/skills/git-pr/references/publish.md +1 -1
  14. package/claude/skills/review-address/REQUIREMENT.md +1 -1
  15. package/claude/skills/review-address/SKILL.md +2 -2
  16. package/claude/skills/review-pr/SKILL.md +3 -3
  17. package/claude/skills/review-pr/references/post.md +1 -1
  18. package/claude/skills/review-ui/REQUIREMENT.md +1 -1
  19. package/claude/skills/review-ui/SKILL.md +1 -1
  20. package/claude/skills/role-orchestrator/REQUIREMENT.md +2 -2
  21. package/claude/skills/role-orchestrator/SKILL.md +11 -12
  22. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +3 -1
  23. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +11 -8
  24. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +10 -4
  25. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +2 -2
  26. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +11 -12
  27. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +12 -15
  28. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +6 -4
  29. package/claude/skills/role-orchestrator/scripts/poll.ts +50 -1
  30. package/claude/skills/role-reviewer/REQUIREMENT.md +17 -7
  31. package/claude/skills/role-reviewer/SKILL.md +17 -10
  32. package/claude/skills/role-worker/REQUIREMENT.md +1 -1
  33. package/claude/skills/role-worker/SKILL.md +6 -6
  34. package/claude/skills/session-compact/references/handoff-note.md +1 -1
  35. package/claude/skills/target-setup/SKILL.md +2 -2
  36. package/claude/skills/target-setup/references/detect.md +3 -3
  37. package/docs/agents/audits.md +1 -1
  38. package/docs/agents/commands.md +2 -4
  39. package/docs/agents/counts.md +5 -6
  40. package/docs/agents/drift-surfaces.md +15 -19
  41. package/docs/agents/index.md +2 -3
  42. package/docs/agents/install-and-sync.md +7 -31
  43. package/docs/agents/markdown-audit.md +10 -12
  44. package/docs/agents/migrate.md +1 -1
  45. package/docs/agents/records-stale.md +1 -1
  46. package/docs/agents/records-validate.md +11 -23
  47. package/docs/agents/records.md +1 -1
  48. package/docs/agents/routing.md +1 -1
  49. package/docs/agents/rule-citations.md +11 -14
  50. package/docs/agents/scripting.md +2 -20
  51. package/docs/agents/state-scoped-risk.md +1 -1
  52. package/docs/agents/tasks-validate.md +1 -1
  53. package/docs/agents/teach.md +1 -1
  54. package/docs/target/index.md +1 -0
  55. package/docs/target/migrations.md +4 -0
  56. package/docs/target/projects.md +9 -9
  57. package/docs/target/scenarios.md +5 -6
  58. package/docs/target/standards-moved.md +32 -0
  59. package/docs/target/sync.md +5 -5
  60. package/docs/workflow/operating-model.md +16 -18
  61. package/docs/workflow/skill-map.md +1 -1
  62. package/governance/detect.toml +0 -4
  63. package/governance/renames.toml +0 -1
  64. package/governance/stacks/base.toml +15 -8
  65. package/governance/stacks/go.toml +2 -0
  66. package/governance/stacks/node.toml +1 -1
  67. package/governance/stacks/php.toml +2 -0
  68. package/governance/stacks/python.toml +1 -1
  69. package/package.json +1 -4
  70. package/scripts/core/install-check.sh +2 -3
  71. package/src/audits/catalog.ts +0 -9
  72. package/src/claude/routing.ts +1 -1
  73. package/src/claude/skills-reach.ts +4 -6
  74. package/src/cli/help.ts +3 -4
  75. package/src/cli.ts +0 -2
  76. package/src/commands/gov.ts +5 -9
  77. package/src/commands/init.ts +1 -1
  78. package/src/commands/labels.ts +4 -44
  79. package/src/commands/migrate.ts +1 -1
  80. package/src/commands/records.ts +4 -20
  81. package/src/commands/tooling.ts +2 -4
  82. package/src/counts/catalogs.ts +0 -6
  83. package/src/gate/boundaries.ts +2 -2
  84. package/src/gate/measures.ts +6 -28
  85. package/src/gate/sequencer.ts +51 -2
  86. package/src/gate/stages.ts +0 -18
  87. package/src/gov/citations.ts +11 -43
  88. package/src/markdown/bans.ts +1 -1
  89. package/src/migrate/scratch-evidence.ts +2 -2
  90. package/src/records/validate.ts +6 -194
  91. package/src/sandbox/provision.ts +0 -5
  92. package/src/secrets/shipped.ts +1 -2
  93. package/src/serve/static.ts +1 -2
  94. package/src/shipped/references.ts +29 -80
  95. package/src/sync/check.ts +18 -14
  96. package/src/tooling/inject.ts +6 -37
  97. package/src/tooling/read.ts +2 -3
  98. package/src/tooling/scan.ts +4 -17
  99. package/src/tooling/subfolder.ts +2 -66
  100. package/tooling/base/configs/scripts/verify.json +1 -1
  101. package/tooling/base/configs/scripts/verify.ts +1 -12
  102. package/tooling/base/manifest.toml +1 -2
  103. package/tooling/base/reference.md +10 -16
  104. package/tooling/base/seeds/.github/workflows/verify.yml +0 -3
  105. package/tooling/base/seeds/.lintstagedrc +1 -2
  106. package/tooling/base/seeds/canon/context/ci.md +0 -1
  107. package/tooling/base/seeds/canon/context/development.md +1 -1
  108. package/tooling/claude/reference.md +1 -1
  109. package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +13 -1
  110. package/tooling/go/reference.md +1 -1
  111. package/tooling/php/reference.md +1 -1
  112. package/tooling/python/reference.md +2 -2
  113. package/tooling/web/configs/.github/workflows/verify.yml +0 -3
  114. package/tooling/web/manifest.toml +0 -1
  115. package/tooling/web/reference.md +2 -4
  116. package/docs/agents/standards-audit.md +0 -40
  117. package/governance/rules/tooling/765-spelling.md +0 -19
  118. package/governance/stacks/astro.toml +0 -2
  119. package/governance/stacks/nextjs.toml +0 -2
  120. package/governance/stacks/node-server.toml +0 -2
  121. package/governance/stacks/python-fastapi.toml +0 -2
  122. package/governance/stacks/react.toml +0 -2
  123. package/src/commands/standards.ts +0 -278
  124. package/src/labels/spelling.ts +0 -120
  125. package/src/standards/audit.ts +0 -146
  126. package/src/standards/list.ts +0 -33
  127. package/src/standards/read.ts +0 -177
  128. package/src/standards/scope.ts +0 -56
  129. package/standards/index.md +0 -10
  130. package/tooling/base/configs/.vscode/extensions.json +0 -9
  131. package/tooling/base/configs/.vscode/settings.json +0 -3
  132. package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
  133. package/tooling/base/seeds/.cspell/tech-stack.txt +0 -25
  134. package/tooling/base/seeds/cspell.json +0 -20
  135. package/tooling/go/seeds/.cspell/tech-stack.txt +0 -16
  136. package/tooling/nextjs/seeds/.cspell/tech-stack.txt +0 -3
  137. package/tooling/php/seeds/.cspell/tech-stack.txt +0 -5
  138. package/tooling/python/seeds/.cspell/tech-stack.txt +0 -18
  139. package/tooling/vite-react/seeds/.cspell/project-terms.txt +0 -1
  140. package/tooling/vite-react/seeds/.cspell/tech-stack.txt +0 -2
  141. package/tooling/web/configs/.vscode/extensions.json +0 -13
  142. package/tooling/web/configs/.vscode/settings.json +0 -10
  143. package/tooling/web/seeds/.cspell/tech-stack.txt +0 -21
@@ -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',
@@ -6,7 +6,6 @@ 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,14 @@ 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, kept on the finding because the three resolve
44
+ * How the citation was written, kept on the finding because the two resolve
46
45
  * against different roots and a reader repairing one needs to know which.
47
46
  *
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.
47
+ * `path` is a backticked repository path. `sibling` is a rule naming another
48
+ * rule by filename alone, which resolves inside the folder the citing rule
49
+ * sits in.
51
50
  */
52
- export type CitationForm = 'path' | 'standard' | 'sibling'
51
+ export type CitationForm = 'path' | 'sibling'
53
52
 
54
53
  /**
55
54
  * What the sweep decided about one citation.
@@ -116,13 +115,6 @@ const FENCE = /^\s*(?:```|~~~)/
116
115
 
117
116
  const FRONTMATTER_DELIMITER = /^---\s*$/
118
117
 
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
118
  /** A rule filename, which is how a rule names a sibling with no folder around it. */
127
119
  const SIBLING_RULE = /^\d{3}-[a-z0-9-]+\.md$/
128
120
 
@@ -146,8 +138,8 @@ function isNotRepositoryPath(span: string): boolean {
146
138
  * This is what separates a citation from the folder and module conventions the
147
139
  * corpus is full of. `next/font`, `try/except`, `react-hooks/set-state-in-effect`,
148
140
  * 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 `claude/standards`
150
- * is a real path this declines to check, which is the bound the report states.
141
+ * slash is a folder rather than a document. The cost is that a real folder path
142
+ * goes unchecked, which is the bound the report states.
151
143
  */
152
144
  function namesAFile(span: string): boolean {
153
145
  if (span.endsWith('/')) return false
@@ -160,10 +152,10 @@ function namesAFile(span: string): boolean {
160
152
  * sweep can resolve.
161
153
  *
162
154
  * The whole span is classified rather than a trailing pattern inside it.
163
- * Extracting `standards/tooling-reference.md` out of
164
- * `internal/standards/tooling-reference.md` and resolving that against the
165
- * standards root manufactures a dead citation out of a file that exists, which
166
- * a session measuring this corpus did before the check was written.
155
+ * Extracting a trailing `<folder>/<name>.md` out of a longer path and resolving
156
+ * that against another root manufactures a dead citation out of a file that
157
+ * exists, which a session measuring this corpus did before the check was
158
+ * written.
167
159
  */
168
160
  export function classifySpan(span: string): CitationForm | undefined {
169
161
  if (isNotRepositoryPath(span)) return undefined
@@ -176,15 +168,6 @@ export function classifySpan(span: string): CitationForm | undefined {
176
168
  * Where a citation could answer, which is wherever the reader's own tools look
177
169
  * and nowhere else.
178
170
  *
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
171
  * A sibling resolves inside the folder the citing rule sits in, since that is
189
172
  * the only place a bare rule filename means anything.
190
173
  */
@@ -193,11 +176,6 @@ function candidatesFor(
193
176
  cited: string,
194
177
  ruleFile: string,
195
178
  ): string[] {
196
- if (form === 'standard') {
197
- const moved = movedStandardPath(cited)
198
-
199
- return [join('standards', `${cited}.md`), ...(moved ? [moved] : [])]
200
- }
201
179
  if (form === 'sibling') return [join(dirname(ruleFile), cited)]
202
180
  return [cited]
203
181
  }
@@ -244,16 +222,6 @@ export function collectCitations(text: string): RawCitation[] {
244
222
  : trimmed
245
223
  const marked = isMarked(lines, index, CITATION_MARKER)
246
224
 
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
225
  // A backticked span is the only carrier a rule writes a citation in. No
258
226
  // rule in either corpus uses a markdown link, and matching running prose
259
227
  // would report every sentence that happens to name a file.
@@ -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 cspell, because a word ban
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 = {
@@ -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/`, `governance/`, or `standards/` names by path,
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/`, `governance/`, or `standards/` names that path. Measured
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
@@ -7,7 +7,6 @@ import {
7
7
  recordDir as resolveRecordDir,
8
8
  recordDirs as resolveRecordDirs,
9
9
  } from '@/roots/record'
10
- import { ATTRIBUTE_MARKER, governedPaths, readScope } from '@/standards/scope'
11
10
  import {
12
11
  TEACH_GLOSSARY,
13
12
  TEACH_MISSION,
@@ -18,34 +17,10 @@ import {
18
17
  WORKSPACE_NAME,
19
18
  } from '@/teach/workspace'
20
19
 
21
- export const RECORD_KINDS = [
22
- 'plans',
23
- 'groundwork',
24
- 'intake',
25
- 'standards',
26
- 'teach',
27
- ] as const
20
+ export const RECORD_KINDS = ['plans', 'groundwork', 'intake', 'teach'] as const
28
21
 
29
22
  export type RecordKind = (typeof RECORD_KINDS)[number]
30
23
 
31
- /**
32
- * The folders standards reads, in precedence order.
33
- *
34
- * It carries two because the corpus authors at the project root and installs
35
- * under `.claude/`. The authoring root wins where both exist, since the
36
- * installed tree is a generated copy here and a finding fixed there is
37
- * overwritten by the next regen. A project that consumed the corpus holds only
38
- * the second, so one order serves both.
39
- *
40
- * Every other kind is a session record and takes the record roots instead,
41
- * resolved by `@/roots/record`. Standards is the one kind that is tracked, so it
42
- * does not move and spells its own two candidates here.
43
- */
44
- const STANDARDS_FOLDERS: readonly string[] = [
45
- 'standards',
46
- join('.claude', 'standards'),
47
- ]
48
-
49
24
  /**
50
25
  * `unknown-kind` is raised at the argument boundary rather than by the walk, and
51
26
  * it sits here because both reach a caller through the same `reason` field. A
@@ -60,7 +35,6 @@ export const FINDING_KINDS = [
60
35
  'name-malformed',
61
36
  'title-missing',
62
37
  'section-missing',
63
- 'scope-unanchored',
64
38
  'entry-unreasoned',
65
39
  'suggestion-missing',
66
40
  'question-unanswerable',
@@ -103,10 +77,6 @@ export type ValidateOutcome = ValidateReport | ValidateRefused
103
77
 
104
78
  /** Every folder a kind would accept, whether or not it is on disk. */
105
79
  export function recordDirs(root: string, kind: RecordKind): string[] {
106
- if (kind === 'standards') {
107
- return STANDARDS_FOLDERS.map((folder) => join(root, folder))
108
- }
109
-
110
80
  return resolveRecordDirs(root, kind)
111
81
  }
112
82
 
@@ -114,19 +84,8 @@ export function recordDirs(root: string, kind: RecordKind): string[] {
114
84
  * The folder a kind reads. The first candidate on disk wins, and the creation
115
85
  * default stands in when none exists, so a refusal and a test fixture both name
116
86
  * the location the kind would be written to.
117
- *
118
- * The default is where this parts company with `dirs[0]`, which the record
119
- * kinds can no longer take: their first candidate is the root the move lands at
120
- * and nothing creates there yet, so a fallback reading it would name a folder no
121
- * verb would ever write. Standards has no such split, since it is tracked and
122
- * its first candidate is where it is authored.
123
87
  */
124
88
  export function recordsDir(root: string, kind: RecordKind): string {
125
- if (kind === 'standards') {
126
- const dirs = recordDirs(root, kind)
127
- return dirs.find((dir) => existsSync(dir)) ?? dirs[0]
128
- }
129
-
130
89
  return resolveRecordDir(root, kind)
131
90
  }
132
91
 
@@ -134,18 +93,6 @@ export function isRecordKind(value: string): value is RecordKind {
134
93
  return (RECORD_KINDS as readonly string[]).includes(value)
135
94
  }
136
95
 
137
- /**
138
- * Whether a kind's folder is shared session scratch at the main worktree root.
139
- *
140
- * The five record folders are, so every session validates the records every
141
- * other session reads. The corpus is tracked instead, so a linked worktree holds
142
- * its own edited copy, and defaulting that kind to the main root would report on
143
- * a tree the session never touched and say nothing about which one it read.
144
- */
145
- export function isSharedScratch(kind: RecordKind): boolean {
146
- return kind !== 'standards'
147
- }
148
-
149
96
  const NONE_IDENTIFIED = 'None identified.'
150
97
  const NUMBERED_FILE = /^\d{2}-[a-z0-9]+(-[a-z0-9]+)*\.md$/
151
98
 
@@ -920,127 +867,6 @@ async function checkWorkspace(dir: string, slug: string): Promise<Finding[]> {
920
867
  ]
921
868
  }
922
869
 
923
- const STANDARD_INDEX = 'index.md'
924
- const STANDARD_FIELDS = ['title', 'description'] as const
925
-
926
- const DOES_NOT_GOVERN = 'Does not govern:'
927
-
928
- /**
929
- * The words a governed path offers a filename. Each segment gives its own word
930
- * and, where it carries a prefix or a placeholder, the parts either side of a
931
- * hyphen, so `.canon/tasks/session-<slug>.md` offers `tasks` and `session`.
932
- *
933
- * A dotted segment gives nothing. It names the folder holding the artifact
934
- * rather than the artifact, and a standard named for it would pass this check
935
- * while naming the container every sibling shares.
936
- */
937
- export function pathWords(path: string): string[] {
938
- const words: string[] = []
939
-
940
- for (const segment of path.split('/')) {
941
- if (segment.startsWith('.')) continue
942
-
943
- const stem = segment.replace(/\.[a-z]+$/i, '').toLowerCase()
944
- words.push(stem)
945
- if (stem.includes('-')) words.push(...stem.split('-'))
946
- }
947
-
948
- return words.filter((word) => /^[a-z]+$/.test(word))
949
- }
950
-
951
- /**
952
- * Accepts the singular and the plural of one word. A standard over a single
953
- * document is named for the document and one over a folder of them is named for
954
- * either, and picking a side would report a conforming half of the corpus.
955
- */
956
- function namesWord(stem: string, word: string): boolean {
957
- return stem === word || `${stem}s` === word || stem === `${word}s`
958
- }
959
-
960
- function checkStandardName(name: string, statement: string): Finding[] {
961
- const paths = governedPaths(statement)
962
-
963
- // The marker is read only where the first sentence backticks nothing, which
964
- // is the catalog's own rule. A statement naming a path publishes that path
965
- // however the rest of the statement describes itself.
966
- if (paths.length === 0) {
967
- if (statement.includes(ATTRIBUTE_MARKER)) return []
968
-
969
- return [
970
- finding(
971
- 'scope-unanchored',
972
- name,
973
- name,
974
- 'backticks no path in its first scope sentence and does not call itself an attribute standard, so it names no artifact to be named for.',
975
- ),
976
- ]
977
- }
978
-
979
- const stem = name.replace(/\.md$/, '')
980
- const words = paths.flatMap(pathWords)
981
-
982
- if (words.some((word) => namesWord(stem, word))) return []
983
-
984
- return [
985
- finding(
986
- 'name-malformed',
987
- name,
988
- name,
989
- `names no part of ${paths.join(', ')}, which is what it governs. A rename reaches every target that installed the corpus and every surface citing it by bare filename.`,
990
- ),
991
- ]
992
- }
993
-
994
- export function checkStandard(name: string, text: string): Finding[] {
995
- const findings: Finding[] = []
996
- const frontmatter = parseFrontmatter(text)
997
-
998
- const missing = STANDARD_FIELDS.filter(
999
- (field) => !readField(frontmatter, field),
1000
- )
1001
-
1002
- if (missing.length > 0) {
1003
- findings.push(
1004
- finding(
1005
- 'frontmatter-incomplete',
1006
- name,
1007
- name,
1008
- `carries no ${missing.join(' and no ')}.`,
1009
- ),
1010
- )
1011
- }
1012
-
1013
- const scope = readScope(text)
1014
-
1015
- // The name derives from the scope statement, so an absent section leaves
1016
- // nothing to derive against. Reporting the name as well would name one defect
1017
- // twice and point the fix at the wrong file.
1018
- if (!scope) {
1019
- return [
1020
- ...findings,
1021
- finding(
1022
- 'section-missing',
1023
- name,
1024
- '## Scope',
1025
- 'is absent, so the standard claims no jurisdiction and can refuse no rule.',
1026
- ),
1027
- ]
1028
- }
1029
-
1030
- if (!scope.lines.some((line) => line.trim().startsWith(DOES_NOT_GOVERN))) {
1031
- findings.push(
1032
- finding(
1033
- 'section-missing',
1034
- name,
1035
- DOES_NOT_GOVERN,
1036
- 'is absent from the scope section, so no boundary names the owner it hands off to.',
1037
- ),
1038
- )
1039
- }
1040
-
1041
- return [...findings, ...checkStandardName(name, scope.statement)]
1042
- }
1043
-
1044
870
  /**
1045
871
  * The three kinds whose records are folders rather than files. Keyed by kind so
1046
872
  * a seventh arrives as an entry here and the compiler names the walk it owes,
@@ -1048,7 +874,7 @@ export function checkStandard(name: string, text: string): Finding[] {
1048
874
  */
1049
875
  const FOLDER_CHECK: Readonly<
1050
876
  Record<
1051
- Exclude<RecordKind, 'plans' | 'standards'>,
877
+ Exclude<RecordKind, 'plans'>,
1052
878
  (dir: string, slug: string) => Promise<Finding[]>
1053
879
  >
1054
880
  > = {
@@ -1066,9 +892,8 @@ async function validateFiles(
1066
892
  dir: string,
1067
893
  kind: RecordKind,
1068
894
  check: (name: string, text: string) => Finding[],
1069
- skip: (file: string) => boolean = () => false,
1070
895
  ): Promise<ValidateReport> {
1071
- const files = (await listMarkdown(dir)).filter((file) => !skip(file))
896
+ const files = await listMarkdown(dir)
1072
897
 
1073
898
  const perFile = await Promise.all(
1074
899
  files.map(async (file) =>
@@ -1080,12 +905,9 @@ async function validateFiles(
1080
905
  }
1081
906
 
1082
907
  /**
1083
- * Reports what every record in one folder claims against the shape its standard
1084
- * fixes. It writes nothing whichever kind runs, and the reason differs by kind.
1085
- * A session record is per-machine scratch with no history behind it, so a repair
1086
- * that guessed wrong could not be undone. A standard installs into every target
1087
- * and is cited by bare filename, so a rename costs more than the file move it
1088
- * looks like.
908
+ * Reports what every record in one folder claims against the shape its reference
909
+ * fixes. It writes nothing, since a session record is per-machine scratch with
910
+ * no history behind it, so a repair that guessed wrong could not be undone.
1089
911
  */
1090
912
  export async function validateRecords(
1091
913
  root: string,
@@ -1102,16 +924,6 @@ export async function validateRecords(
1102
924
 
1103
925
  if (kind === 'plans') return validateFiles(dir, kind, checkPlan)
1104
926
 
1105
- // The walk stays flat, matching install and the catalog.
1106
- if (kind === 'standards') {
1107
- return validateFiles(
1108
- dir,
1109
- kind,
1110
- checkStandard,
1111
- (file) => file === STANDARD_INDEX,
1112
- )
1113
- }
1114
-
1115
927
  const folders = await listFolders(dir)
1116
928
  const check = FOLDER_CHECK[kind]
1117
929
  const perFolder = await Promise.all(
@@ -220,11 +220,6 @@ function install(
220
220
  fail(`Could not install ${label} into the sandbox.`)
221
221
  }
222
222
 
223
- /**
224
- * No standards install. The corpus installs into no target, so a scenario
225
- * driving a skill that reads one takes the `canon standards <name>` path a real
226
- * project takes.
227
- */
228
223
  function setupAssets(
229
224
  root: string,
230
225
  sandbox: string,
@@ -13,8 +13,7 @@ import { join } from 'node:path'
13
13
  *
14
14
  * What it does not cover is the plugin, which a marketplace install reads live
15
15
  * from `claude/` rather than from a tarball. That folder is a `files` entry
16
- * too, and its `standards` symlink resolves into a tree the field lists in its
17
- * own right, so both routes land inside the same corpus.
16
+ * too, so both routes land inside the same corpus.
18
17
  */
19
18
  export type ShipEntries =
20
19
  /** The field declares a corpus, which is what this check reads. */
@@ -200,8 +200,7 @@ export function resolveWithin(
200
200
  /**
201
201
  * Re-tests containment after following symlinks. `resolveWithin` is lexical
202
202
  * and `resolve` does not follow a link, so a link inside the root clears that
203
- * test while the file it points at sits outside. This repository is a live
204
- * instance, since `claude/standards` is a link out of `claude/`.
203
+ * test while the file it points at sits outside.
205
204
  *
206
205
  * Only a path that exists is checked, because a link can be followed only once
207
206
  * there is something on the other end, and a path that resolves to nothing is
@@ -10,8 +10,7 @@ import { isMarked } from '@/sweep/exempt-marker'
10
10
  * on purpose rather than by omission. What decides membership here is whether
11
11
  * anything serves the file to a reader who does not hold this repository: a
12
12
  * skill body loads into a session, a docs page is read through `canon docs`, a
13
- * standard through `canon standards`, a rule through a glob match, and a seed
14
- * through an install. A `src/` doc comment
13
+ * rule through a glob match, and a seed through an install. A `src/` doc comment
15
14
  * is read by someone working on the toolkit, in this checkout, where every
16
15
  * number resolves.
17
16
  *
@@ -19,18 +18,12 @@ import { isMarked } from '@/sweep/exempt-marker'
19
18
  * `src/design/base.css` and `src/design/tokens.ts` carry sixteen values shaped
20
19
  * `#191512`, which no width or boundary rule separates from a pull request
21
20
  * number, so telling them apart would need the line's syntactic position.
22
- *
23
- * `claude/standards` is a symlink into one of the roots below, and a walk that
24
- * followed it would read that corpus twice and report every finding in it
25
- * under two paths. Nothing is lost by not following, since it is a corpus here
26
- * in its own right.
27
21
  */
28
22
  export const SHIPPED_CORPORA = [
29
23
  'claude',
30
24
  'docs',
31
25
  'governance',
32
26
  'scripts',
33
- 'standards',
34
27
  'tooling',
35
28
  ] as const
36
29
 
@@ -146,37 +139,15 @@ const SAME_REPOSITORY =
146
139
  const DOCS_PATH = /(?<![\w./-])docs\/[^\s`)\]]*\.md\b/g
147
140
 
148
141
  /**
149
- * A bare `standards/<name>.md` citation from a body under `claude/skills/`,
150
- * which `598-authoring-layout.md` fixes as the broken form there:
151
- * `${CLAUDE_SKILL_DIR}` is what resolves off the `claude/standards` symlink
152
- * in every plugin cache, and a raw path has nothing to expand it.
153
- *
154
- * Scoped to that one corpus rather than every `isShippedCorpus` reads,
155
- * because `docs/agents/` and `docs/workflow/` carry the identical
156
- * `standards/<name>.md` shape correctly: a docs page resolves from this
157
- * checkout's own root rather than through a skill's `${CLAUDE_SKILL_DIR}`,
158
- * so the same string is the fix in one corpus and the defect in the other.
159
- * Widening the pattern to every corpus turns it into a mass false-positive
160
- * over the dozens of correct citations those two folders carry.
161
- *
162
- * The same leading-boundary discipline as `DOCS_PATH` excludes the already
163
- * correct form: a `/` sits ahead of `standards` in
164
- * `${CLAUDE_SKILL_DIR}/../../standards/<name>.md`, which the negative
165
- * lookbehind rejects, so a citation already rewritten to the resolving form
166
- * does not fail this check a second time.
142
+ * A call to `canon standards`, a verb no installed CLI carries any more. A
143
+ * shipped body naming it sends a target reader to an unknown-command error,
144
+ * so every corpus reports it, `docs/` included. A page saying the verb is gone
145
+ * carries the reference marker on that line.
167
146
  *
168
- * Reported unless `isStandardsPathReportable` reads the match as a
169
- * deliberate placeholder rather than a real citation. A body illustrating
170
- * the shape a project's own `standards/<name>.md` takes writes a bracketed
171
- * token this pattern also matches, such as `standards/<slug>.md`
172
- * in a skill that drafts one, which names no real file. A bare,
173
- * non-bracketed match now reports whether or not the file exists, unlike
174
- * `DOCS_PATH`, which still gates on existence: a same-repository citation
175
- * under this corpus names no target project's own tree the way a `docs/`
176
- * path can, so shape alone separates a placeholder from a broken citation
177
- * here with no false-positive class existence was catching.
147
+ * The trailing lookahead keeps a hyphenated token such as a `standards-audit`
148
+ * page name from matching, since that names a file rather than a call.
178
149
  */
179
- const STANDARDS_PATH = /(?<![\w./-])standards\/[^\s`)\]]*\.md\b/g
150
+ const STANDARDS_CALL = /\bcanon standards(?![\w-])/g
180
151
 
181
152
  /**
182
153
  * A phase-label-shaped token: exactly two numeric groups, with a negative
@@ -223,7 +194,7 @@ export interface ShippedReference {
223
194
  | 'pull-request'
224
195
  | 'commit'
225
196
  | 'docs-path'
226
- | 'standards-path'
197
+ | 'retired-verb'
227
198
  | 'phase-label'
228
199
  | 'rule-path'
229
200
  /** The reference as written, so a report names the token to qualify. */
@@ -272,50 +243,31 @@ function isDocsPathReportable(
272
243
  }
273
244
 
274
245
  /**
275
- * Whether `file` sits in the one corpus `STANDARDS_PATH` and `RULE_PATH`
276
- * both gate: a shipped skill body, minus its own `REQUIREMENT.md`.
246
+ * Whether `file` sits in the one corpus `RULE_PATH` gates: a shipped skill
247
+ * body, minus its own `REQUIREMENT.md`.
277
248
  *
278
- * Both patterns share this scope because both bans live in
279
- * `598-authoring-layout.md`, stated for the same reader: a session loading
280
- * the `SKILL.md` body a target actually receives. `REQUIREMENT.md` is
281
- * excluded for the reason that file states there, since a maintainer or an
282
- * audit command reads it rather than a session loading it, so neither
283
- * resolver rule this pair enforces ever applies to it. One predicate serves
284
- * both call sites rather than two copies drifting apart with nothing
285
- * comparing them.
249
+ * The ban lives in `598-authoring-layout.md`, stated for one reader: a
250
+ * session loading the `SKILL.md` body a target actually receives.
251
+ * `REQUIREMENT.md` is excluded for the reason that file states there, since a
252
+ * maintainer or an audit command reads it rather than a session loading it,
253
+ * so the resolver rule never applies to it.
286
254
  */
287
255
  function isSkillBodyScope(file: string): boolean {
288
256
  return file.startsWith('claude/skills/') && !file.endsWith('/REQUIREMENT.md')
289
257
  }
290
258
 
291
- /**
292
- * Whether a `STANDARDS_PATH` match should be reported: every match that is
293
- * not a placeholder, whether or not the named file exists.
294
- *
295
- * Existence used to gate this the same way `isDocsPathReportable` gates its
296
- * corpus, which conflated a deliberate placeholder with a genuinely broken,
297
- * real-looking citation: both fail existence and both passed muted. A bare
298
- * `standards/<name>.md` names no target-project tree the way a `docs/` path
299
- * can, so shape alone separates the two classes here with no false-positive
300
- * class existence was catching.
301
- */
302
- function isStandardsPathReportable(path: string): boolean {
303
- return !isPlaceholderPath(path)
304
- }
305
-
306
259
  /**
307
260
  * Every reference in one shipped file that no marker mutes.
308
261
  *
309
262
  * The corpus walk is deliberately absent, which lets most of the shape be
310
263
  * tested against a string rather than against a fixture. That is the seam
311
264
  * `headingCitationsIn` draws in `src/claude/skills-headings.ts` and `citationsIn`
312
- * draws in `skills-reach.ts`. `DOCS_PATH` and `STANDARDS_PATH` are the two
313
- * patterns that still need a filesystem, since resolving against this
314
- * checkout is the only thing that separates a real citation from an
315
- * illustration for either, so `referencesIn` takes `root` as the one
316
- * caller-supplied exception to that rule. `root` is required rather than
317
- * defaulted, since a caller that dropped it silently would report zero
318
- * findings for both rather than raising, which is the wrong failure
265
+ * draws in `skills-reach.ts`. `DOCS_PATH` is the one pattern that still
266
+ * needs a filesystem, since resolving against this checkout is the only thing
267
+ * that separates a real citation from an illustration, so `referencesIn`
268
+ * takes `root` as the one caller-supplied exception to that rule. `root` is
269
+ * required rather than defaulted, since a caller that dropped it silently
270
+ * would report zero findings rather than raising, which is the wrong failure
319
271
  * direction for a gate.
320
272
  *
321
273
  * The unit is the match rather than the line, unlike those two, because one
@@ -375,16 +327,13 @@ export function referencesIn(
375
327
  })
376
328
  }
377
329
 
378
- if (isSkillBodyScope(file)) {
379
- for (const match of line.matchAll(STANDARDS_PATH)) {
380
- if (!isStandardsPathReportable(match[0])) continue
381
- references.push({
382
- file,
383
- line: index + 1,
384
- kind: 'standards-path',
385
- text: match[0],
386
- })
387
- }
330
+ for (const match of line.matchAll(STANDARDS_CALL)) {
331
+ references.push({
332
+ file,
333
+ line: index + 1,
334
+ kind: 'retired-verb',
335
+ text: match[0],
336
+ })
388
337
  }
389
338
 
390
339
  for (const match of line.matchAll(PHASE_LABEL)) {