@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.
- package/CHANGELOG.md +26 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/references/adopted.md +1 -1
- package/claude/skills/context-fold/REQUIREMENT.md +2 -3
- package/claude/skills/context-fold/SKILL.md +1 -11
- package/claude/skills/context-fold/references/completion-report.md +8 -9
- package/claude/skills/create-skill/references/skill-practice.md +16 -1
- package/claude/skills/design-taste/references/adopted.md +17 -97
- package/claude/skills/design-taste/references/tells.md +2 -2
- package/claude/skills/document-health/REQUIREMENT.md +1 -1
- package/claude/skills/document-health/SKILL.md +3 -14
- package/claude/skills/document-health/references/axes.md +14 -16
- package/claude/skills/test-craft/references/e2e.md +1 -1
- package/docs/agents/audits.md +2 -6
- package/docs/agents/context-audit.md +1 -1
- package/governance/ledger/skills/api-design/SKILL.md +62 -0
- package/governance/ledger/skills/code-craft/SKILL.md +53 -0
- package/governance/ledger/skills/codebase-layout/SKILL.md +64 -0
- package/governance/ledger/skills/codebase-layout/frontend.md +62 -0
- package/governance/ledger/skills/codebase-layout/tests.md +26 -0
- package/governance/ledger/skills/deprecation-migration/SKILL.md +68 -0
- package/governance/ledger/skills/design-taste/SKILL.md +64 -0
- package/governance/ledger/skills/design-taste/craft.md +66 -0
- package/governance/ledger/skills/design-taste/kinds.md +58 -0
- package/governance/ledger/skills/design-taste/systems.md +42 -0
- package/governance/ledger/skills/design-taste/tells.md +103 -0
- package/governance/ledger/skills/review-craft/SKILL.md +61 -0
- package/governance/ledger/skills/review-craft/security.md +34 -0
- package/governance/ledger/skills/teach-craft/SKILL.md +33 -0
- package/governance/ledger/skills/teach-craft/lesson-craft.md +47 -0
- package/governance/ledger/skills/teach-craft/pedagogy.md +77 -0
- package/governance/ledger/skills/test-craft/SKILL.md +67 -0
- package/governance/ledger/skills/test-craft/component.md +49 -0
- package/governance/ledger/skills/test-craft/e2e.md +55 -0
- package/governance/ledger/skills/test-craft/unit.md +54 -0
- package/package.json +3 -3
- package/src/audits/catalog.ts +0 -52
- package/src/claude/skills-audit.ts +124 -41
- package/src/cli/retired.ts +8 -0
- package/src/commands/claude.ts +11 -2
- package/src/commands/context.ts +0 -425
- package/src/commands/sandbox.ts +247 -2
- package/src/context/citations.ts +9 -3
- package/src/gate/measures.ts +4 -1
- package/src/gate/stages.ts +25 -0
- package/src/git/files.ts +8 -3
- package/src/markdown/gated.ts +1 -1
- package/src/markdown/scope.ts +0 -1
- package/src/migrate/records.ts +6 -0
- package/src/sandbox/replay/expected.ts +137 -0
- package/src/sandbox/replay/run.ts +323 -0
- package/src/sandbox/replay/verdict.ts +102 -0
- package/tooling/astro/configs/playwright.config.ts +2 -2
- package/tooling/nextjs/configs/playwright.config.ts +2 -2
- package/tooling/vite-react/configs/playwright.config.ts +2 -2
- package/claude/skills/context-fold/references/classify.md +0 -87
- package/src/context/classify/extract.ts +0 -443
- package/src/context/classify/ollama.ts +0 -172
- package/src/context/classify/patterns.ts +0 -75
- package/src/context/classify/prompts.ts +0 -74
- package/src/context/classify/run.ts +0 -348
- 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.
|
|
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
|
-
"oxlint": "1.
|
|
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",
|
package/src/audits/catalog.ts
CHANGED
|
@@ -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.
|
|
91
|
-
*
|
|
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:
|
|
94
|
-
|
|
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>/<
|
|
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
|
|
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
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
381
|
-
* audit's own comparison, so a
|
|
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
|
-
|
|
390
|
-
texts: readonly string[],
|
|
450
|
+
files: readonly { path: string; text: string }[],
|
|
391
451
|
): Promise<ReferenceLedger[]> {
|
|
392
452
|
return Promise.all(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
|
package/src/cli/retired.ts
CHANGED
|
@@ -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
|
/**
|
package/src/commands/claude.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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.`,
|