@erclx/canon 5.62.0 → 5.64.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 (101) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/references/adopted.md +1 -1
  4. package/claude/skills/context-fold/REQUIREMENT.md +2 -3
  5. package/claude/skills/context-fold/SKILL.md +1 -11
  6. package/claude/skills/context-fold/references/completion-report.md +8 -9
  7. package/claude/skills/create-skill/references/skill-practice.md +17 -1
  8. package/claude/skills/design-record/references/design.md +1 -1
  9. package/claude/skills/design-taste/REQUIREMENT.md +2 -1
  10. package/claude/skills/design-taste/SKILL.md +6 -5
  11. package/claude/skills/design-taste/references/adopted.md +56 -8
  12. package/claude/skills/design-taste/references/craft.md +1 -1
  13. package/claude/skills/design-taste/references/kinds.md +8 -2
  14. package/claude/skills/design-taste/references/preflight.md +3 -3
  15. package/claude/skills/design-taste/references/tells.md +26 -5
  16. package/claude/skills/document-health/REQUIREMENT.md +1 -1
  17. package/claude/skills/document-health/SKILL.md +3 -14
  18. package/claude/skills/document-health/references/axes.md +14 -16
  19. package/claude/skills/plan-feature/SKILL.md +2 -2
  20. package/claude/skills/plan-feature/references/plan.md +24 -3
  21. package/claude/skills/review-craft/SKILL.md +1 -1
  22. package/claude/skills/role-worker/SKILL.md +2 -1
  23. package/claude/skills/ui-checklist/SKILL.md +1 -1
  24. package/claude/skills/ui-craft/REQUIREMENT.md +29 -0
  25. package/claude/skills/ui-craft/SKILL.md +44 -0
  26. package/{governance/rules/ui → claude/skills/ui-craft/references}/a11y.md +0 -10
  27. package/claude/skills/ui-craft/references/adopted.md +27 -0
  28. package/{governance/rules/ui/surface-capture.md → claude/skills/ui-craft/references/capture.md} +0 -10
  29. package/{governance/rules/ui/ux-controls.md → claude/skills/ui-craft/references/controls.md} +3 -10
  30. package/claude/skills/ui-craft/references/copy.md +45 -0
  31. package/{governance/rules/ui → claude/skills/ui-craft/references}/forms.md +0 -9
  32. package/{governance/rules/ui → claude/skills/ui-craft/references}/keyboard.md +0 -10
  33. package/{governance/rules/ui → claude/skills/ui-craft/references}/motion.md +0 -10
  34. package/{governance/rules/ui → claude/skills/ui-craft/references}/pointer.md +0 -10
  35. package/{governance/rules/ui/ux-completeness.md → claude/skills/ui-craft/references/states.md} +0 -10
  36. package/claude/skills/ux-audit/SKILL.md +1 -1
  37. package/claude/skills/write-human/references/adopted.md +4 -1
  38. package/docs/agents/audits.md +2 -6
  39. package/docs/agents/context-audit.md +1 -1
  40. package/docs/agents/install-and-sync.md +2 -2
  41. package/docs/target/migrations.md +3 -3
  42. package/docs/target/sync.md +3 -2
  43. package/docs/workflow/skill-map.md +1 -0
  44. package/docs/workflow/visual-design-workflow.md +2 -2
  45. package/governance/detect.toml +1 -45
  46. package/governance/ledger/lib/test-scope.md +1 -1
  47. package/governance/ledger/skills/api-design/SKILL.md +62 -0
  48. package/governance/ledger/skills/code-craft/SKILL.md +53 -0
  49. package/governance/ledger/skills/deprecation-migration/SKILL.md +68 -0
  50. package/governance/ledger/skills/design-taste/SKILL.md +64 -0
  51. package/governance/ledger/skills/design-taste/craft.md +66 -0
  52. package/governance/ledger/skills/design-taste/kinds.md +58 -0
  53. package/governance/ledger/skills/design-taste/systems.md +42 -0
  54. package/governance/ledger/skills/design-taste/tells.md +103 -0
  55. package/governance/ledger/{ui → skills/ui-craft}/a11y.md +3 -3
  56. package/governance/ledger/{ui/surface-capture.md → skills/ui-craft/capture.md} +3 -3
  57. package/governance/ledger/{ui/ux-controls.md → skills/ui-craft/controls.md} +12 -7
  58. package/governance/ledger/skills/ui-craft/copy.md +102 -0
  59. package/governance/ledger/{ui → skills/ui-craft}/forms.md +3 -3
  60. package/governance/ledger/{ui → skills/ui-craft}/keyboard.md +3 -3
  61. package/governance/ledger/{ui → skills/ui-craft}/motion.md +3 -3
  62. package/governance/ledger/{ui → skills/ui-craft}/pointer.md +3 -3
  63. package/governance/ledger/{ui/ux-completeness.md → skills/ui-craft/states.md} +3 -3
  64. package/governance/ledger/ui/craft.md +19 -0
  65. package/governance/renames.toml +17 -3
  66. package/governance/rules/lib/test-scope.md +1 -1
  67. package/governance/rules/ui/craft.md +22 -0
  68. package/governance/splits.toml +0 -2
  69. package/package.json +1 -1
  70. package/src/audits/catalog.ts +0 -52
  71. package/src/claude/cases/misc.ts +5 -0
  72. package/src/claude/skills-audit.ts +226 -12
  73. package/src/cli/retired.ts +8 -0
  74. package/src/commands/claude.ts +15 -1
  75. package/src/commands/context.ts +0 -425
  76. package/src/commands/tooling.ts +4 -1
  77. package/src/context/citations.ts +9 -3
  78. package/src/gate/measures.ts +4 -3
  79. package/src/git/files.ts +8 -3
  80. package/src/gov/audit.ts +6 -1
  81. package/src/inventory/subjects.ts +1 -1
  82. package/src/markdown/gated.ts +1 -1
  83. package/src/markdown/scope.ts +0 -1
  84. package/src/tooling/package.ts +22 -13
  85. package/tooling/web/reference.md +3 -1
  86. package/claude/skills/context-fold/references/classify.md +0 -87
  87. package/governance/ledger/ui/copy.md +0 -49
  88. package/governance/ledger/ui/design-taste.md +0 -19
  89. package/governance/ledger/ui/interface-casing.md +0 -47
  90. package/governance/ledger/ui/link-behavior.md +0 -29
  91. package/governance/rules/ui/copy.md +0 -28
  92. package/governance/rules/ui/design-taste.md +0 -18
  93. package/governance/rules/ui/interface-casing.md +0 -29
  94. package/governance/rules/ui/link-behavior.md +0 -19
  95. package/src/context/classify/extract.ts +0 -443
  96. package/src/context/classify/ollama.ts +0 -172
  97. package/src/context/classify/patterns.ts +0 -75
  98. package/src/context/classify/prompts.ts +0 -74
  99. package/src/context/classify/run.ts +0 -348
  100. package/src/context/classify/settings.ts +0 -196
  101. /package/tooling/web/{configs → seeds}/e2e/home.spec.ts +0 -0
