@erclx/canon 4.78.0 → 4.79.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 (140) hide show
  1. package/README.md +9 -9
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/canon-operator/SKILL.md +1 -1
  4. package/claude/skills/create-standard/SKILL.md +1 -1
  5. package/claude/skills/design-extract/REQUIREMENT.md +4 -4
  6. package/claude/skills/design-extract/SKILL.md +11 -11
  7. package/claude/skills/docs-fold/SKILL.md +14 -14
  8. package/claude/skills/docs-fold/references/anchor-sweep.md +1 -1
  9. package/claude/skills/docs-fold/references/wireframe-sweep.md +4 -4
  10. package/claude/skills/draft-context/REQUIREMENT.md +6 -6
  11. package/claude/skills/draft-context/SKILL.md +8 -6
  12. package/claude/skills/draft-diagram/SKILL.md +5 -5
  13. package/claude/skills/draft-identity/SKILL.md +3 -3
  14. package/claude/skills/draft-readme/REQUIREMENT.md +1 -1
  15. package/claude/skills/draft-screencast/SKILL.md +1 -1
  16. package/claude/skills/draft-wireframes/REQUIREMENT.md +7 -7
  17. package/claude/skills/draft-wireframes/SKILL.md +7 -5
  18. package/claude/skills/memory-capture/REQUIREMENT.md +1 -1
  19. package/claude/skills/memory-capture/SKILL.md +6 -6
  20. package/claude/skills/memory-review/SKILL.md +1 -1
  21. package/claude/skills/migration-claude-md/REQUIREMENT.md +3 -3
  22. package/claude/skills/migration-claude-md/SKILL.md +7 -5
  23. package/claude/skills/migration-context/REQUIREMENT.md +3 -3
  24. package/claude/skills/migration-context/SKILL.md +11 -9
  25. package/claude/skills/plan-feature/SKILL.md +4 -4
  26. package/claude/skills/plan-groundwork/SKILL.md +2 -2
  27. package/claude/skills/plan-intake/SKILL.md +2 -2
  28. package/claude/skills/project-commands/SKILL.md +2 -2
  29. package/claude/skills/review-branch/SKILL.md +3 -3
  30. package/claude/skills/review-pr/SKILL.md +2 -2
  31. package/claude/skills/role-orchestrator/SKILL.md +1 -1
  32. package/claude/skills/role-planner/SKILL.md +1 -1
  33. package/claude/skills/session-relay/REQUIREMENT.md +1 -1
  34. package/claude/skills/session-resume/SKILL.md +1 -1
  35. package/claude/skills/setup-gov/SKILL.md +1 -1
  36. package/claude/skills/setup-init/SKILL.md +1 -1
  37. package/claude/skills/teach-workspace/references/promotion.md +5 -5
  38. package/claude/skills/ux-audit/SKILL.md +5 -5
  39. package/claude/skills/ux-measure/SKILL.md +1 -1
  40. package/docs/agents/commands.md +7 -2
  41. package/docs/agents/context-audit-checks.md +3 -3
  42. package/docs/agents/context-audit.md +2 -2
  43. package/docs/agents/counts.md +3 -3
  44. package/docs/agents/design-board.md +3 -3
  45. package/docs/agents/docs.md +3 -3
  46. package/docs/agents/indexes.md +1 -1
  47. package/docs/agents/install-and-sync.md +1 -1
  48. package/docs/agents/review-classification.md +1 -1
  49. package/docs/agents/routing.md +2 -2
  50. package/docs/agents/rule-citations.md +1 -1
  51. package/docs/agents/sandbox.md +1 -1
  52. package/docs/agents/scripting.md +1 -1
  53. package/docs/agents/skills-audit.md +1 -1
  54. package/docs/agents/skills-reach.md +4 -4
  55. package/docs/target-projects.md +10 -10
  56. package/docs/workflow/ai-workflow.md +15 -13
  57. package/docs/workflow/operating-model.md +2 -2
  58. package/docs/workflow/visual-design-workflow.md +8 -8
  59. package/governance/rules/claude/510-context.md +3 -2
  60. package/governance/rules/claude/520-wireframes.md +3 -2
  61. package/governance/rules/claude/530-requirements.md +2 -1
  62. package/governance/rules/claude/540-architecture.md +2 -1
  63. package/governance/rules/claude/550-design.md +2 -1
  64. package/governance/rules/core/025-indexes.md +1 -1
  65. package/governance/rules/core/045-memory.md +1 -1
  66. package/package.json +1 -1
  67. package/scripts/core/check-ignore-parity.sh +1 -1
  68. package/scripts/core/check-seed-independence.sh +5 -2
  69. package/scripts/core/install-check.sh +1 -1
  70. package/scripts/core/list-seed-roots.sh +3 -3
  71. package/scripts/core/regen-hero.sh +1 -1
  72. package/scripts/docs/list.sh +3 -3
  73. package/scripts/lib/sandbox-git.sh +1 -1
  74. package/scripts/lib/tooling.sh +6 -4
  75. package/snippets/claude/figma-steps.md +1 -1
  76. package/src/audits/catalog.ts +2 -2
  77. package/src/autoship/paths.ts +1 -1
  78. package/src/claude/cases/authoring.ts +2 -2
  79. package/src/claude/cases/setup.ts +1 -1
  80. package/src/claude/routing.ts +1 -1
  81. package/src/claude/seeds-list.ts +1 -1
  82. package/src/claude/seeds.ts +62 -28
  83. package/src/claude/skills-reach.ts +3 -3
  84. package/src/commands/claude.ts +1 -1
  85. package/src/commands/context.ts +8 -8
  86. package/src/commands/design.ts +1 -1
  87. package/src/commands/gov.ts +1 -1
  88. package/src/commands/migrate.ts +182 -1
  89. package/src/context/audit.ts +1 -1
  90. package/src/context/citations.ts +7 -5
  91. package/src/context/folders.ts +2 -2
  92. package/src/counts/scan.ts +4 -4
  93. package/src/design/board.ts +5 -5
  94. package/src/design/components.ts +2 -2
  95. package/src/design/css.ts +1 -1
  96. package/src/design/document.ts +1 -1
  97. package/src/design/regen.ts +1 -1
  98. package/src/design/render.ts +1 -1
  99. package/src/design/tokens.ts +2 -2
  100. package/src/docs/read.ts +9 -3
  101. package/src/gate/measures.ts +10 -7
  102. package/src/gate/stages.ts +4 -4
  103. package/src/markdown/scan.ts +1 -1
  104. package/src/migrate/surface-roots.ts +458 -0
  105. package/src/record-root.ts +6 -4
  106. package/src/sandbox/census.ts +1 -1
  107. package/src/shipped/references.ts +3 -3
  108. package/src/surface-root.ts +39 -7
  109. package/src/sync/layout.ts +10 -6
  110. package/src/sync/reverse.ts +1 -1
  111. package/src/tooling/inject.ts +5 -3
  112. package/src/tooling/scan.ts +4 -1
  113. package/standards/architecture.md +4 -4
  114. package/standards/context.md +7 -7
  115. package/standards/design.md +3 -3
  116. package/standards/diagrams.md +6 -6
  117. package/standards/index.md +5 -5
  118. package/standards/markdown.md +2 -2
  119. package/standards/publish.md +3 -3
  120. package/standards/readme.md +1 -1
  121. package/standards/requirements.md +4 -4
  122. package/standards/skill.md +2 -2
  123. package/standards/tasks.md +1 -1
  124. package/standards/wiki.md +1 -1
  125. package/standards/wireframes.md +8 -8
  126. package/tooling/astro/reference.md +3 -3
  127. package/tooling/base/reference.md +3 -3
  128. package/tooling/claude/reference.md +7 -3
  129. package/tooling/claude/seeds/CLAUDE.md +9 -9
  130. package/tooling/nextjs/reference.md +2 -2
  131. package/tooling/python/reference.md +1 -1
  132. package/tooling/vite-react/reference.md +2 -2
  133. package/tooling/web/reference.md +2 -2
  134. /package/tooling/base/seeds/{.claude → canon}/context/ci.md +0 -0
  135. /package/tooling/base/seeds/{.claude → canon}/context/development.md +0 -0
  136. /package/tooling/base/seeds/{.claude → canon}/context/index.md +0 -0
  137. /package/tooling/claude/seeds/{.claude → canon}/ARCHITECTURE.md +0 -0
  138. /package/tooling/claude/seeds/{.claude → canon}/DESIGN.md +0 -0
  139. /package/tooling/claude/seeds/{.claude → canon}/REQUIREMENTS.md +0 -0
  140. /package/tooling/claude/seeds/{.claude → canon}/wireframes/index.md +0 -0
