@erclx/canon 5.63.0 → 5.65.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 (62) 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 +16 -1
  8. package/claude/skills/design-taste/references/adopted.md +17 -97
  9. package/claude/skills/design-taste/references/tells.md +2 -2
  10. package/claude/skills/document-health/REQUIREMENT.md +1 -1
  11. package/claude/skills/document-health/SKILL.md +3 -14
  12. package/claude/skills/document-health/references/axes.md +14 -16
  13. package/claude/skills/test-craft/references/e2e.md +1 -1
  14. package/docs/agents/audits.md +2 -6
  15. package/docs/agents/context-audit.md +1 -1
  16. package/governance/ledger/skills/api-design/SKILL.md +62 -0
  17. package/governance/ledger/skills/code-craft/SKILL.md +53 -0
  18. package/governance/ledger/skills/codebase-layout/SKILL.md +64 -0
  19. package/governance/ledger/skills/codebase-layout/frontend.md +62 -0
  20. package/governance/ledger/skills/codebase-layout/tests.md +26 -0
  21. package/governance/ledger/skills/deprecation-migration/SKILL.md +68 -0
  22. package/governance/ledger/skills/design-taste/SKILL.md +64 -0
  23. package/governance/ledger/skills/design-taste/craft.md +66 -0
  24. package/governance/ledger/skills/design-taste/kinds.md +58 -0
  25. package/governance/ledger/skills/design-taste/systems.md +42 -0
  26. package/governance/ledger/skills/design-taste/tells.md +103 -0
  27. package/governance/ledger/skills/review-craft/SKILL.md +61 -0
  28. package/governance/ledger/skills/review-craft/security.md +34 -0
  29. package/governance/ledger/skills/teach-craft/SKILL.md +33 -0
  30. package/governance/ledger/skills/teach-craft/lesson-craft.md +47 -0
  31. package/governance/ledger/skills/teach-craft/pedagogy.md +77 -0
  32. package/governance/ledger/skills/test-craft/SKILL.md +67 -0
  33. package/governance/ledger/skills/test-craft/component.md +49 -0
  34. package/governance/ledger/skills/test-craft/e2e.md +55 -0
  35. package/governance/ledger/skills/test-craft/unit.md +54 -0
  36. package/package.json +3 -3
  37. package/src/audits/catalog.ts +0 -52
  38. package/src/claude/skills-audit.ts +124 -41
  39. package/src/cli/retired.ts +8 -0
  40. package/src/commands/claude.ts +11 -2
  41. package/src/commands/context.ts +0 -425
  42. package/src/commands/sandbox.ts +247 -2
  43. package/src/context/citations.ts +9 -3
  44. package/src/gate/measures.ts +4 -1
  45. package/src/gate/stages.ts +25 -0
  46. package/src/git/files.ts +8 -3
  47. package/src/markdown/gated.ts +1 -1
  48. package/src/markdown/scope.ts +0 -1
  49. package/src/migrate/records.ts +6 -0
  50. package/src/sandbox/replay/expected.ts +137 -0
  51. package/src/sandbox/replay/run.ts +323 -0
  52. package/src/sandbox/replay/verdict.ts +102 -0
  53. package/tooling/astro/configs/playwright.config.ts +2 -2
  54. package/tooling/nextjs/configs/playwright.config.ts +2 -2
  55. package/tooling/vite-react/configs/playwright.config.ts +2 -2
  56. package/claude/skills/context-fold/references/classify.md +0 -87
  57. package/src/context/classify/extract.ts +0 -443
  58. package/src/context/classify/ollama.ts +0 -172
  59. package/src/context/classify/patterns.ts +0 -75
  60. package/src/context/classify/prompts.ts +0 -74
  61. package/src/context/classify/run.ts +0 -348
  62. package/src/context/classify/settings.ts +0 -196