@@ -1,8 +1,20 @@
1
1
  import { existsSync } from 'node:fs'
2
- import { dirname, join } from 'node:path'
2
+ import { dirname, join, relative } from 'node:path'
3
3
  import { isFamilyKey } from '@/claude/skills-families'
4
+ import { plural } from '@/cli/ui'
5
+ import {
6
+ compareLedger,
7
+ countLedgerRows,
8
+ type HeadingCount,
9
+ type LedgerMismatch,
10
+ } from '@/gov/audit'
4
11
  import { parseFrontmatter, readField } from '@/indexes/frontmatter'
5
- import { type BodyLine, bodyLines, maskDisplayed } from '@/markdown/scan'
12
+ import {
13
+ type BodyLine,
14
+ bodyLines,
15
+ maskCodeSpans,
16
+ maskDisplayed,
17
+ } from '@/markdown/scan'
6
18
 
7
19
  /** Returned when a skill folder carries no `REQUIREMENT.md`, the gating check. */
8
20
  export const EXIT_MISSING_REQUIREMENT = 2
@@ -69,8 +81,52 @@ export const LEDGER_SKILLS: readonly string[] = [
69
81
  join(SHIPPED_CORPUS, 'design-taste'),
70
82
  join(SHIPPED_CORPUS, 'write-human'),
71
83
  join(SHIPPED_CORPUS, 'test-first'),
84
+ join(SHIPPED_CORPUS, 'ui-craft'),
72
85
  ]