@@ -23,13 +23,15 @@ list_tooling_stacks() {
23
23
  done < <(find "$tooling_dir" -mindepth 1 -maxdepth 1 -type d | sort)
24
24
  }
25
25
 
26
- # Seed roots that carry a `.claude/`, emitted relative to `PROJECT_ROOT`. Every
27
- # stage measuring seed content discovers through this rather than naming a stack,
28
- # so a stack seeding `.claude/` later arrives covered with no edit to any caller.
26
+ # Seed roots that carry a `.claude/` or a `canon/`, emitted relative to
27
+ # `PROJECT_ROOT`. Every stage measuring seed content discovers through this
28
+ # rather than naming a stack, so a stack seeding either root later arrives
29
+ # covered with no edit to any caller. A stack seeding only tracked surfaces
30
+ # carries `canon/` alone, which a `.claude/` test would drop from both stages.
29
31
  collect_seed_roots() {
30
32
  local dir
31
33
  for dir in "$PROJECT_ROOT"/tooling/*/seeds; do
32
- [ -d "$dir/.claude" ] || continue
34
+ [ -d "$dir/.claude" ] || [ -d "$dir/canon" ] || continue
33
35
  printf '%s\n' "${dir#"$PROJECT_ROOT"/}"
34
36
  done
35
37
  }
@@ -1,4 +1,4 @@
1
- Read the relevant spec from `.claude/DESIGN.md` and any other design files needed. Write step-by-step Figma instructions to produce the component exactly.
1
+ Read the relevant spec from `canon/DESIGN.md` and any other design files needed. Write step-by-step Figma instructions to produce the component exactly.
2
2
 
3
3
  For each step, specify:
4
4
 
@@ -490,8 +490,8 @@ export const AUDITS: readonly AuditSpec[] = [
490
490
  corpus: 'tracked',
491
491
  // The one reason this verb refuses for that is an absence rather than a
492
492
  // break, on the same test the reach check and the skill audit take: no
493
- // target adopts `.claude/context/`, `.canon/diagrams/`, and
494
- // `.claude/wireframes/` all at once, so without the allowance every such
493
+ // target adopts `canon/context/`, `.canon/diagrams/`, and
494
+ // `canon/wireframes/` all at once, so without the allowance every such
495
495
  // project reports the verb unmeasured on every run and never changes.
496
496
  absentReasons: ['no-folders'] satisfies ContextAuditRefusal[],
497
497
  counts: contextCounts,
@@ -7,7 +7,7 @@
7
7
  * application three times, so the set moved here and the body now calls a verb
8
8
  * that reads it. Being machine-parsed makes it permanently exempt from any
9
9
  * later design that folds a standard back into the surface citing it, per the
10
- * machine-parsed clause in `.claude/ARCHITECTURE.md`.
10
+ * machine-parsed clause in `canon/ARCHITECTURE.md`.
11
11
  */
12
12
 
13
13
  /**
@@ -48,12 +48,12 @@ export const AUTHORING_CASES: readonly SkillCase[] = [
48
48
  },
49
49
  {
50
50
  prompt:
51
- 'Write a context entry for the payments domain, there is no .claude/context page for it yet.',
51
+ 'Write a context entry for the payments domain, there is no canon/context page for it yet.',
52
52
  expect: 'draft-context',
53
53
  },
54
54
  {
55
55
  prompt:
56
- 'Draft a wireframe for the settings panel, nothing under .claude/wireframes covers that surface yet.',
56
+ 'Draft a wireframe for the settings panel, nothing under canon/wireframes covers that surface yet.',
57
57
  expect: 'draft-wireframes',
58
58
  },
59
59
  {
@@ -72,7 +72,7 @@ export const SETUP_CASES: readonly SkillCase[] = [
72
72
  },
73
73
  // The performing skill wins a phrase naming its operation over the front
74
74
  // door's own routing framing, even though canon-operator's description
75
- // quotes a phrase near this one. See .claude/context/cli/audits.md.
75
+ // quotes a phrase near this one. See canon/context/cli/audits.md.
76
76
  {
77
77
  prompt:
78
78
  "Whatever the right toolkit command is, get this project's rules installed.",
@@ -82,7 +82,7 @@ export function namedPaths(line: string): string[] {
82
82
  * The openable part of a token, which for a shape is the folder above the
83
83
  * placeholder.
84
84
  *
85
- * `.claude/context/<domain>.md` names `.claude/context/` and nothing narrower,
85
+ * `canon/context/<domain>.md` names `canon/context/` and nothing narrower,
86
86
  * so dropping the whole token would report the section that carries it as
87
87
  * naming no path at all. A token whose placeholder sits in the first segment
88
88
  * has no openable prefix and is dropped.
@@ -19,7 +19,7 @@ export interface SeedListingWithContent {
19
19
  /**
20
20
  * Reads the same plan `canon claude init` applies. The bash re-globbed the seeds
21
21
  * directory with its own hard-coded subdirectory list, which had drifted: it
22
- * never listed `.claude/context/`, so a seed init installs went unreported.
22
+ * never listed `canon/context/`, so a seed init installs went unreported.
23
23
  *
24
24
  * The target is irrelevant to a listing, so it resolves against the source
25
25
  * root and only the source-side fields are read.
@@ -1,18 +1,21 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
3
- import { dirname, join } from 'node:path'
3
+ import { dirname, join, relative } from 'node:path'
4
4
  import { copyPreservingMode } from '@/copy'
5
5
  import { creationRel, isRecordEntry } from '@/record-root'
6
6
  import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
7
+ import { SURFACE_ENTRIES, surfaceDir } from '@/surface-root'
7
8
 
8
9
  const SEEDS_DIR = join('tooling', 'claude', 'seeds')
9
10
  const CLAUDE_DIR = '.claude'
11
+ const SURFACE_DIR = 'canon'
10
12
  const CLAUDE_MD = 'CLAUDE.md'
11
13
  const HOOKS = 'hooks'
12
14
  /**
13
- * Seed subdirectories under `.claude/`. Exported because each one replaced a
14
- * single file of the same stem in an older layout, which is what
15
- * `@/sync/layout` pairs a target against to find a superseded artifact.
15
+ * Seed subdirectories, authored under `.claude/` or, for a tracked surface,
16
+ * under `canon/`. Exported because each one replaced a single file of the same
17
+ * stem in an older layout, which is what `@/sync/layout` pairs a target against
18
+ * to find a superseded artifact.
16
19
  */
17
20
  export const SUBDIRS: readonly string[] = [
18
21
  HOOKS,
@@ -48,10 +51,26 @@ function seedsRoot(root: string): string {
48
51
  return join(root, SEEDS_DIR)
49
52
  }
50
53
 
54
+ function isSurfaceEntry(name: string): boolean {
55
+ return SURFACE_ENTRIES.includes(name)
56
+ }
57
+
58
+ /**
59
+ * Where a surface seed installs, relative to the target.
60
+ *
61
+ * Resolved the way a read resolves rather than fixed at `canon/`. A target that
62
+ * still holds the surface under `.claude/` receives the seed beside the copy it
63
+ * has, since a fresh `canon/` folder there would win read precedence and hide
64
+ * every entry that target already wrote.
65
+ */
66
+ function surfaceRel(target: string, entry: string): string {
67
+ return relative(target, surfaceDir(target, entry))
68
+ }
69
+
51
70
  /**
52
71
  * Lists a single directory level, sorted the way `find -maxdepth 1 -type f |
53
- * sort` was. Bun.Glob skips dotfiles without `dot`, and every path here sits
54
- * under `.claude`, so omitting it would match nothing.
72
+ * sort` was. Bun.Glob skips dotfiles without `dot`, and the `.claude` source
73
+ * sits under a dotted name, so omitting it would match nothing there.
55
74
  */
56
75
  function listLevel(dir: string): string[] {
57
76
  if (!existsSync(dir)) return []
@@ -61,19 +80,19 @@ function listLevel(dir: string): string[] {
61
80
  }
62
81
 
63
82
  /**
64
- * Builds the seed list in the order the bash scanned it: the `.claude` root
65
- * level, then each subdirectory, then the project-level `CLAUDE.md`. Order is
66
- * load-bearing because it is also the order the timeline prints.
83
+ * Builds the seed list in the order the timeline prints it: the `.claude` root
84
+ * level, the `canon` root level, then each subdirectory from whichever source
85
+ * authors it, then the project-level `CLAUDE.md`.
67
86
  */
68
87
  export function planSeeds(root: string, target: string): SeedEntry[] {
69
- const source = join(seedsRoot(root), CLAUDE_DIR)
70
- const destDir = join(target, CLAUDE_DIR)
88
+ const claudeSource = join(seedsRoot(root), CLAUDE_DIR)
89
+ const surfaceSource = join(seedsRoot(root), SURFACE_DIR)
71
90
  const seeds: Seed[] = []
72
91
 
73
- for (const name of listLevel(source)) {
92
+ for (const name of listLevel(claudeSource)) {
74
93
  seeds.push({
75
- src: join(source, name),
76
- dest: join(destDir, name),
94
+ src: join(claudeSource, name),
95
+ dest: join(target, CLAUDE_DIR, name),
77
96
  scanLabel: name,
78
97
  applyLabel: join(CLAUDE_DIR, name),
79
98
  scope: 'claude',
@@ -81,23 +100,38 @@ export function planSeeds(root: string, target: string): SeedEntry[] {
81
100
  })
82
101
  }
83
102
 
103
+ for (const name of listLevel(surfaceSource)) {
104
+ const rel = surfaceRel(target, name)
105
+ seeds.push({
106
+ src: join(surfaceSource, name),
107
+ dest: join(target, rel),
108
+ scanLabel: name,
109
+ applyLabel: rel,
110
+ scope: 'claude',
111
+ executable: false,
112
+ })
113
+ }
114
+
84
115
  for (const subdir of SUBDIRS) {
85
- // The seed tree authors every subdirectory under `.claude/`, and three of
86
- // them are record folders that install under the record root instead. A
87
- // target that has not migrated resolves back to `.claude/`, so the same seed
88
- // lands beside the records already there rather than opening a second root.
89
- // Scaffolding one under `.claude/` now would also land it outside the single
90
- // `.canon/` ignore entry a target receives, which tracks the memory pen.
91
- const installRel = isRecordEntry(subdir)
92
- ? creationRel(target, subdir)
93
- : join(CLAUDE_DIR, subdir)
94
-
95
- for (const name of listLevel(join(source, subdir))) {
96
- const rel = `${subdir}/${name}`
116
+ const surface = isSurfaceEntry(subdir)
117
+ const source = join(surface ? surfaceSource : claudeSource, subdir)
118
+
119
+ // Three of the `.claude/` subdirectories are record folders that install
120
+ // under the record root instead. A target that has not migrated resolves
121
+ // back to `.claude/`, so the same seed lands beside the records already
122
+ // there rather than opening a second root, and never outside the single
123
+ // `.canon/` ignore entry a target receives.
124
+ const installRel = surface
125
+ ? surfaceRel(target, subdir)
126
+ : isRecordEntry(subdir)
127
+ ? creationRel(target, subdir)
128
+ : join(CLAUDE_DIR, subdir)
129
+
130
+ for (const name of listLevel(source)) {
97
131
  seeds.push({
98
- src: join(source, subdir, name),
132
+ src: join(source, name),
99
133
  dest: join(target, installRel, name),
100
- scanLabel: rel,
134
+ scanLabel: `${subdir}/${name}`,
101
135
  applyLabel: join(installRel, name),
102
136
  scope: 'claude',
103
137
  executable: subdir === HOOKS,
@@ -17,7 +17,7 @@ import { SURFACE_ROOTS } from '@/surface-root'
17
17
  * `docs/agents/` is the exception, being the CLI contract pages that exist
18
18
  * here alone.
19
19
  *
20
- * `.claude/context/` carries both surface-root spellings, since this list
20
+ * The context folder carries both surface-root spellings, since this list
21
21
  * decides whether a shipped body names this repository's own tree and a body
22
22
  * naming either root is doing that regardless of which root a given checkout
23
23
  * carries.
@@ -51,7 +51,7 @@ const TOKEN = /`([^`\s]+)`/g
51
51
  * A path a reader could open, which is the only kind worth measuring.
52
52
  *
53
53
  * Requires an extension and a separator, and admits no `<`, `$`, or `*`. A
54
- * body writes `.claude/context/<domain>.md` to name a shape rather than a
54
+ * body writes `canon/context/<domain>.md` to name a shape rather than a
55
55
  * file, and `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` to resolve
56
56
  * against the plugin root, which is self-contained by construction.
57
57
  */
@@ -88,7 +88,7 @@ export function isQualified(line: string): boolean {
88
88
  * The roots that belong to the toolkit rather than to the reader, read
89
89
  * against the corpus being measured.
90
90
  *
91
- * A target's `.claude/context/` is the reader's own tree. A seed put the
91
+ * A target's `canon/context/` is the reader's own tree. A seed put the
92
92
  * entries there and the project owns them afterward, so a body under
93
93
  * `.claude/skills/` citing one names a file its reader holds. Measuring it
94
94
  * would report a correct citation on every run, which is exactly why `src/`
@@ -435,7 +435,7 @@ export function register(program: Command): void {
435
435
  ' carrying that corpus alone. A cited path counts when it sits under',
436
436
  ' an authoring root no install channel delivers and the project',
437
437
  ' holds it. A path under src/, scripts/, or bare docs/ names the',
438
- " reader's own tree and is not measured, and .claude/context/ joins",
438
+ " reader's own tree and is not measured, and canon/context/ joins",
439
439
  " them when the corpus read is a project's own.",
440
440
  '',
441
441
  'Exit codes:',
@@ -455,13 +455,13 @@ function reportReferenceForm(
455
455
  const scoped = folders.filter(matchesSiblings)
456
456
  if (scoped.length === 0) {
457
457
  logInfo(
458
- `Out of scope. A bare name is matched against the siblings of a domain split into a folder, and no audited folder under .claude/${PROVENANCE_FOLDER}/ is one.`,
458
+ `Out of scope. A bare name is matched against the siblings of a domain split into a folder, and no audited folder under canon/${PROVENANCE_FOLDER}/ is one.`,
459
459
  )
460
460
  return
461
461
  }
462
462
 
463
463
  logInfo(
464
- `Covers ${plural(scoped.length, 'split folder')} under .claude/${PROVENANCE_FOLDER}/, whose standard asks a reference to spell its path.`,
464
+ `Covers ${plural(scoped.length, 'split folder')} under canon/${PROVENANCE_FOLDER}/, whose standard asks a reference to spell its path.`,
465
465
  )
466
466
  logInfo(
467
467
  'The flat folder is out of reach, since a domain filename there is shared by seeds and other trees.',
@@ -513,13 +513,13 @@ function reportSections(
513
513
  const governed = folders.filter(governsContent)
514
514
  if (governed.length === 0) {
515
515
  logInfo(
516
- `Out of scope. The list is stated in the standard governing .claude/${PROVENANCE_FOLDER}/, and no audited folder is that one.`,
516
+ `Out of scope. The list is stated in the standard governing canon/${PROVENANCE_FOLDER}/, and no audited folder is that one.`,
517
517
  )
518
518
  return
519
519
  }
520
520
 
521
521
  logInfo(
522
- `Covers .claude/${PROVENANCE_FOLDER}/ alone, whose standard requires ${REQUIRED_SECTIONS.join(' and ')}.`,
522
+ `Covers canon/${PROVENANCE_FOLDER}/ alone, whose standard requires ${REQUIRED_SECTIONS.join(' and ')}.`,
523
523
  )
524
524
  logInfo(
525
525
  'A heading at any level counts. Each entry answers for itself, except in a domain split across a folder, where a sibling answers for the rest.',
@@ -635,13 +635,13 @@ function reportProvenance(
635
635
  const governed = folders.filter(governsContent)
636
636
  if (governed.length === 0) {
637
637
  logInfo(
638
- `Out of scope. The rule is stated in the standard governing .claude/${PROVENANCE_FOLDER}/, and no audited folder is that one.`,
638
+ `Out of scope. The rule is stated in the standard governing canon/${PROVENANCE_FOLDER}/, and no audited folder is that one.`,
639
639
  )
640
640
  return
641
641
  }
642
642
 
643
643
  logInfo(
644
- `Covers .claude/${PROVENANCE_FOLDER}/ alone, whose standard carries the rule. The sibling standards do not restate it.`,
644
+ `Covers canon/${PROVENANCE_FOLDER}/ alone, whose standard carries the rule. The sibling standards do not restate it.`,
645
645
  )
646
646
  logInfo(
647
647
  'Fenced blocks are excluded, and so is a date whose clause stamps a measurement. A marker is a judgment, never a defect.',
@@ -697,7 +697,7 @@ function reportNarration(
697
697
  const governed = folders.filter(governsContent)
698
698
  if (governed.length === 0) {
699
699
  logInfo(
700
- `Out of scope. The rule is stated in the standard governing .claude/${PROVENANCE_FOLDER}/, and no audited folder is that one.`,
700
+ `Out of scope. The rule is stated in the standard governing canon/${PROVENANCE_FOLDER}/, and no audited folder is that one.`,
701
701
  )
702
702
  return
703
703
  }
@@ -710,7 +710,7 @@ function reportNarration(
710
710
  }
711
711
 
712
712
  logInfo(
713
- `Covers .claude/${PROVENANCE_FOLDER}/ alone, reading ${plural(narration.pronouns.length, 'pronoun')} and ${plural(narration.verbs.length, 'verb')} from ${narration.source}.`,
713
+ `Covers canon/${PROVENANCE_FOLDER}/ alone, reading ${plural(narration.pronouns.length, 'pronoun')} and ${plural(narration.verbs.length, 'verb')} from ${narration.source}.`,
714
714
  )
715
715
  logInfo(
716
716
  'A rejected alternative is a legitimate hit, since the standard keeps what was tried and why it lost.',
@@ -28,7 +28,7 @@ export function register(program: Command): void {
28
28
  design
29
29
  .command('regen')
30
30
  .description(
31
- 'Rewrite .claude/DESIGN.md and the base stylesheet from src/design/tokens.ts',
31
+ 'Rewrite canon/DESIGN.md and the base stylesheet from src/design/tokens.ts',
32
32
  )
33
33
  .addHelpText(
34
34
  'after',
@@ -419,7 +419,7 @@ export function register(program: Command): void {
419
419
  '',
420
420
  'Not read, each a shape the corpus writes and none of them a citation:',
421
421
  ' a placeholder or glob segment, which describes a shape rather than',
422
- ' naming a file, such as .claude/context/<domain>.md or app/**/route.ts',
422
+ ' naming a file, such as canon/context/<domain>.md or app/**/route.ts',
423
423
  ' a bare filename naming a convention, such as route.ts or manifest.toml',
424
424
  ' a span carrying no file extension, which is a folder or a module',
425
425
  ' specifier, such as src/pages/ , next/font , or claude/standards',
@@ -1,4 +1,4 @@
1
- import { readFileSync } from 'node:fs'
1
+ import { existsSync, readFileSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import type { Command } from 'commander'
4
4
  import { listRepositoryFiles } from '@/git-files'
@@ -26,6 +26,10 @@ import {
26
26
  walkFlatRules,
27
27
  } from '@/migrate/rule-layout'
28
28
  import { SKILL_NAME_MAP, SKILL_NAME_RULES } from '@/migrate/skill-names'
29
+ import {
30
+ planSurfaceRootsMove,
31
+ type SurfaceRootsPlan,
32
+ } from '@/migrate/surface-roots'
29
33
  import {
30
34
  applyScratchEvidence,
31
35
  planScratchEvidence,
@@ -396,6 +400,143 @@ function toRecordsRecord(
396
400
  }
397
401
  }
398
402
 
403
+ /**
404
+ * Moves the tracked toolkit surfaces to `canon/` and repoints what cites them.
405
+ *
406
+ * Every path here is tracked, so the write goes through `applyRename`'s
407
+ * `git mv` rather than the records move's plain rename, and the history
408
+ * follows each file. A destination already on disk refuses the whole plan,
409
+ * since `git mv` onto an existing path fails per file and leaves a tree split
410
+ * across both roots.
411
+ */
412
+ async function runSurfaceRoots(opts: SweepOptions): Promise<number> {
413
+ const root = opts.root ?? process.cwd()
414
+
415
+ const files = await listRepositoryFiles(root)
416
+ if (files === undefined) {
417
+ logError(`Could not list files under ${root}. Is it a git repository?`)
418
+ return 1
419
+ }
420
+
421
+ const toSweep = files.filter((path) => !isRecordArtifact(path))
422
+ const plan = planSurfaceRootsMove(await readSources(root, toSweep))
423
+ const taken = plan.entries
424
+ .map((entry) => entry.movesTo)
425
+ .filter((to): to is string => to !== undefined)
426
+ .filter((to) => existsSync(join(root, to)))
427
+
428
+ if (opts.json) {
429
+ process.stdout.write(
430
+ `${JSON.stringify(toSurfaceRootsRecord(plan, taken, opts.write))}\n`,
431
+ )
432
+ }
433
+
434
+ reportSurfaceRoots(plan)
435
+
436
+ if (taken.length > 0) {
437
+ logError(
438
+ `${plural(taken.length, 'destination')} already exist under canon/. Merging two copies of a surface is not a call this verb takes.`,
439
+ )
440
+ for (const path of taken) logError(` ${path}`)
441
+ return 1
442
+ }
443
+
444
+ if (plan.entries.length === 0) return 0
445
+
446
+ if (!opts.write) {
447
+ logWarn('Nothing was written. Pass --write to apply this plan.')
448
+ return 2
449
+ }
450
+
451
+ const applied = await applyRename(root, {
452
+ entries: plan.entries.map((entry) => ({
453
+ path: entry.path,
454
+ ...(entry.movesTo === undefined ? {} : { movesTo: entry.movesTo }),
455
+ ...(entry.text === undefined ? {} : { text: entry.text }),
456
+ renamed: entry.rewritten,
457
+ protectedCount: entry.kept,
458
+ })),
459
+ excluded: plan.excluded,
460
+ renamed: plan.rewritten,
461
+ protectedCount: plan.kept,
462
+ moves: plan.moves,
463
+ })
464
+ logStep(
465
+ `Rewrote ${plural(applied.written, 'file')} and moved ${plural(applied.moved, 'file')}.`,
466
+ )
467
+
468
+ if (applied.failed.length > 0) {
469
+ logError(`Could not move ${plural(applied.failed.length, 'path')}.`)
470
+ for (const path of applied.failed) logError(` ${path}`)
471
+ return 1
472
+ }
473
+
474
+ return 0
475
+ }
476
+
477
+ function reportSurfaceRoots(plan: SurfaceRootsPlan): void {
478
+ logInfo(`${plural(plan.moves, 'file')} to move.`)
479
+ logInfo(
480
+ `${plural(plan.entries.filter((entry) => entry.text !== undefined).length, 'file')} to change, ${plural(plan.rewritten, 'citation')} to rewrite.`,
481
+ )
482
+ logInfo(`${plural(plan.kept, 'citation')} marked to keep the old root.`)
483
+
484
+ if (plan.excluded.length > 0) {
485
+ logInfo(`${plural(plan.excluded.length, 'file')} excluded from the sweep:`)
486
+ for (const path of plan.excluded) logInfo(` ${path}`)
487
+ }
488
+
489
+ if (plan.coupled.length > 0) {
490
+ logInfo(
491
+ `${plural(plan.coupled.length, 'file')} couple to an excluded path:`,
492
+ )
493
+ for (const path of plan.coupled) logInfo(` ${path}`)
494
+ }
495
+
496
+ for (const [label, held] of [
497
+ ['a frontmatter paths: glob', plan.frontmatterGlobs],
498
+ ['a citation into another repository', plan.crossRepoCitations],
499
+ ['a dated citation', plan.datedCitations],
500
+ ] as const) {
501
+ if (held.length === 0) continue
502
+ logInfo(`${plural(held.length, 'file')} carry ${label}, left alone:`)
503
+ for (const entry of held) {
504
+ for (const line of entry.lines) {
505
+ logInfo(` ${entry.path}:${line.line} ${excerpt(line.text)}`)
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ function toSurfaceRootsRecord(
512
+ plan: SurfaceRootsPlan,
513
+ collisions: readonly string[],
514
+ wrote: boolean | undefined,
515
+ ): unknown {
516
+ return {
517
+ ok: collisions.length === 0,
518
+ wrote: wrote === true,
519
+ moves: plan.moves,
520
+ collisions,
521
+ files: plan.entries.length,
522
+ rewritten: plan.rewritten,
523
+ kept: plan.kept,
524
+ excluded: plan.excluded,
525
+ coupled: plan.coupled,
526
+ frontmatterGlobs: plan.frontmatterGlobs,
527
+ crossRepoCitations: plan.crossRepoCitations,
528
+ datedCitations: plan.datedCitations,
529
+ globs: plan.globs,
530
+ crossRepo: plan.crossRepo,
531
+ dated: plan.dated,
532
+ paths: plan.entries.map((entry) => ({
533
+ path: entry.path,
534
+ ...(entry.movesTo === undefined ? {} : { movesTo: entry.movesTo }),
535
+ rewritten: entry.rewritten,
536
+ })),
537
+ }
538
+ }
539
+
399
540
  interface RecordTreeOptions {
400
541
  readonly json?: boolean
401
542
  readonly write?: boolean
@@ -763,6 +904,46 @@ export function register(program: Command): void {
763
904
  process.exitCode = await runRecords(opts)
764
905
  })
765
906
 
907
+ migrate
908
+ .command('surface-roots')
909
+ .description('Move the tracked toolkit surfaces to canon/')
910
+ .helpOption('-h, --help', 'Show this help message')
911
+ .option('--json', 'Add a machine-readable record on stdout')
912
+ .option('--write', 'Apply the plan rather than reporting it')
913
+ .option(
914
+ '--root <path>',
915
+ 'Project root, defaulting to the working directory',
916
+ )
917
+ .addHelpText(
918
+ 'after',
919
+ [
920
+ '',
921
+ 'Exit codes:',
922
+ ' 0 nothing to move, or --write applied the whole plan',
923
+ ' 1 refused, or a move failed',
924
+ ' 2 a plan exists and --write was not passed',
925
+ '',
926
+ 'Moves canon/context/, canon/wireframes/, and the ARCHITECTURE.md,',
927
+ 'REQUIREMENTS.md, and DESIGN.md documents to canon/ with git mv, and',
928
+ 'rewrites every tracked citation of them. .claude/canon/, rules/,',
929
+ 'skills/, hooks/, and settings.json stay where the vendor reads them.',
930
+ '',
931
+ 'A line carrying canon-keep-surface-root, or the nearest non-blank line',
932
+ 'above it, keeps the old root. A frontmatter paths: glob, a citation',
933
+ 'into another repository, and one inside a dated paragraph are reported',
934
+ 'and left alone rather than rewritten.',
935
+ '',
936
+ 'Examples:',
937
+ ' canon migrate surface-roots',
938
+ ' canon migrate surface-roots --write',
939
+ ' canon migrate surface-roots --json',
940
+ '',
941
+ ].join('\n'),
942
+ )
943
+ .action(async (opts: SweepOptions) => {
944
+ process.exitCode = await runSurfaceRoots(opts)
945
+ })
946
+
766
947
  migrate
767
948
  .command('record-tree')
768
949
  .description('Repoint old-root citations inside the records themselves')
@@ -19,7 +19,7 @@ import { isStubSeed } from '@/seed-marker'
19
19
  * Every reason `canon context audit` refuses for.
20
20
  *
21
21
  * `no-folders` is the one ordinary absence: a project that never adopted
22
- * `.claude/context/`, `.canon/diagrams/`, or `.claude/wireframes/` names no
22
+ * `canon/context/`, `.canon/diagrams/`, or `canon/wireframes/` names no
23
23
  * corpus this audit can measure, the same state `no-skills` reads for the
24
24
  * skill corpora. The other four are a malformed invocation or a checkout git
25
25
  * cannot read, which stay a break rather than an absence.
@@ -84,13 +84,15 @@ export function isFixture(rel: string): boolean {
84
84
  * A dotted root cannot be a suffix of a path segment, since a segment boundary
85
85
  * is a slash and a slash can never sit inside the dot itself, so a preceding
86
86
  * slash is not a false match to guard against and the boundary admits it. That
87
- * is what a relative link needs, since `../.claude/context/<entry>.md` carries
88
- * a slash immediately before `.claude`. A bare root has no such protection: it
89
- * is a suffix of a dotted root's own name and of any `/<root>/` path segment,
90
- * so its boundary rejects a slash along with a name character or a dot.
87
+ * is what a relative link into a dotted root needs. A bare root has no such
88
+ * protection: it is a suffix of a dotted root's own name and of any
89
+ * `/<root>/` path segment, such as a seed tree's own `canon/` folder, so its
90
+ * boundary rejects a name character, a dot, and a slash, except a slash that
91
+ * closes a `..` segment. That one exception is a relative link climbing out of
92
+ * a folder into the bare root, which is the form a moved docs page writes.
91
93
  */
92
94
  function rootBoundary(root: string): string {
93
- return root.startsWith('.') ? '(?<![\\w.])' : '(?<![\\w./])'
95
+ return root.startsWith('.') ? '(?<![\\w.])' : '(?<![\\w.])(?<!(?<!\\.\\.)/)'
94
96
  }
95
97
 
96
98
  /**
@@ -61,7 +61,7 @@ export interface AuditedFolder {
61
61
  * The requested folder name this was resolved under, which is what says
62
62
  * which standard governs the entries. A nested split folder carries the name
63
63
  * of the folder it sits beneath rather than its own, so
64
- * `.claude/context/claude-plugin` is governed as `context`.
64
+ * `canon/context/claude-plugin` is governed as `context`.
65
65
  */
66
66
  readonly name: string
67
67
  /**
@@ -90,7 +90,7 @@ export interface AuditedFolder {
90
90
  * Names the requested record-root folders that actually exist, which is the
91
91
  * citation check's scope.
92
92
  *
93
- * A skill or seed pointing into `.claude/wireframes/` is a live instruction for
93
+ * A skill or seed pointing into `canon/wireframes/` is a live instruction for
94
94
  * a project that carries the folder and says nothing about one that does not.
95
95
  * Checking a path into an absent folder would fail eight shipped references
96
96
  * here for the sole reason that this repository has no wireframes.
@@ -35,7 +35,7 @@ export type CountsReport =
35
35
 
36
36
  /**
37
37
  * A calendar date, which reads a sentence as a historical record rather than
38
- * a live claim about the tree. `.claude/ARCHITECTURE.md` and the context
38
+ * a live claim about the tree. `canon/ARCHITECTURE.md` and the context
39
39
  * entries carry a figure this way deliberately, and every one of them stays
40
40
  * correct forever, so a sentence carrying one is read past rather than
41
41
  * matched.
@@ -80,7 +80,7 @@ function sentencesOf(line: string): string[] {
80
80
  * the 290 showed removing all of them while keeping that one.
81
81
  *
82
82
  * `authors` joined the list on a corpus measurement rather than on the shape
83
- * argument above. `.claude/context/development/regeneration.md` states `the
83
+ * argument above. `canon/context/development/regeneration.md` states `the
84
84
  * toolkit authors 69 rules under governance/rules/`, which is the assertion
85
85
  * shape this list was built for with a verb the list did not carry, and the
86
86
  * word costs one further sentence across the whole corpus and no finding.
@@ -129,7 +129,7 @@ const QUANTIFIERS = ['all']
129
129
  * of the number.
130
130
  *
131
131
  * Found on this module's own first review, against a live instance the verb
132
- * gate alone reported clean: `.claude/context/development/gates.md` stated
132
+ * gate alone reported clean: `canon/context/development/gates.md` stated
133
133
  * `the twelve audits` while the tree held 20, sitting inside the plausibility
134
134
  * bound this design already carries, so nothing but the missing shape kept it
135
135
  * from reporting.
@@ -142,7 +142,7 @@ const QUANTIFIERS = ['all']
142
142
  * match at all. No syntactic rule tells a qualifier that narrows a catalog
143
143
  * from one that only restates it, and the sentence was true when written, so
144
144
  * the fix was not a narrower matcher: the clause was dated as a past state,
145
- * the way `.claude/ARCHITECTURE.md` dates its own figures, which is the class
145
+ * the way `canon/ARCHITECTURE.md` dates its own figures, which is the class
146
146
  * the date exclusion below already reads past.
147
147
  */
148
148
  const ARTICLES = ['the', 'a', 'an']