@erclx/canon 5.64.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.
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: Test craft skill ledger
3
+ description: Sources for claude/skills/test-craft/SKILL.md, one row per bullet in its concern groups
4
+ ---
5
+
6
+ # Test craft skill ledger
7
+
8
+ The field is software testing practice. Software Engineering at Google carries test sizes, clarity, and doubles, Fowler and Ham Vocke carry the pyramid, and `obra/superpowers` carries the opening question the body asks of every test. The closing sections and `## What this delegates` restate these rules or route, so they take no table.
9
+
10
+ `references/adopted.md` stays the record of what was adopted and declined at the source level.
11
+
12
+ ## Open every test with one question
13
+
14
+ | Bullet | Source | Read |
15
+ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
16
+ | A bug is what the test is for | [obra/superpowers, Writing Good Tests](https://github.com/obra/superpowers/blob/5bf4e78/skills/test-driven-development/writing-good-tests.md): "what production change should make this test fail", then whether "that change [is] a bug or a decision" | 2026-10-24 |
17
+ | A decision makes a change detector | [obra/superpowers, Writing Good Tests](https://github.com/obra/superpowers/blob/5bf4e78/skills/test-driven-development/writing-good-tests.md): when "only intentional decisions can fail a test", such as "exact message wording", "it fires on redesign and sleeps through bugs" | 2026-10-24 |
18
+ | No answer proves nothing yet | [obra/superpowers, Writing Good Tests](https://github.com/obra/superpowers/blob/5bf4e78/skills/test-driven-development/writing-good-tests.md): "Cannot name one → redesign around an observable behavior" | 2026-10-24 |
19
+ | Cover the edges as their own cases | [Vocke, The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html): unit tests "should ensure that all your non-trivial code paths are tested (including happy path and edge cases)" | 2026-10-24 |
20
+
21
+ ## Put the test at the smallest layer that can catch the break
22
+
23
+ | Bullet | Source | Read |
24
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
25
+ | The row read off the behavior | [Software Engineering at Google, ch. 11](https://abseil.io/resources/swe-book/html/ch11.html): "small tests run in a single process, medium tests run on a single machine, and large tests run wherever they want", a size set by what the test touches | 2026-10-24 |
26
+ | Split a behavior that spans rows | [Vocke, The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html): "Push your tests as far down the test pyramid as you can" | 2026-10-24 |
27
+ | A red higher run names a missing lower one | [Vocke, The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html): "If a higher-level test spots an error and there's no lower-level test failing, you need to write a lower-level test" | 2026-10-24 |
28
+ | Real-browser component before end to end | [Vitest, Component Testing](https://vitest.dev/guide/browser/component-testing): component tests run "in real browser environments", which "provides the most accurate testing environment as your components run in real browsers with actual DOM implementations" | 2026-10-24 |
29
+
30
+ ## Spend end to end on journeys
31
+
32
+ | Bullet | Source | Read |
33
+ | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
34
+ | Journeys the product cannot lose | [Vocke, The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html): "Try to come up with user journeys that define the core value of your product and translate the most important steps of these user journeys into automated end-to-end tests." | 2026-10-24 |
35
+ | No end to end for a component's state | [Vocke, The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html): "UI tests don't have to be on the highest level of your pyramid - you're perfectly able to unit test your UI in all of these frameworks" | 2026-10-24 |
36
+ | Every later run pays for a new one | [Vocke, The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html): "Due to their high maintenance cost you should aim to reduce the number of end-to-end tests to a bare minimum." | 2026-10-24 |
37
+ | No numeric mix | [web.dev, Pyramid or Crab?](https://web.dev/articles/ta-strategies): the strategy "should fir your architecture, your requirements, and last but not least, your users and their requirements", typo as published | 2026-10-24 |
38
+
39
+ ## Keep test code readable over clever
40
+
41
+ | Bullet | Source | Read |
42
+ | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
43
+ | A repeated arrange over a shared helper | [Software Engineering at Google, ch. 12](https://abseil.io/resources/swe-book/html/ch12.html): "A little bit of duplication is OK in tests so long as that duplication makes the test simpler and clearer." | 2026-10-24 |
44
+ | Test data through a factory or builder | [Pryce, Test Data Builders](http://www.natpryce.com/articles/000714.html): "For each class you want to use in a test, create a Builder for that class", one that "Initialises its instance variables to commonly used or safe values" | 2026-10-24 |
45
+ | Assert through the public surface | [Software Engineering at Google, ch. 12](https://abseil.io/resources/swe-book/html/ch12.html): tests should "make calls against its public API rather than its implementation details" | 2026-10-24 |
46
+ | Real, then fake, then stub, mock last | [addyosmani/agent-skills, Test-Driven Development](https://github.com/addyosmani/agent-skills/blob/c004a74/skills/test-driven-development/SKILL.md): "Preference order (most to least preferred): 1. Real implementation", then fake, stub, and mock | 2026-10-24 |
47
+
48
+ ## Hold the structure floors
49
+
50
+ | Bullet | Source | Read |
51
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
52
+ | Arrange, act, assert | [Vocke, The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html): "There's a nice mnemonic to remember this structure: “Arrange, Act, Assert”" | 2026-10-24 |
53
+ | One behavior per test | [Software Engineering at Google, ch. 12](https://abseil.io/resources/swe-book/html/ch12.html): "Each test should cover only a single behavior" | 2026-10-24 |
54
+ | No branches or loops in a test body | [Software Engineering at Google, ch. 12](https://abseil.io/resources/swe-book/html/ch12.html): "in test code, stick to straight-line code over clever logic" | 2026-10-24 |
55
+ | No coverage percentage as a target | [Fowler, Test Coverage](https://martinfowler.com/bliki/TestCoverage.html): "If you make a certain level of coverage a target, people will try to attain it. The trouble is that high coverage numbers are too easy to reach with low quality testing." | 2026-10-24 |
56
+ | Nesting kept to two levels | [Software Engineering at Google, ch. 12](https://abseil.io/resources/swe-book/html/ch12.html) groups tests "nested within one another and named using strings". The two-level cap is a House choice: deeper nesting spreads a test's setup across hooks, which [Dodds](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) names as "the number one reason I strongly recommend against nested tests" | 2026-10-24 |
57
+ | Independent tests, state restored | [Beck, Test Desiderata](https://testdesiderata.com/): Isolated tests "should return the same results regardless of the order in which they are run." | 2026-10-24 |
58
+ | Await every async operation | [Jest, Testing Asynchronous Code](https://jestjs.io/docs/asynchronous): "if you omit the return / await statement, your test will complete before the promise returned from fetchData resolves or rejects" | 2026-10-24 |
59
+ | Assertions, never a snapshot | [Dodds, Effective Snapshot Testing](https://kentcdodds.com/blog/effective-snapshot-testing), quoting Justin Searls: "Snapshot tests lack (or at least, fail to encourage) expressing the author's intent as to what the code does (much less why)" | 2026-10-24 |
60
+
61
+ ## Read the reference for the layer
62
+
63
+ | Bullet | Source | Read |
64
+ | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
65
+ | Unit and integration reference | House choice: a unit test written without `unit.md` reaches for a mock before a real collaborator or a fake, and skips the mutation check that finds its missing case | 2026-10-24 |
66
+ | Component reference | House choice: a component test written without `component.md` queries by test id and fires synthetic events, so it passes on markup a user cannot reach | 2026-10-24 |
67
+ | End to end reference | House choice: a first spec written without `e2e.md` pauses for a fixed time and runs with motion on, the two causes of the flakes its worked failures record | 2026-10-24 |
@@ -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.64.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",
@@ -113,6 +113,13 @@ export const BULLET_LEDGER_SKILLS: Readonly<Record<string, BulletLedgerScope>> =
113
113
  exempt: references('preflight', 'vocabulary'),
114
114
  body: true,
115
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 },
116
123
  }
117
124
 
118
125
  /** Where a per-bullet ledger lives, as `<skill>/<file>.md` below it. */
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
- import { join } from 'node:path'
2
+ import { basename, dirname, join, relative, resolve } from 'node:path'
3
3
  import type { Command } from 'commander'
4
4
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
5
5
  import {
@@ -27,8 +27,24 @@ import {
27
27
  } from '@/sandbox/expect'
28
28
  import { runHeadless } from '@/sandbox/headless/run'
29
29
  import { runSandbox } from '@/sandbox/provision'
30
+ import {
31
+ EXPECTED_FILE,
32
+ expectationOf,
33
+ judgeDrift,
34
+ readExpectation,
35
+ writeExpectation,
36
+ type ReplayExpectation,
37
+ } from '@/sandbox/replay/expected'
38
+ import {
39
+ checkoutRefusal,
40
+ hasFormatter,
41
+ listSnapshots,
42
+ replaySource,
43
+ type ReplayResult,
44
+ type ReplaySource,
45
+ } from '@/sandbox/replay/run'
30
46
  import { listCategories, listScenarioFiles } from '@/sandbox/scenario'
31
- import { sandboxTree } from '@/sandbox/tree'
47
+ import { assertSandboxDirSafe, sandboxRoots, sandboxTree } from '@/sandbox/tree'
32
48
  import {
33
49
  frameError,
34
50
  intro,
@@ -434,6 +450,194 @@ async function runEquivalence(
434
450
  process.exitCode = equivalenceExitCode(record)
435
451
  }
436
452
 
453
+ interface ReplayOptions {
454
+ readonly json?: boolean
455
+ readonly record?: boolean
456
+ }
457
+
458
+ const REPLAY_FINDINGS_SHOWN = 5
459
+
460
+ function refuseReplay(reason: string, message: string, json: boolean): void {
461
+ logError(message)
462
+ if (json)
463
+ process.stdout.write(`${JSON.stringify({ ok: false, reason, message })}\n`)
464
+ outro()
465
+ process.exitCode = 1
466
+ }
467
+
468
+ /**
469
+ * Replays each consumer through a sync with this checkout's CLI and judges
470
+ * the reads that follow. Bare, it replays the frozen snapshots, which is what
471
+ * the gate runs. Given paths, it copies each real checkout, the operator's
472
+ * step before a release. Every source is checked before any replay starts, so
473
+ * a refusal never arrives after half the runs. A snapshot is judged against
474
+ * the answer recorded beside it, since a frozen tree answers the same on every
475
+ * push, and `--record` rewrites that answer from a run that broke nothing.
476
+ */
477
+ function runReplay(paths: string[], options: ReplayOptions): void {
478
+ if (reportAbsentScenarioTree()) return
479
+
480
+ intro('canon sandbox replay')
481
+ const json = options.json === true
482
+ const record = options.record === true
483
+
484
+ if (record && paths.length > 0) {
485
+ refuseReplay(
486
+ 'record-checkout',
487
+ 'A named checkout moves between runs, so --record pins snapshots only. Drop the paths.',
488
+ json,
489
+ )
490
+ return
491
+ }
492
+
493
+ if (!hasFormatter(PROJECT_ROOT)) {
494
+ refuseReplay(
495
+ 'no-formatter',
496
+ 'No oxfmt under node_modules, so the format step cannot run. Run bun install in this checkout.',
497
+ json,
498
+ )
499
+ return
500
+ }
501
+
502
+ let sources: ReplaySource[]
503
+ if (paths.length === 0) {
504
+ sources = listSnapshots(join(SANDBOX_DIR, 'fixtures', 'replay'))
505
+ if (sources.length === 0) {
506
+ refuseReplay(
507
+ 'no-snapshots',
508
+ 'No snapshot under sandbox/fixtures/replay/',
509
+ json,
510
+ )
511
+ return
512
+ }
513
+ } else {
514
+ sources = []
515
+ for (const raw of paths) {
516
+ const path = resolve(process.cwd(), raw)
517
+ const refusal = checkoutRefusal(path)
518
+ if (refusal !== undefined) {
519
+ refuseReplay(refusal.reason, refusal.message, json)
520
+ return
521
+ }
522
+ sources.push({ name: basename(path), path, kind: 'checkout' })
523
+ }
524
+ }
525
+
526
+ // Beside the provisioned tree rather than inside it, since `canon sandbox
527
+ // clean` wipes that one and a concurrent replay would lose its tree.
528
+ const tempRoot = join(dirname(sandboxTree()), 'replay')
529
+ const unsafe = assertSandboxDirSafe(
530
+ join(tempRoot, 'tree'),
531
+ sandboxRoots(PROJECT_ROOT),
532
+ )
533
+ if (unsafe !== undefined) {
534
+ refuseReplay('unsafe-root', unsafe, json)
535
+ return
536
+ }
537
+
538
+ const expectations = new Map<string, ReplayExpectation | undefined>()
539
+ if (!record)
540
+ for (const source of sources) {
541
+ if (source.kind !== 'snapshot') continue
542
+ try {
543
+ expectations.set(source.name, readExpectation(source.path))
544
+ } catch (error) {
545
+ refuseReplay('bad-expectation', (error as Error).message, json)
546
+ return
547
+ }
548
+ }
549
+
550
+ const replays: ReplayResult[] = []
551
+ for (const source of sources) {
552
+ const run = replaySource(source, {
553
+ checkoutRoot: PROJECT_ROOT,
554
+ tempRoot,
555
+ })
556
+ const result: ReplayResult =
557
+ source.kind === 'snapshot' && !record
558
+ ? {
559
+ ...run,
560
+ steps: judgeDrift(run.steps, expectations.get(source.name)),
561
+ }
562
+ : run
563
+ replays.push(result)
564
+
565
+ logStep(`${result.name} (${result.kind})`)
566
+ for (const step of result.steps) {
567
+ if (step.verdict === 'ok') logInfo(`${step.step}, exit ${step.exit}`)
568
+ else
569
+ logError(`${result.name}: ${step.step} ${step.verdict}: ${step.detail}`)
570
+ // A sync over an old layout prints a line per rule it moves, so the
571
+ // frame shows the first few and the record carries every one.
572
+ if (json) continue
573
+ for (const finding of step.findings.slice(0, REPLAY_FINDINGS_SHOWN))
574
+ logWarn(`${step.step}: ${finding}`)
575
+ const hidden = step.findings.length - REPLAY_FINDINGS_SHOWN
576
+ if (hidden > 0)
577
+ logWarn(`${step.step}: ${hidden} more, listed under --json`)
578
+ }
579
+ }
580
+
581
+ const failed = replays
582
+ .flatMap((r) => r.steps)
583
+ .filter((s) => s.verdict !== 'ok')
584
+
585
+ if (record) {
586
+ recordReplays(sources, replays, failed.length, json)
587
+ return
588
+ }
589
+
590
+ logStep('Replay')
591
+ const summary = `${replays.length} replayed, ${failed.length} failed steps`
592
+ if (failed.length === 0) logInfo(summary)
593
+ else logWarn(summary)
594
+
595
+ if (json)
596
+ process.stdout.write(
597
+ `${JSON.stringify({ ok: true, failed: failed.length, replays })}\n`,
598
+ )
599
+
600
+ outro()
601
+ process.exitCode = failed.length === 0 ? 0 : 2
602
+ }
603
+
604
+ /**
605
+ * Writes each snapshot's expectation from its run, or refuses and writes none
606
+ * when any step broke, since pinning a crash would make it the expected answer.
607
+ */
608
+ function recordReplays(
609
+ sources: readonly ReplaySource[],
610
+ replays: readonly ReplayResult[],
611
+ broken: number,
612
+ json: boolean,
613
+ ): void {
614
+ if (broken > 0) {
615
+ refuseReplay(
616
+ 'broken-run',
617
+ `${broken} steps broke, so nothing was recorded. Fix the run, then record it.`,
618
+ json,
619
+ )
620
+ return
621
+ }
622
+
623
+ logStep('Record')
624
+ const recorded: string[] = []
625
+ for (const [i, source] of sources.entries()) {
626
+ const replay = replays[i]
627
+ if (replay === undefined) continue
628
+ writeExpectation(source.path, expectationOf(replay.steps))
629
+ const path = relative(PROJECT_ROOT, join(source.path, EXPECTED_FILE))
630
+ recorded.push(path)
631
+ logInfo(path)
632
+ }
633
+
634
+ if (json)
635
+ process.stdout.write(`${JSON.stringify({ ok: true, recorded, replays })}\n`)
636
+
637
+ outro()
638
+ process.exitCode = 0
639
+ }
640
+
437
641
  function runCheck(
438
642
  target: string,
439
643
  arm: string | undefined,
@@ -683,4 +887,45 @@ export function register(program: Command): void {
683
887
  .action((targets: string[], options: EquivalenceOptions) =>
684
888
  runEquivalence(targets, options),
685
889
  )
890
+
891
+ sandbox
892
+ .command('replay')
893
+ .description(
894
+ 'Sync a copy of each consumer with this checkout and judge the reads that follow',
895
+ )
896
+ .argument(
897
+ '[paths...]',
898
+ 'Consumer checkouts to copy, defaulting to every snapshot under sandbox/fixtures/replay/',
899
+ )
900
+ .helpOption('-h, --help', 'Show this help message')
901
+ .option('--json', 'Emit the record as JSON on stdout')
902
+ .option(
903
+ '--record',
904
+ `Write each snapshot's ${EXPECTED_FILE} from this run instead of judging against it`,
905
+ )
906
+ .addHelpText(
907
+ 'after',
908
+ [
909
+ '',
910
+ 'Examples:',
911
+ ' canon sandbox replay',
912
+ ' canon sandbox replay --record',
913
+ ' canon sandbox replay ../frameboard ../jobtriage --json',
914
+ '',
915
+ 'Each replay copies the source to a temp tree outside the repository,',
916
+ 'runs sync, then sync --check, context audit, markdown audit, and',
917
+ 'migrate status, and checks what sync wrote with oxfmt. A finding or a',
918
+ 'refusal from a read is the verb working. Only a crash, stdout a --json',
919
+ 'caller cannot parse, or an unformatted write fails the run.',
920
+ 'A snapshot also fails a step whose exit or finding count drifted from',
921
+ `the ${EXPECTED_FILE} beside it, and fails every step when it has none.`,
922
+ '--record rewrites those files, refusing named paths and a broken run.',
923
+ 'A named checkout with uncommitted changes is refused, never synced over.',
924
+ '',
925
+ 'Exit codes: 0 clean or recorded, 1 refused, 2 when any step failed.',
926
+ ].join('\n'),
927
+ )
928
+ .action((paths: string[], options: ReplayOptions) => {
929
+ runReplay(paths, options)
930
+ })
686
931
  }
@@ -519,9 +519,12 @@ export const documentCeiling: Measure = async (ctx) => {
519
519
  *
520
520
  * `canon/context/context-model.md` is exempt, since "context model" is the
521
521
  * domain's own name and the entry reads as nothing else without the prefix.
522
+ * So is a replay snapshot, which copies a consumer's paths verbatim, such as
523
+ * `canon/config/config.json`, and a renamed copy would sync as another file.
522
524
  */
523
525
  export function isFolderEchoedName(path: string): boolean {
524
526
  if (path === 'canon/context/context-model.md') return false
527
+ if (path.startsWith('sandbox/fixtures/replay/')) return false
525
528
  const segments = path.split('/')
526
529
  if (segments.length < 2) return false
527
530
  const folder = segments[segments.length - 2] ?? ''
@@ -304,6 +304,31 @@ export const STAGES: readonly Stage[] = [
304
304
  label: 'Unreferenced rules',
305
305
  checks: [{ kind: 'measure', measure: unreferencedRules }],
306
306
  },
307
+ {
308
+ // A defect that only shows after a sync, such as a read crashing on a
309
+ // rule layout the sync just renamed, passes every unit test and every
310
+ // stage that reads this repository. Replaying the frozen consumer
311
+ // snapshots through this checkout's sync is what reaches it before a
312
+ // consumer does. Scoped to what a sync ships or reads, since a replay
313
+ // costs seconds a docs-only push should not pay. Governance is whole
314
+ // but for the ledger, since sync resolves stacks and renames as well as
315
+ // rules, and a rename is the class the stage was built against.
316
+ id: 'consumer-replay',
317
+ label: 'Consumer replay',
318
+ scope:
319
+ /^src\/|^governance\/(?!ledger\/)|^tooling\/|^sandbox\/fixtures\/replay\//,
320
+ skipped:
321
+ 'No synced surface or snapshot changed, so no consumer was replayed',
322
+ checks: [
323
+ {
324
+ kind: 'cli',
325
+ argv: ['sandbox', 'replay'],
326
+ failure:
327
+ 'A consumer replay crashed, printed unparsable JSON, left an unformatted write, or drifted from its recorded answer. Run bun src/cli.ts sandbox replay, and when a drift is one the change meant, such as a governance rename moving a sync count, run it with --record and commit the expectations.',
328
+ },
329
+ ],
330
+ success: 'Consumer replays clean',
331
+ },
307
332
  {
308
333
  // The facts the audit can answer from the files gate: a cited path that
309
334
  // does not resolve, a required section an entry does not declare, an H2