73
86
 
87
+ function references(...names: string[]): string[] {
88
+ return names.map((name) => join('references', `${name}.md`))
89
+ }
90
+
91
+ export interface BulletLedgerScope {
92
+ /** References left off, each a checklist or glossary naming no new source. */
93
+ readonly exempt: readonly string[]
94
+ /** True when `SKILL.md` carries directives of its own to ledger. */
95
+ readonly body: boolean
96
+ }
97
+
98
+ /**
99
+ * The ledger-listed skills whose every reference but the source record carries
100
+ * a ledger of its own, one table row per bullet under each H2, the way a rule
101
+ * in a practice folder does. A reference is ledgered unless exempted by name,
102
+ * so one added later fails until its rows land rather than passing unread. The
103
+ * ledgers sit outside the plugin as rule ledgers do, so a target pays nothing
104
+ * for them.
105
+ */
106
+ export const BULLET_LEDGER_SKILLS: Readonly<Record<string, BulletLedgerScope>> =
107
+ {
108
+ [join(SHIPPED_CORPUS, 'ui-craft')]: { exempt: [], body: false },
109
+ [join(SHIPPED_CORPUS, 'api-design')]: { exempt: [], body: true },
110
+ [join(SHIPPED_CORPUS, 'code-craft')]: { exempt: [], body: true },
111
+ [join(SHIPPED_CORPUS, 'deprecation-migration')]: { exempt: [], body: true },
112
+ [join(SHIPPED_CORPUS, 'design-taste')]: {
113
+ exempt: references('preflight', 'vocabulary'),
114
+ body: true,
115
+ },
116
+ }
117
+
118
+ /** Where a per-bullet ledger lives, as `<skill>/<file>.md` below it. */
119
+ export const SKILL_LEDGERS = join('governance', 'ledger', 'skills')
120
+
121
+ /**
122
+ * The ledger a ledgered file pairs with, keyed by the file's own name, so a
123
+ * reference drops its `references/` segment and a `SKILL.md` keeps its name.
124
+ */
125
+ export function skillLedgerPath(folder: string, file: string): string {
126
+ const inside = relative('references', file)
127
+ return join(SKILL_LEDGERS, folder, inside.startsWith('..') ? file : inside)
128
+ }
129
+
74
130
  /** The closing H2s a practice skill carries, matched exactly outside fences. */