@@ -0,0 +1,49 @@
1
+ ---
2
+ title: Component tests reference ledger
3
+ description: Sources for claude/skills/test-craft/references/component.md, one row per bullet
4
+ ---
5
+
6
+ # Component tests reference ledger
7
+
8
+ The field is UI testing, where Testing Library and Kent C. Dodds's guidance carry how a rendered component is queried, driven, and waited on. Vitest and Playwright carry the real-browser runners. Each row cites the bullet's own inline link where the reference carries one.
9
+
10
+ ## What belongs here
11
+
12
+ | Bullet | Source | Read |
13
+ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
14
+ | Every state a component shows | [Vocke, The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html): in a UI test, "data should be presented to the user, the UI state should change as expected" | 2026-10-24 |
15
+ | Every interaction that changes it | [Vocke, The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html): "User input should trigger the right actions" | 2026-10-24 |
16
+ | Conditional rendering, keys, and focus | [Testing Library, user-event](https://testing-library.com/docs/user-event/intro): it "manipulates the DOM just like a user interaction in the browser would", and "wouldn't let a user click a hidden element or type in a disabled text box" | 2026-10-24 |
17
+ | Not layout, scroll, or motion | [Vitest, Why Browser Mode](https://vitest.dev/guide/browser/why): jsdom and happy-dom "only simulate a browser environment and not an actual browser, which may result in some discrepancies" | 2026-10-24 |
18
+
19
+ ## Render
20
+
21
+ | Bullet | Source | Read |
22
+ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
23
+ | Real children | [Dodds, Write tests](https://kentcdodds.com/blog/write-tests): "When you mock something you're removing all confidence in the integration between what you're testing and what's being mocked." | 2026-10-24 |
24
+ | The network doubled at its edge | [Mock Service Worker, Philosophy](https://mswjs.io/docs/philosophy): it intercepts "outgoing network traffic", which "means having zero changes to your code altogether" | 2026-10-24 |
25
+ | Props and fixtures through a factory | [Pryce, Test Data Builders](http://www.natpryce.com/articles/000714.html): a builder "Has "chainable" public methods for overriding the values in its instance variables", so a test spells only the values it depends on | 2026-10-24 |
26
+
27
+ ## Query and act like a user
28
+
29
+ | Bullet | Source | Read |
30
+ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
31
+ | Role and name first, test id last | [Dodds, Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library): "Which query should I use?" lists "the queries you should attempt to use in the order you should attempt to use them" | 2026-10-24 |
32
+ | User events over synthetic ones | [Dodds, Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library): user-event "provides several methods that resemble the user interactions more closely" | 2026-10-24 |
33
+ | Assert what the user perceives | [Testing Library, Guiding Principles](https://testing-library.com/docs/guiding-principles): a test "should deal with DOM nodes rather than component instances" | 2026-10-24 |
34
+
35
+ ## Wait on one condition
36
+
37
+ | Bullet | Source | Read |
38
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
39
+ | One assertion inside each wait | [Dodds, Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library): "Advice: only put one assertion in a callback." | 2026-10-24 |
40
+ | No side effect inside a wait | [Dodds, Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library): the callback runs "a non-deterministic number of times", so "your side-effect could run multiple times!" | 2026-10-24 |
41
+ | The query that waits | [Dodds, Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library): "find* queries use waitFor under the hood), but the second is simpler and the error message you get will be better" | 2026-10-24 |
42
+
43
+ ## When a simulated DOM cannot render it
44
+
45
+ | Bullet | Source | Read |
46
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
47
+ | Vitest browser mode | [Vitest, Browser Mode](https://vitest.dev/guide/browser/): the feature "allows you to run your tests in the browser natively, providing access to browser globals like window and document" | 2026-10-24 |
48
+ | Astro through its client module | [Astro, Testing](https://docs.astro.build/en/guides/testing/): "Use Astro’s getViteConfig() helper in your vitest.config.ts configuration file to set up Vitest with your Astro project’s settings", while [Vitest's component list](https://vitest.dev/guide/browser/component-testing) names no Astro | 2026-10-24 |
49
+ | Playwright component testing | [Playwright, Components](https://playwright.dev/docs/test-components): "It is stable. Tests import test and expect from plain @playwright/test", and a user of the removed packages should "stay on Playwright 1.62 until you have followed the migration guide" | 2026-10-24 |
@@ -0,0 +1,55 @@
1
+ ---
2
+ title: End to end tests reference ledger
3
+ description: Sources for claude/skills/test-craft/references/e2e.md, one row per bullet
4
+ ---
5
+
6
+ # End to end tests reference ledger
7
+
8
+ The field is browser test automation. Playwright's own documentation carries the runner rules, at the version the reference states, and Ham Vocke's pyramid carries which journeys earn a browser run. Each row cites the bullet's own inline link where the reference carries one.
9
+
10
+ The two worked failures report what one project's suite did, so each of their rows is a house choice naming the failure the incident shows rather than an outside citation.
11
+
12
+ ## What earns a browser run
13
+
14
+ | Bullet | Source | Read |
15
+ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
16
+ | A journey the product cannot lose | [Vocke, The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html): "your most valuable customer journey could be a user searching for a product, putting it in the shopping basket and doing a checkout" | 2026-10-24 |
17
+ | What only a real browser computes | [Vitest, Why Browser Mode](https://vitest.dev/guide/browser/why): "To achieve the highest level of confidence in our tests, it's crucial to test in a real browser environment." | 2026-10-24 |
18
+ | Not a state a component test reaches | [Vocke, The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html): end-to-end tests "are notoriously flaky and often fail for unexpected and unforeseeable reasons" | 2026-10-24 |
19
+ | Not copy | [obra/superpowers, Writing Good Tests](https://github.com/obra/superpowers/blob/5bf4e78/skills/test-driven-development/writing-good-tests.md): a test pinning "exact message wording" "fires on redesign and sleeps through bugs" | 2026-10-24 |
20
+
21
+ ## Settle on conditions
22
+
23
+ | Bullet | Source | Read |
24
+ | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
25
+ | Web-first assertions | [Playwright, Best Practices](https://playwright.dev/docs/best-practices): "By using web first assertions Playwright will wait until the expected condition is met." | 2026-10-24 |
26
+ | Actionability waits, a read does not | [Playwright, Actionability](https://playwright.dev/docs/actionability): an element is "Stable , as in not animating or completed animation", while with "isVisible() the test won't wait a single second", per [Best Practices](https://playwright.dev/docs/best-practices) | 2026-10-24 |
27
+ | No fixed pause for a condition | [Fowler, Eradicating Non-Determinism in Tests](https://martinfowler.com/articles/nonDeterminism.html): "Never use bare sleeps to wait for asynchonous responses: use a callback or polling.", typo as published | 2026-10-24 |
28
+ | A smooth scroll settles on `scrollend` | [MDN, Element: scrollend event](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollend_event): "The scrollend event fires when element scrolling has completed." The stillness fallback is a House choice: a target already in view never scrolls, so no event comes | 2026-10-24 |
29
+
30
+ ## Motion off by default
31
+
32
+ | Bullet | Source | Read |
33
+ | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
34
+ | Reduced unless the test asserts motion | [Playwright, Browser](https://playwright.dev/docs/api/class-browser#browser-new-context): `reducedMotion` "Emulates 'prefers-reduced-motion' media feature", and a test opts back in through `page.emulateMedia()` | 2026-10-24 |
35
+ | Set once in `use.contextOptions` | [Playwright, Browser](https://playwright.dev/docs/api/class-browser#browser-new-context): `reducedMotion` "Defaults to 'no-preference'", and [TestOptions](https://playwright.dev/docs/api/class-testoptions) shows `use : { contextOptions : { reducedMotion : 'reduce' } }` | 2026-10-24 |
36
+ | A top-level `use.reducedMotion` from 1.50 | [Playwright, TestOptions](https://playwright.dev/docs/api/class-testoptions): `testOptions.reducedMotion` was added in Playwright 1.50 and "Emulates 'prefers-reduced-motion' media feature", with `use : { reducedMotion : 'reduce' }` as its usage | 2026-10-25 |
37
+
38
+ ## Isolation and speed
39
+
40
+ | Bullet | Source | Read |
41
+ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
42
+ | Every test its own state | [Playwright, Isolation](https://playwright.dev/docs/browser-contexts): "Tests written with Playwright execute in isolated clean-slate environments called browser contexts." | 2026-10-24 |
43
+ | Workers and sharding over cut waits | [Playwright, Best Practices](https://playwright.dev/docs/best-practices): "Playwright runs tests in parallel by default", and "Playwright can shard a test suite, so that it can be executed on multiple machines" | 2026-10-24 |
44
+ | A suite failing as workers rise | House choice: an assertion that passes on one worker and fails on four depends on machine load, and no settle at that layer removes the dependence | 2026-10-24 |
45
+ | A trace on the first retry | [Playwright, Best Practices](https://playwright.dev/docs/best-practices): traces "are set to run on CI on the first retry of a failed test. We don't recommend setting this to on so that traces are run on every test as it's very performance heavy." | 2026-10-24 |
46
+ | Count the cost before adding a test | House choice: a test's runtime multiplies by every engine and every push, so a suite grown one cheap-looking test at a time is what makes the gate slow | 2026-10-24 |
47
+
48
+ ## Two worked failures
49
+
50
+ | Bullet | Source | Read |
51
+ | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
52
+ | A copy edit was a decision, not a bug | House choice: an assertion on exact wording in a browser run is a change detector, failing every engine on an edit nobody would call a bug | 2026-10-24 |
53
+ | The copy belonged at unit | House choice: a copy assertion run in a browser holds a deploy on every wording change, where a unit test beside the copy fails once and fast | 2026-10-24 |
54
+ | The settle was right and the layer wrong | House choice: an assertion whose result depends on CI load keeps failing at the layer where load decides it, however correct its settle | 2026-10-24 |
55
+ | The focus behavior moves down a layer | House choice: a focus assertion on the whole-site run flakes its file and skips a deploy, where a real-browser component test observes focus without the site's load | 2026-10-24 |
@@ -0,0 +1,54 @@
1
+ ---
2
+ title: Unit and integration tests reference ledger
3
+ description: Sources for claude/skills/test-craft/references/unit.md, one row per bullet and ordered step
4
+ ---
5
+
6
+ # Unit and integration tests reference ledger
7
+
8
+ The field is unit testing practice. `addyosmani/agent-skills` and `obra/superpowers`, the two sources the reference cites inline, carry the doubles order, DAMP, and the mutation check. Software Engineering at Google, Fowler, and the Go wiki carry the rest.
9
+
10
+ The four ordered steps under `## Doubles` and the three under `## The mutation check` each take a row, the way a bullet does.
11
+
12
+ ## Doubles
13
+
14
+ | Bullet | Source | Read |
15
+ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
16
+ | Real first | [addyosmani/agent-skills, Test-Driven Development](https://github.com/addyosmani/agent-skills/blob/c004a74/skills/test-driven-development/SKILL.md): "1. Real implementation → Highest confidence, catches real bugs" | 2026-10-24 |
17
+ | Fake second | [addyosmani/agent-skills, Test-Driven Development](https://github.com/addyosmani/agent-skills/blob/c004a74/skills/test-driven-development/SKILL.md): "2. Fake → In-memory version of a dependency (e.g., fake DB)" | 2026-10-24 |
18
+ | Stub third | [addyosmani/agent-skills, Test-Driven Development](https://github.com/addyosmani/agent-skills/blob/c004a74/skills/test-driven-development/SKILL.md): "3. Stub → Returns canned data, no behavior" | 2026-10-24 |
19
+ | Mock last, when the call is the contract | [Software Engineering at Google, ch. 13](https://abseil.io/resources/swe-book/html/ch13.html): "interaction testing is useful in certain situations but should be avoided when possible because overuse can easily result in brittle tests" | 2026-10-24 |
20
+ | Double only at an unowned boundary | [addyosmani/agent-skills, Test-Driven Development](https://github.com/addyosmani/agent-skills/blob/c004a74/skills/test-driven-development/SKILL.md): "Mock only at boundaries where real deps are slow or non-deterministic" | 2026-10-24 |
21
+ | Never assert a mock exists | [obra/superpowers, Writing Good Tests](https://github.com/obra/superpowers/blob/5bf4e78/skills/test-driven-development/writing-good-tests.md): "A mock assertion passes when the mock is present and fails when it is absent" | 2026-10-24 |
22
+ | Clock and randomness injected | [Fowler, Eradicating Non-Determinism in Tests](https://martinfowler.com/articles/nonDeterminism.html): "always wrap the system clock with routines that can be replaced with a seeded value for testing" | 2026-10-24 |
23
+
24
+ ## Descriptive over shared
25
+
26
+ | Bullet | Source | Read |
27
+ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
28
+ | DAMP over DRY | [addyosmani/agent-skills, Test-Driven Development](https://github.com/addyosmani/agent-skills/blob/c004a74/skills/test-driven-development/SKILL.md): "each test should tell a complete story without requiring the reader to trace through shared helpers" | 2026-10-24 |
29
+ | A factory with the key values shown | [Software Engineering at Google, ch. 12](https://abseil.io/resources/swe-book/html/ch12.html): a test is not complete when "important details are hidden away in helper methods that the reader can’t see" | 2026-10-24 |
30
+ | No shared mutable state | [Fowler, Eradicating Non-Determinism in Tests](https://martinfowler.com/articles/nonDeterminism.html): "If one test creates some data in the database and leaves it lying around, it can corrupt the run of another test" | 2026-10-24 |
31
+
32
+ ## Table-driven cases
33
+
34
+ | Bullet | Source | Read |
35
+ | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
36
+ | One table for inputs differing by value | [Go wiki, TableDrivenTests](https://go.dev/wiki/TableDrivenTests): "Each table entry is a complete test case with inputs and expected results, and sometimes with additional information such as a test name" | 2026-10-24 |
37
+ | A row holds input and expected output | [obra/superpowers, Writing Good Tests](https://github.com/obra/superpowers/blob/5bf4e78/skills/test-driven-development/writing-good-tests.md): "table-driven tests with literal `want` values are the preferred shape" | 2026-10-24 |
38
+ | Split the table on a new assertion | [Go wiki, TableDrivenTests](https://go.dev/wiki/TableDrivenTests): "the actual test simply iterates through all table entries and for each entry performs the necessary tests", one test body every row shares | 2026-10-24 |
39
+
40
+ ## Integration at a boundary
41
+
42
+ | Bullet | Source | Read |
43
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
44
+ | Through the callers' interface | [Vocke, The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html): "Narrow integration tests live at the boundary of your service", "triggering an action that leads to integrating with the outside part" | 2026-10-24 |
45
+ | A temporary directory or database each | [Fowler, Eradicating Non-Determinism in Tests](https://martinfowler.com/articles/nonDeterminism.html): tests need "a well-known state at the beginning of the test", and "Properly isolated tests can be run in any sequence." | 2026-10-24 |
46
+ | The failure path at the boundary | [Software Engineering at Google, ch. 11](https://abseil.io/resources/swe-book/html/ch11.html): a review is easier with "thorough tests that demonstrate code correctness, edge cases, and error conditions" | 2026-10-24 |
47
+
48
+ ## The mutation check
49
+
50
+ | Bullet | Source | Read |
51
+ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
52
+ | Name three to five breaks | [obra/superpowers, Writing Good Tests](https://github.com/obra/superpowers/blob/5bf4e78/skills/test-driven-development/writing-good-tests.md): "mentally mutate the production code", and "at least one test should fail for each realistic mutation" | 2026-10-24 |
53
+ | Say which test fails for each | [Stryker, What is mutation testing?](https://stryker-mutator.io/docs/): "If your tests fail then the mutant is killed", and "If your tests passed, the mutant survived" | 2026-10-24 |
54
+ | An uncaught break is a missing case | [obra/superpowers, Writing Good Tests](https://github.com/obra/superpowers/blob/5bf4e78/skills/test-driven-development/writing-good-tests.md): "A mutation nothing catches marks the behavior as unprotected" | 2026-10-24 |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.63.0",
4
+ "version": "5.65.0",
5
5
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -69,8 +69,8 @@
69
69
  "astro": "^6.1.9",
70
70
  "husky": "^9.1.7",
71
71
  "micromatch": "^4.0.8",
72
- "oxfmt": "0.73.0",
73
- "oxlint": "1.88.0",
72
+ "oxfmt": "0.74.0",
73
+ "oxlint": "1.89.0",
74
74
  "prettier": "^3.8.1",
75
75
  "prettier-plugin-astro": "^0.14.1",
76
76
  "prettier-plugin-tailwindcss": "^0.8.1",
@@ -2,7 +2,6 @@ import type { SkillsAuditRefusal } from '@/claude/skills-audit'
2
2
  import type { RankRefusal } from '@/claude/skills-rank'
3
3
  import type { ReachRefusal } from '@/claude/skills-reach'
4
4
  import type { ContextAuditRefusal } from '@/context/audit'
5
- import type { SweepVerdict } from '@/context/classify/patterns'
6
5
  import type { AuditRefusal } from '@/deps/audit'
7
6
  import type { RuleAuditRefusal } from '@/gov/audit'
8
7
  import type { RestatedRefusal } from '@/gov/restated'
@@ -161,45 +160,6 @@ function requirementsCounts(
161
160
  return { requirementsOverWords: record.words > record.wordCap ? 1 : 0 }
162
161
  }
163
162
 
164
- /**
165
- * The retained key for each verdict the sweep can return. A record rather than
166
- * a list, so the compiler fails here when the classifier gains a verdict,
167
- * instead of that verdict's sections dropping out of the tally.
168
- */
169
- const PLACEMENT_KEYS: Record<SweepVerdict, string> = {
170
- KEEP: 'keep',
171
- REWRITE: 'rewrite',
172
- MOVE: 'move',
173
- }
174
-
175
- function isPlacementVerdict(value: unknown): value is SweepVerdict {
176
- return typeof value === 'string' && Object.hasOwn(PLACEMENT_KEYS, value)
177
- }
178
-
179
- /**
180
- * Tallies the sweep's findings by verdict, with a zero for a verdict nothing
181
- * returned, so a baseline reads the same three keys on every run.
182
- *
183
- * `KEEP` is retained beside the two that ask for a change. It moves when a
184
- * section is added or split, which is what tells a falling `rewrite` count
185
- * from a corpus that shrank.
186
- */
187
- function placementCounts(record: unknown): Record<string, number> | undefined {
188
- const findings = asObject(record)?.findings
189
- if (!Array.isArray(findings)) return undefined
190
-
191
- const counts: Record<string, number> = Object.fromEntries(
192
- Object.values(PLACEMENT_KEYS).map((key) => [key, 0]),
193
- )
194
- for (const finding of findings) {
195
- const verdict = asObject(finding)?.verdict
196
- if (!isPlacementVerdict(verdict)) return undefined
197
- counts[PLACEMENT_KEYS[verdict]] += 1
198
- }
199
-
200
- return counts
201
- }
202
-
203
163
  function contextCounts(record: unknown): Record<string, number> | undefined {
204
164
  const root = asObject(record)
205
165
  if (root === undefined) return undefined
@@ -526,18 +486,6 @@ export const AUDITS: readonly AuditSpec[] = [
526
486
  absentReasons: ['no-folders'] satisfies ContextAuditRefusal[],
527
487
  counts: contextCounts,
528
488
  },
529
- {
530
- id: 'placement',
531
- label: 'Context placement',
532
- // Pinned to the regex layer rather than the resolved classifier setting.
533
- // A count that moves with whichever model a machine runs is not
534
- // comparable across clones, and the model layer turns a sub-second read
535
- // into a multi-minute one. `document-health` is where the model reads.
536
- argv: ['context', 'classify', 'sweep', '--backend', 'off', '--json'],
537
- gatingExits: [],
538
- corpus: 'tracked',
539
- counts: placementCounts,
540
- },
541
489
  {
542
490
  id: 'markdown',
543
491
  label: 'Markdown corpus',
@@ -5,8 +5,8 @@ import { plural } from '@/cli/ui'
5
5
  import {
6
6
  compareLedger,
7
7
  countLedgerRows,
8
+ type HeadingCount,
8
9
  type LedgerMismatch,
9
- measureRule,
10
10
  } from '@/gov/audit'
11
11
  import { parseFrontmatter, readField } from '@/indexes/frontmatter'
12
12
  import {
@@ -84,19 +84,56 @@ export const LEDGER_SKILLS: readonly string[] = [
84
84
  join(SHIPPED_CORPUS, 'ui-craft'),
85
85
  ]
86
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
+
87
98
  /**
88
99
  * The ledger-listed skills whose every reference but the source record carries
89
100
  * a ledger of its own, one table row per bullet under each H2, the way a rule
90
- * in a practice folder does. The ledgers sit outside the plugin as rule
91
- * ledgers do, so a target pays nothing for them.
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.
92
105
  */
93
- export const BULLET_LEDGER_SKILLS: readonly string[] = [
94
- join(SHIPPED_CORPUS, 'ui-craft'),
95
- ]
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
+ [join(SHIPPED_CORPUS, 'review-craft')]: {
117
+ exempt: references('ui'),
118
+ body: true,
119
+ },
120
+ [join(SHIPPED_CORPUS, 'teach-craft')]: { exempt: [], body: true },
121
+ [join(SHIPPED_CORPUS, 'test-craft')]: { exempt: [], body: true },
122
+ [join(SHIPPED_CORPUS, 'codebase-layout')]: { exempt: [], body: true },
123
+ }
96
124
 
97
- /** Where a per-bullet ledger lives, as `<skill>/<reference>.md` below it. */
125
+ /** Where a per-bullet ledger lives, as `<skill>/<file>.md` below it. */
98
126
  export const SKILL_LEDGERS = join('governance', 'ledger', 'skills')
99
127
 
128
+ /**
129
+ * The ledger a ledgered file pairs with, keyed by the file's own name, so a
130
+ * reference drops its `references/` segment and a `SKILL.md` keeps its name.
131
+ */
132
+ export function skillLedgerPath(folder: string, file: string): string {
133
+ const inside = relative('references', file)
134
+ return join(SKILL_LEDGERS, folder, inside.startsWith('..') ? file : inside)
135
+ }
136
+
100
137
  /** The closing H2s a practice skill carries, matched exactly outside fences. */
101
138
  export const PRACTICE_SECTIONS: readonly string[] = [
102
139
  'Excuses and rebuttals',
@@ -104,6 +141,16 @@ export const PRACTICE_SECTIONS: readonly string[] = [
104
141
  'Before handing over',
105
142
  ]
106
143
 
144
+ /**
145
+ * The H2s that end a listed body's concern groups. The closing sections
146
+ * restate the groups' rules as excuses, signs, and checks, and the delegates
147
+ * section routes, so neither states a rule of its own to source.
148
+ */
149
+ export const BODY_CLOSERS: readonly string[] = [
150
+ ...PRACTICE_SECTIONS,
151
+ 'What this delegates',
152
+ ]
153
+
107
154
  /** The source ledger every practice skill carries beside its body. */
108
155
  export const PRACTICE_LEDGER = join('references', 'adopted.md')
109
156
 
@@ -157,6 +204,12 @@ const ISSUE_NUMBER = /(?<![\w&/#])#\d+\b/g
157
204
 
158
205
  const TOP_LEVEL_BULLET = /^(?:[-*+]|\d+[.)])\s/
159
206
 
207
+ /**
208
+ * A line a skill ledger pairs with a row: a top-level `- ` bullet as a rule
209
+ * writes one, or a top-level ordered step, which states a directive as surely.
210
+ */
211
+ const LEDGERED_LINE = /^(?:- |\d+[.)]\s)/
212
+
160
213
  const WORD = /\S*[\p{L}\p{N}]\S*/gu
161
214
 
162
215
  export interface SkillFinding {
@@ -213,8 +266,10 @@ interface SkillSource {
213
266
  * requirement is where the standard sends a date.
214
267
  */
215
268
  readonly bodies: readonly SkillText[]
216
- /** One entry per reference of a skill on the per-bullet ledger list. */
269
+ /** One entry per ledgered file of a skill on the per-bullet ledger list. */
217
270
  readonly referenceLedgers: readonly ReferenceLedger[]
271
+ /** Exempted references the skill no longer carries, which exempt nothing. */
272
+ readonly staleExemptions: readonly string[]
218
273
  }
219
274
 
220
275
  interface ReferenceLedger {
@@ -342,15 +397,22 @@ async function readSkill(
342
397
 
343
398
  const fields = parseFrontmatter(body)
344
399
  const skillRel = join(rel, folder)
345
- const referenceLedgers = BULLET_LEDGER_SKILLS.includes(skillRel)
346
- ? await readReferenceLedgers(
347
- root,
348
- skillRel,
349
- folder,
350
- references,
351
- referenceTexts,
352
- )
353
- : []
400
+ const scope = BULLET_LEDGER_SKILLS[skillRel]
401
+ const referenceLedgers =
402
+ scope === undefined
403
+ ? []
404
+ : await readReferenceLedgers(root, skillRel, folder, [
405
+ ...(scope.body ? [{ path: 'SKILL.md', text: body }] : []),
406
+ ...references
407
+ .map((path, index) => ({ path, text: referenceTexts[index] ?? '' }))
408
+ .filter(
409
+ ({ path }) =>
410
+ path !== PRACTICE_LEDGER && !scope.exempt.includes(path),
411
+ ),
412
+ ])
413
+ const staleExemptions = (scope?.exempt ?? [])
414
+ .filter((path) => !references.includes(path))
415
+ .map((path) => join(skillRel, path))
354
416
 
355
417
  return {
356
418
  rel: skillRel,
@@ -373,44 +435,61 @@ async function readSkill(
373
435
  })),
374
436
  ],
375
437
  referenceLedgers,
438
+ staleExemptions,
376
439
  }
377
440
  }
378
441
 
379
442
  /**
380
- * Pairs each reference's bullets with its ledger's rows through the rule
381
- * audit's own comparison, so a reference and a rule are held to one count.
382
- * The source record names authorities rather than stating bullets, so it is
383
- * the one reference asked for no ledger.
443
+ * Pairs each ledgered file's directives with its ledger's rows through the rule
444
+ * audit's own comparison, so a skill file and a rule mismatch alike.
384
445
  */
385
446
  async function readReferenceLedgers(
386
447
  root: string,
387
448
  skillRel: string,
388
449
  folder: string,
389
- references: readonly string[],
390
- texts: readonly string[],
450
+ files: readonly { path: string; text: string }[],
391
451
  ): Promise<ReferenceLedger[]> {
392
452
  return Promise.all(
393
- references
394
- .map((path, index) => ({ path, text: texts[index] ?? '' }))
395
- .filter(({ path }) => path !== PRACTICE_LEDGER)
396
- .map(async ({ path, text }) => {
397
- const ledgerRel = join(
398
- SKILL_LEDGERS,
399
- folder,
400
- relative('references', path),
401
- )
402
- const ledgerPath = join(root, ledgerRel)
403
- const mismatches = existsSync(ledgerPath)
404
- ? compareLedger(
405
- measureRule(text).groups,
406
- countLedgerRows(await Bun.file(ledgerPath).text()),
407
- )
408
- : undefined
409
- return { rel: join(skillRel, path), ledgerRel, mismatches }
410
- }),
453
+ files.map(async ({ path, text }) => {
454
+ const ledgerRel = skillLedgerPath(folder, path)
455
+ const ledgerPath = join(root, ledgerRel)
456
+ const mismatches = existsSync(ledgerPath)
457
+ ? compareLedger(
458
+ ledgerGroups(text, path === 'SKILL.md'),
459
+ countLedgerRows(await Bun.file(ledgerPath).text()),
460
+ )
461
+ : undefined
462
+ return { rel: join(skillRel, path), ledgerRel, mismatches }
463
+ }),
411
464
  )
412
465
  }
413
466
 
467
+ /**
468
+ * Directives per H2, `null` for any above the first. A contents list repeats
469
+ * the headings rather than stating a rule, so `## Contents` takes no rows. A
470
+ * nested bullet elaborates its parent, so only a top-level line counts. A body
471
+ * stops at its first closer, and a ledger table under one then reads as rows
472
+ * with no bullets rather than passing unread.
473
+ */
474
+ function ledgerGroups(text: string, isBody: boolean): HeadingCount[] {
475
+ const counts = new Map<string | null, number>()
476
+ let heading: string | null = null
477
+
478
+ for (const line of bodyLines(text)) {
479
+ if (line.fenced) continue
480
+ const section = SECTION.exec(line.text)?.[1]
481
+ if (section !== undefined) {
482
+ if (isBody && BODY_CLOSERS.includes(section)) break
483
+ heading = section
484
+ continue
485
+ }
486
+ if (heading === CONTENTS_SECTION || !LEDGERED_LINE.test(line.text)) continue
487
+ counts.set(heading, (counts.get(heading) ?? 0) + 1)
488
+ }
489
+
490
+ return [...counts].map(([key, count]) => ({ heading: key, count }))
491
+ }
492
+
414
493
  /**
415
494
  * A key carrying an empty value declares nothing, so blank reads as absent
416
495
  * rather than as a name that disagrees with every folder.
@@ -603,6 +682,10 @@ function practiceFindings(source: SkillSource): SkillFinding[] {
603
682
  ...sections,
604
683
  ...ledgerFindings(source),
605
684
  ...source.referenceLedgers.flatMap(referenceLedgerFindings),
685
+ ...source.staleExemptions.map((rel) => ({
686
+ rel,
687
+ detail: 'missing file: exempt from a ledger',
688
+ })),
606
689
  ]
607
690
  }
608
691
 
@@ -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
  /**
@@ -168,6 +168,15 @@ interface RoutingOptions {
168
168
  const SEEDED_FILES: readonly string[] = ['REQUIREMENTS.md', 'DESIGN.md']
169
169
  const SEEDED_DIRS: readonly string[] = ['tasks']
170
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
+
171
180
  /**
172
181
  * Where a `SEEDED_DIRS` entry resolves for the sync presence check.
173
182
  *
@@ -679,7 +688,7 @@ async function runSync(target: string): Promise<number> {
679
688
  // already holds.
680
689
  logStep('Seeded')
681
690
  for (const name of SEEDED_FILES) {
682
- if (existsSync(join(resolved, '.claude', name))) logInfo(name)
691
+ if (existsSync(seededFilePath(resolved, name))) logInfo(name)
683
692
  else logWarn(`${name} missing. Run \`canon claude init\``)
684
693
  }
685
694
  for (const name of SEEDED_DIRS) {
@@ -1771,7 +1780,7 @@ function reportPracticeShape(report: SkillsAudit): void {
1771
1780
  `A listed practice skill closes with ${PRACTICE_SECTIONS.map((section) => `## ${section}`).join(', ')}, and every skill on the wider ledger list carries ${PRACTICE_LEDGER}.`,
1772
1781
  )
1773
1782
  logInfo(
1774
- `A skill on the per-bullet list, ${plural(BULLET_LEDGER_SKILLS.length, 'skill')} here, also matches each other reference to a ledger under ${SKILL_LEDGERS}, one row per bullet under each H2.`,
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.`,
1775
1784
  )
1776
1785
  logInfo(
1777
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.`,