75
131
  export const PRACTICE_SECTIONS: readonly string[] = [
76
132
  'Excuses and rebuttals',
@@ -78,9 +134,32 @@ export const PRACTICE_SECTIONS: readonly string[] = [
78
134
  'Before handing over',
79
135
  ]
80
136
 
137
+ /**
138
+ * The H2s that end a listed body's concern groups. The closing sections
139
+ * restate the groups' rules as excuses, signs, and checks, and the delegates
140
+ * section routes, so neither states a rule of its own to source.
141
+ */
142
+ export const BODY_CLOSERS: readonly string[] = [
143
+ ...PRACTICE_SECTIONS,
144
+ 'What this delegates',
145
+ ]
146
+
81
147
  /** The source ledger every practice skill carries beside its body. */
82
148
  export const PRACTICE_LEDGER = join('references', 'adopted.md')
83
149
 
150
+ /**
151
+ * A ledger cites a source by linking a page outside the project, as an inline
152
+ * link or an autolink. A relative link points back into the skill and cites
153
+ * nothing a reader could check the rule against.
154
+ */
155
+ const EXTERNAL_LINK = /\]\(\s*https?:\/\/|<https?:\/\//
156
+
157
+ /**
158
+ * A table cell opening on the marker the governance ledgers already use for a
159
+ * rule the toolkit chose, so one check reads every ledger alike.
160
+ */
161
+ const HOUSE_CHOICE = /\|\s*House choice\b/
162
+
84
163
  /**
85
164
  * The line count past which a reference opens with a contents list, from
86
165
  * Anthropic's skill authoring guidance. Counted over the whole file the way
@@ -118,6 +197,12 @@ const ISSUE_NUMBER = /(?<![\w&/#])#\d+\b/g
118
197
 
119
198
  const TOP_LEVEL_BULLET = /^(?:[-*+]|\d+[.)])\s/
120
199
 
200
+ /**
201
+ * A line a skill ledger pairs with a row: a top-level `- ` bullet as a rule
202
+ * writes one, or a top-level ordered step, which states a directive as surely.
203
+ */
204
+ const LEDGERED_LINE = /^(?:- |\d+[.)]\s)/
205
+
121
206
  const WORD = /\S*[\p{L}\p{N}]\S*/gu
122
207
 
123
208
  export interface SkillFinding {
@@ -166,13 +251,25 @@ interface SkillSource {
166
251
  readonly requirement: string | undefined
167
252
  /** Every H2 in `SKILL.md` outside a fence, for the practice shape. */
168
253
  readonly sections: readonly string[]
169
- readonly hasLedger: boolean
254
+ /** Undefined when the folder carries no ledger at all. */
255
+ readonly ledger: string | undefined
170
256
  /**
171
257
  * `SKILL.md` and every reference below the folder, keyed by the path a
172
258
  * finding names. `REQUIREMENT.md` and `EVAL.md` are left out, since the
173
259
  * requirement is where the standard sends a date.
174
260
  */
175
261
  readonly bodies: readonly SkillText[]
262
+ /** One entry per ledgered file of a skill on the per-bullet ledger list. */
263
+ readonly referenceLedgers: readonly ReferenceLedger[]
264
+ /** Exempted references the skill no longer carries, which exempt nothing. */
265
+ readonly staleExemptions: readonly string[]
266
+ }
267
+
268
+ interface ReferenceLedger {
269
+ readonly rel: string
270
+ readonly ledgerRel: string
271
+ /** Undefined when the ledger file is absent. */
272
+ readonly mismatches: readonly LedgerMismatch[] | undefined
176
273
  }
177
274
 
178
275
  interface SkillText {
@@ -195,7 +292,7 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
195
292
  )
196
293
 
197
294
  const perCorpus = await Promise.all(
198
- present.map((corpus) => readCorpus(corpus.rel, corpus.dir)),
295
+ present.map((corpus) => readCorpus(root, corpus.rel, corpus.dir)),
199
296
  )
200
297
  const sources = perCorpus.flat()
201
298
 
@@ -247,21 +344,29 @@ export function auditExitCode(report: SkillsAudit): number {
247
344
  return report.missingRequirement.length > 0 ? EXIT_MISSING_REQUIREMENT : 0
248
345
  }
249
346
 
250
- async function readCorpus(rel: string, dir: string): Promise<SkillSource[]> {
347
+ async function readCorpus(
348
+ root: string,
349
+ rel: string,
350
+ dir: string,
351
+ ): Promise<SkillSource[]> {
251
352
  const paths = [
252
353
  ...new Bun.Glob('*/SKILL.md').scanSync({ cwd: dir, onlyFiles: true }),
253
354
  ].sort()
254
355
 
255
- return Promise.all(paths.map((path) => readSkill(rel, dir, dirname(path))))
356
+ return Promise.all(
357
+ paths.map((path) => readSkill(root, rel, dir, dirname(path))),
358
+ )
256
359
  }
257
360
 
258
361
  async function readSkill(
362
+ root: string,
259
363
  rel: string,
260
364
  dir: string,
261
365
  folder: string,
262
366
  ): Promise<SkillSource> {
263
367
  const skillDir = join(dir, folder)
264
368
  const requirementPath = join(skillDir, 'REQUIREMENT.md')
369
+ const ledgerPath = join(skillDir, PRACTICE_LEDGER)
265
370
 
266
371
  const references = [
267
372
  ...new Bun.Glob('references/**/*.md').scanSync({
@@ -270,11 +375,14 @@ async function readSkill(
270
375
  }),
271
376
  ].sort()
272
377
 
273
- const [body, requirement, referenceTexts] = await Promise.all([
378
+ const [body, requirement, ledger, referenceTexts] = await Promise.all([
274
379
  Bun.file(join(skillDir, 'SKILL.md')).text(),
275
380
  existsSync(requirementPath)
276
381
  ? Bun.file(requirementPath).text()
277
382
  : Promise.resolve(undefined),
383
+ existsSync(ledgerPath)
384
+ ? Bun.file(ledgerPath).text()
385
+ : Promise.resolve(undefined),
278
386
  Promise.all(
279
387
  references.map((path) => Bun.file(join(skillDir, path)).text()),
280
388
  ),
@@ -282,6 +390,22 @@ async function readSkill(
282
390
 
283
391
  const fields = parseFrontmatter(body)
284
392
  const skillRel = join(rel, folder)
393
+ const scope = BULLET_LEDGER_SKILLS[skillRel]
394
+ const referenceLedgers =
395
+ scope === undefined
396
+ ? []
397
+ : await readReferenceLedgers(root, skillRel, folder, [
398
+ ...(scope.body ? [{ path: 'SKILL.md', text: body }] : []),
399
+ ...references
400
+ .map((path, index) => ({ path, text: referenceTexts[index] ?? '' }))
401
+ .filter(
402
+ ({ path }) =>
403
+ path !== PRACTICE_LEDGER && !scope.exempt.includes(path),
404
+ ),
405
+ ])
406
+ const staleExemptions = (scope?.exempt ?? [])
407
+ .filter((path) => !references.includes(path))
408
+ .map((path) => join(skillRel, path))
285
409
 
286
410
  return {
287
411
  rel: skillRel,
@@ -294,7 +418,7 @@ async function readSkill(
294
418
  requirement === undefined ? undefined : headings(requirement),
295
419
  requirement,
296
420
  sections: sectionsOf(body),
297
- hasLedger: existsSync(join(skillDir, PRACTICE_LEDGER)),
421
+ ledger,
298
422
  bodies: [
299
423
  { rel: join(skillRel, 'SKILL.md'), text: body, isReference: false },
300
424
  ...references.map((path, index) => ({
@@ -303,9 +427,62 @@ async function readSkill(
303
427
  isReference: true,
304
428
  })),
305
429
  ],
430
+ referenceLedgers,
431
+ staleExemptions,
306
432
  }
307
433
  }
308
434
 
435
+ /**
436
+ * Pairs each ledgered file's directives with its ledger's rows through the rule
437
+ * audit's own comparison, so a skill file and a rule mismatch alike.
438
+ */
439
+ async function readReferenceLedgers(
440
+ root: string,
441
+ skillRel: string,
442
+ folder: string,
443
+ files: readonly { path: string; text: string }[],
444
+ ): Promise<ReferenceLedger[]> {
445
+ return Promise.all(
446
+ files.map(async ({ path, text }) => {
447
+ const ledgerRel = skillLedgerPath(folder, path)
448
+ const ledgerPath = join(root, ledgerRel)
449
+ const mismatches = existsSync(ledgerPath)
450
+ ? compareLedger(
451
+ ledgerGroups(text, path === 'SKILL.md'),
452
+ countLedgerRows(await Bun.file(ledgerPath).text()),
453
+ )
454
+ : undefined
455
+ return { rel: join(skillRel, path), ledgerRel, mismatches }
456
+ }),
457
+ )
458
+ }
459
+
460
+ /**
461
+ * Directives per H2, `null` for any above the first. A contents list repeats
462
+ * the headings rather than stating a rule, so `## Contents` takes no rows. A
463
+ * nested bullet elaborates its parent, so only a top-level line counts. A body
464
+ * stops at its first closer, and a ledger table under one then reads as rows
465
+ * with no bullets rather than passing unread.
466
+ */
467
+ function ledgerGroups(text: string, isBody: boolean): HeadingCount[] {
468
+ const counts = new Map<string | null, number>()
469
+ let heading: string | null = null
470
+
471
+ for (const line of bodyLines(text)) {
472
+ if (line.fenced) continue
473
+ const section = SECTION.exec(line.text)?.[1]
474
+ if (section !== undefined) {
475
+ if (isBody && BODY_CLOSERS.includes(section)) break
476
+ heading = section
477
+ continue
478
+ }
479
+ if (heading === CONTENTS_SECTION || !LEDGERED_LINE.test(line.text)) continue
480
+ counts.set(heading, (counts.get(heading) ?? 0) + 1)
481
+ }
482
+
483
+ return [...counts].map(([key, count]) => ({ heading: key, count }))
484
+ }
485
+
309
486
  /**
310
487
  * A key carrying an empty value declares nothing, so blank reads as absent
311
488
  * rather than as a name that disagrees with every folder.
@@ -493,11 +670,48 @@ function practiceFindings(source: SkillSource): SkillFinding[] {
493
670
  rel: source.rel,
494
671
  detail: `missing section: ${section}`,
495
672
  }))
496
- const ledger = source.hasLedger
497
- ? []
498
- : [{ rel: source.rel, detail: `missing ledger: ${PRACTICE_LEDGER}` }]
499
673
 
500
- return [...sections, ...ledger]
674
+ return [
675
+ ...sections,
676
+ ...ledgerFindings(source),
677
+ ...source.referenceLedgers.flatMap(referenceLedgerFindings),
678
+ ...source.staleExemptions.map((rel) => ({
679
+ rel,
680
+ detail: 'missing file: exempt from a ledger',
681
+ })),
682
+ ]
683
+ }
684
+
685
+ function referenceLedgerFindings(entry: ReferenceLedger): SkillFinding[] {
686
+ if (entry.mismatches === undefined) {
687
+ return [{ rel: entry.rel, detail: `missing ledger: ${entry.ledgerRel}` }]
688
+ }
689
+ return entry.mismatches.map((mismatch) => ({
690
+ rel: entry.rel,
691
+ detail: `${mismatch.heading === null ? 'above the first H2' : `## ${mismatch.heading}`}: ${plural(mismatch.bullets, 'bullet')}, ${plural(mismatch.rows, 'ledger row')}`,
692
+ }))
693
+ }
694
+
695
+ /**
696
+ * A ledger present but citing nothing passed for as long as the check read the
697
+ * file's existence alone, so the contents are read too. A sample block shows a
698
+ * reader the shape of a row and cites nothing, so fences and code spans are
699
+ * left out.
700
+ */
701
+ function ledgerFindings(source: SkillSource): SkillFinding[] {
702
+ if (source.ledger === undefined) {
703
+ return [{ rel: source.rel, detail: `missing ledger: ${PRACTICE_LEDGER}` }]
704
+ }
705
+
706
+ const cites = bodyLines(source.ledger)
707
+ .filter((line) => !line.fenced)
708
+ .map((line) => maskCodeSpans(line.text))
709
+ .some((text) => EXTERNAL_LINK.test(text) || HOUSE_CHOICE.test(text))
710
+ if (cites) return []
711
+
712
+ return [
713
+ { rel: source.rel, detail: `ledger cites nothing: ${PRACTICE_LEDGER}` },
714
+ ]
501
715
  }
502
716
 
503
717
  /**
@@ -9,6 +9,8 @@ export interface RetiredVerb {
9
9
  const DESIGN_MOVED =
10
10
  'the design values now live in the stylesheet your canon/DESIGN.md lead names'
11
11
 
12
+ const CONTEXT_MOVED = '`canon context audit`, which reports placement findings'
13
+
12
14
  /**
13
15
  * Every verb a release dropped, with the release and where the work went. The
14
16
  * router reads this ahead of the parser, and `canon migrate status` reads it to
@@ -41,6 +43,12 @@ export const RETIRED_VERBS: readonly RetiredVerb[] = [
41
43
  release: '5.53.0',
42
44
  movedTo: 'a tool built for composed video',
43
45
  },
46
+ { path: ['context', 'classify'], release: '5.64.0', movedTo: CONTEXT_MOVED },
47
+ {
48
+ path: ['context', 'classifier'],
49
+ release: '5.64.0',
50
+ movedTo: CONTEXT_MOVED,
51
+ },
44
52
  ]
45
53
 
46
54
  /**
@@ -30,6 +30,8 @@ import {
30
30
  DESCRIPTION_LIMIT,
31
31
  LEDGER_SKILLS,
32
32
  PRACTICE_LEDGER,
33
+ BULLET_LEDGER_SKILLS,
34
+ SKILL_LEDGERS,
33
35
  PRACTICE_SECTIONS,
34
36
  PRACTICE_SKILLS,
35
37
  REQUIREMENT_LIMITS,
@@ -166,6 +168,15 @@ interface RoutingOptions {
166
168
  const SEEDED_FILES: readonly string[] = ['REQUIREMENTS.md', 'DESIGN.md']
167
169
  const SEEDED_DIRS: readonly string[] = ['tasks']
168
170
 
171
+ /**
172
+ * Where a `SEEDED_FILES` entry resolves for the sync presence check, at either
173
+ * surface root, so a target holding its records under `canon/` is not sent to
174
+ * `canon claude init`.
175
+ */
176
+ export function seededFilePath(resolved: string, name: string): string {
177
+ return surfaceDir(resolved, name)
178
+ }
179
+
169
180
  /**
170
181
  * Where a `SEEDED_DIRS` entry resolves for the sync presence check.
171
182
  *
@@ -677,7 +688,7 @@ async function runSync(target: string): Promise<number> {
677
688
  // already holds.
678
689
  logStep('Seeded')
679
690
  for (const name of SEEDED_FILES) {
680
- if (existsSync(join(resolved, '.claude', name))) logInfo(name)
691
+ if (existsSync(seededFilePath(resolved, name))) logInfo(name)
681
692
  else logWarn(`${name} missing. Run \`canon claude init\``)
682
693
  }
683
694
  for (const name of SEEDED_DIRS) {
@@ -1768,6 +1779,9 @@ function reportPracticeShape(report: SkillsAudit): void {
1768
1779
  logInfo(
1769
1780
  `A listed practice skill closes with ${PRACTICE_SECTIONS.map((section) => `## ${section}`).join(', ')}, and every skill on the wider ledger list carries ${PRACTICE_LEDGER}.`,
1770
1781
  )
1782
+ logInfo(
1783
+ `A skill on the per-bullet list, ${plural(Object.keys(BULLET_LEDGER_SKILLS).length, 'skill')} here, also matches each reference it does not exempt, and a listed body, to a ledger under ${SKILL_LEDGERS}, one row per bullet under each H2.`,
1784
+ )
1771
1785
  logInfo(
1772
1786
  `Both lists are kept here and name ${plural(PRACTICE_SKILLS.length, 'practice skill')} and ${plural(LEDGER_SKILLS.length, 'ledger skill')}, so a skill outside ${SHIPPED_CORPUS} is never read.`,
1773
1787
  )