@dialecte/core 0.9.0 → 0.10.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 (33) hide show
  1. package/dist/{dialecte-oAXwOG-Q.js → dialecte-CVLWSkgW.js} +1631 -1557
  2. package/dist/document/query/get/record/get-record.d.ts +14 -1
  3. package/dist/document/query/get/record/get-record.d.ts.map +1 -1
  4. package/dist/document/query/get/record/get-records.d.ts +12 -2
  5. package/dist/document/query/get/record/get-records.d.ts.map +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/sqlite-engine.js +14 -10
  8. package/dist/store/sqlite/runtime/close-engine.d.ts +5 -2
  9. package/dist/store/sqlite/runtime/close-engine.d.ts.map +1 -1
  10. package/dist/store/sqlite/runtime/install-pool.d.ts.map +1 -1
  11. package/dist/store/sqlite/runtime/runtime.types.d.ts +3 -2
  12. package/dist/store/sqlite/runtime/runtime.types.d.ts.map +1 -1
  13. package/dist/test/assert-xml/assert-xml.d.ts +4 -2
  14. package/dist/test/assert-xml/assert-xml.d.ts.map +1 -1
  15. package/dist/test/assert-xml/index.d.ts +1 -0
  16. package/dist/test/assert-xml/index.d.ts.map +1 -1
  17. package/dist/test/assert-xml/xml-assertion-error.d.ts +8 -0
  18. package/dist/test/assert-xml/xml-assertion-error.d.ts.map +1 -0
  19. package/dist/test/indexeddb-storage.setup.d.ts +1 -0
  20. package/dist/test/indexeddb-storage.setup.d.ts.map +1 -0
  21. package/dist/test.js +154 -148
  22. package/package.json +40 -4
  23. package/skills/dialecte/SKILL.md +8 -6
  24. package/skills/dialecte/references/api.json +5 -5
  25. package/skills/dialecte/references/api.md +2 -2
  26. package/skills/dialecte/references/testing.md +167 -11
  27. package/skills/dialecte-contributing/SKILL.md +1 -1
  28. package/skills/dialecte-contributing/references/code-rules.md +7 -0
  29. package/skills/dialecte-contributing/references/landing-changes.md +4 -4
  30. package/skills/dialecte-lot/SKILL.md +67 -28
  31. package/skills/dialecte-lot/references/briefs.md +30 -6
  32. package/skills/dialecte-lot/references/standard-check.md +2 -2
  33. package/skills/dialecte-lot/references/stations.md +5 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dialecte/core",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Dialecte - Core functionality for Dialecte SDK",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,9 +52,11 @@
52
52
  "doc:preview": "vitepress preview",
53
53
  "//====== TEST ======//": "",
54
54
  "test": "npm run test:node && npm run test:browser",
55
- "test:node": "vitest run --project node --project node-indexeddb --project node-sqlite",
55
+ "test:node": "vitest run --project node --project node-indexeddb --project node-sqlite --project node-indexeddb-documents",
56
+ "test:scope": "tsx scripts/test-scope/run-test-scope.ts",
56
57
  "test:browser": "vitest run --project browser",
57
58
  "test:browser:all": "vitest run --project browser-all",
59
+ "test:browser:release": "STORE_CONTRACT_FULL_TABLE=1 vitest run --project browser-all",
58
60
  "test:perf": "VITE_BENCH=1 vitest run --project perf --mode bench --reporter=verbose",
59
61
  "test:perf:node": "VITE_BENCH=1 vitest run --project perf-node --mode bench --reporter=verbose",
60
62
  "test:hook": "dialecte gate --hook",
@@ -86,7 +88,7 @@
86
88
  "sax": "^1.4.3"
87
89
  },
88
90
  "devDependencies": {
89
- "@dialecte/cli": "0.3.1",
91
+ "@dialecte/cli": "0.4.0",
90
92
  "@tsconfig/node24": "^24.0.4",
91
93
  "@types/node": "^24.12.4",
92
94
  "@typescript/native": "npm:typescript@^7.0.2",
@@ -158,8 +160,42 @@
158
160
  },
159
161
  {
160
162
  "name": "browser tests",
161
- "run": "npx vitest run --project browser",
163
+ "run": "npm run test:scope",
164
+ "counts": "tests",
165
+ "scoped": true
166
+ }
167
+ ],
168
+ "release": [
169
+ {
170
+ "name": "check",
171
+ "run": "npm run check"
172
+ },
173
+ {
174
+ "name": "skills",
175
+ "run": "npm run skills:check"
176
+ },
177
+ {
178
+ "name": "build",
179
+ "run": "npm run build"
180
+ },
181
+ {
182
+ "name": "tests",
183
+ "run": "npm run test:node",
162
184
  "counts": "tests"
185
+ },
186
+ {
187
+ "name": "browser tests",
188
+ "run": "npm run test:browser:release",
189
+ "counts": "tests",
190
+ "heavy": true
191
+ },
192
+ {
193
+ "name": "type narrowing",
194
+ "run": "npm run type-narrowing"
195
+ },
196
+ {
197
+ "name": "type bench",
198
+ "run": "npm run type-bench:check"
163
199
  }
164
200
  ]
165
201
  }
@@ -21,7 +21,7 @@ uuid/name fix-up that a hook already performs on every write. This skill exists
21
21
 
22
22
  ```
23
23
  your app custom extensions, services, UI (yours)
24
- dialecte package @dialecte/<id>, @acme/widget (domain)
24
+ a dialecte package @dialecte/<id>, @acme/widget (domain)
25
25
  ├ definition/*.generated.ts generated from the XSD - never edited by hand
26
26
  ├ config/ wires the definition into core, `namespace <Name>` types
27
27
  ├ hooks/ automatic coherence on every write and on import
@@ -237,8 +237,9 @@ doc.query.myFeature.findOrphanParts({ assembly }) // first param is bound away
237
237
  (no service, store, composable, component, policy file) and carries none of your product's rules.
238
238
  Where your product decides differently from the plain behaviour, see policies below.
239
239
  - **Tests on the node tier**: table-driven with the package's runner (`<package>/<version>/test` →
240
- `run<Name>TestCases`), inline XML fixtures, the function called directly with `source.query` / `tx`,
241
- not through the bound method. No test names a store: the runner runs them in Node on the in-memory
240
+ `run<Name>TestCases`), whole-document XML fixtures, one call of the unit per test (the function
241
+ with `source.query` / `tx`, or its bound method; never both), see references/testing.md. No test
242
+ names a store: the runner runs them in Node on the in-memory
242
243
  store. See [Testing](#testing).
243
244
 
244
245
  ### Policies: your product's choices, passed per call
@@ -289,9 +290,10 @@ composable then imports them with `import type`): do not wrap a type only to re-
289
290
  Each dialecte package exports table-driven runners from its `/test` entry
290
291
  (`<package>/<version>/test` → `run<Name>TestCases`; core's own: `@dialecte/core/test`). Details
291
292
  and the case format: [references/testing.md](references/testing.md). Short version: one table per
292
- behaviour, inline XML fixtures with `dev:db-id` ids that say intent, `.withExport` asserts the
293
- resulting XML with XPath, `.withoutExport` asserts inside `act`, `.generic` for pure functions.
294
- Red first, one case at a time. No test names its store: the runner chooses it (in Node on the
293
+ behaviour, whole-document XML fixtures (inline up to 40 lines, else a sibling `?raw` file) with
294
+ `dev:db-id` ids that say intent, one call of the unit per `act`, `.withExport` asserts the resulting
295
+ XML with hand-written XPath, `.withoutExport` asserts inside `act`, `.generic` for pure functions.
296
+ Refusals assert the error key and nothing staged, never a spy. Red first, one case at a time. No test names its store: the runner chooses it (in Node on the
295
297
  in-memory store under a node vitest project whose setup file calls `useTestStorage`, core 0.8+).
296
298
  Assertions take XML text; on a big file, check record expectations (tag, attributes, parent,
297
299
  count) with the query API instead of XPath on the whole export.
@@ -681,12 +681,12 @@
681
681
  "entry": "@dialecte/core/test",
682
682
  "otherRoutes": [],
683
683
  "signature": "(params: XmlQueriesWithNamespacesParams) => void",
684
- "summary": "Asserts that each XPath query selects at least one node of the XML text, parsed once for all of them. A failure names the steps that matched, the step that failed, where the last match sits, what the failing step finds there without its predicates, and the XML around it.",
684
+ "summary": "Asserts that each XPath query selects at least one node of the XML text, parsed once for all of them. A failure names the steps that matched, the step that failed, where the last match sits, what the failing step finds there without its predicates, and the XML around it. A query that selects nothing throws an `XmlAssertionError`.",
685
685
  "intents": [
686
686
  "assert that XML holds the elements XPath queries select",
687
687
  "check that an export holds an element"
688
688
  ],
689
- "declaredAt": "src/test/assert-xml/assert-xml.ts:47"
689
+ "declaredAt": "src/test/assert-xml/assert-xml.ts:49"
690
690
  },
691
691
  {
692
692
  "path": "@dialecte/core/test.assertUnexpectedElementQueries",
@@ -694,12 +694,12 @@
694
694
  "entry": "@dialecte/core/test",
695
695
  "otherRoutes": [],
696
696
  "signature": "(params: XmlQueriesWithNamespacesParams) => void",
697
- "summary": "Asserts that no XPath query selects a node of the XML text, parsed once for all of them.",
697
+ "summary": "Asserts that no XPath query selects a node of the XML text, parsed once for all of them. A query that selects a node throws an `XmlAssertionError`.",
698
698
  "intents": [
699
699
  "assert that XML does not hold an element",
700
700
  "check that an element was removed from an export"
701
701
  ],
702
- "declaredAt": "src/test/assert-xml/assert-xml.ts:61"
702
+ "declaredAt": "src/test/assert-xml/assert-xml.ts:64"
703
703
  },
704
704
  {
705
705
  "path": "@dialecte/core/test.createMockRandomUUID",
@@ -775,7 +775,7 @@
775
775
  "make XPath assertions bound to a dialecte's namespaces",
776
776
  "assert exported XML in tests"
777
777
  ],
778
- "declaredAt": "src/test/assert-xml/assert-xml.ts:26"
778
+ "declaredAt": "src/test/assert-xml/assert-xml.ts:27"
779
779
  },
780
780
  {
781
781
  "path": "@dialecte/core/test.createXmlSchemaAssertions",
@@ -481,8 +481,8 @@ ruled out and why in your hand-over.
481
481
 
482
482
  | Function | What it answers | Reach for it to |
483
483
  | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
484
- | `assertExpectedElementQueries` | Asserts that each XPath query selects at least one node of the XML text, parsed once for all of them. A failure names the steps that matched, the step that failed, where the last match sits, what the failing step finds there without its predicates, and the XML around it. | assert that XML holds the elements XPath queries select; check that an export holds an element |
485
- | `assertUnexpectedElementQueries` | Asserts that no XPath query selects a node of the XML text, parsed once for all of them. | assert that XML does not hold an element; check that an element was removed from an export |
484
+ | `assertExpectedElementQueries` | Asserts that each XPath query selects at least one node of the XML text, parsed once for all of them. A failure names the steps that matched, the step that failed, where the last match sits, what the failing step finds there without its predicates, and the XML around it. A query that selects nothing throws an `XmlAssertionError`. | assert that XML holds the elements XPath queries select; check that an export holds an element |
485
+ | `assertUnexpectedElementQueries` | Asserts that no XPath query selects a node of the XML text, parsed once for all of them. A query that selects a node throws an `XmlAssertionError`. | assert that XML does not hold an element; check that an element was removed from an export |
486
486
  | `createMockRandomUUID` | The `crypto.randomUUID` a test case's act runs with: UUID-shaped ids that follow from a seed (0 by default), the same sequence on every run. | get deterministic uuids in a test; mock crypto.randomUUID |
487
487
  | `createTestHelpers` | Every test helper a dialecte needs, derived from its declaration: the runner, the test project, the record factory, the XPath assertions and the schema assertions. A fixture is checked against the schema of the config it is imported under, then by the dialecte's extra checks, before a case or a test project uses it. | get every test helper of a dialecte; set up a dialecte package's tests; check fixtures against the schema |
488
488
  | `createTestProject` | Spin up a Project of the declared dialecte - every config, the default key, its extensions and its own hooks instance; core's own test dialecte when none is given - with the source (and optionally the target) imported under the default config. Returns project + pre-opened documents. Opened inside a running test, the project is destroyed when the test ends (after its other `onTestFinished` hooks); opened outside one (`beforeAll`), the caller destroys it. Spin up a Project from a config, extensions and hooks given one by one, with the source (and optionally the target) imported under the key `default`. | open a project in a test with a fixture imported; get documents to test against |
@@ -58,24 +58,180 @@ A dialecte package's runner has the same shape with its own types (`<Name>Test.T
58
58
 
59
59
  ## Rules
60
60
 
61
+ A test is read far more often than it is written: by the reviewer, by the next person who changes the code,
62
+ by whoever reads a failure. Every rule below keeps one case readable on its own and keeps each behaviour proven
63
+ in exactly one place.
64
+
65
+ ### The table
66
+
61
67
  - **Red first, one case at a time.** Add one case with `only: true`, run it, see it fail for the
62
68
  right reason (an assertion, not a missing module - stub the function with the right type first),
63
- write the minimum code, then the next case. Remove every `only` at the end. A table written after
64
- the implementation only pins what the code happens to do.
65
- - **Case keys** read `<initial state> → <outcome>`.
66
- - **Fixtures are inline XML** in the test file. `dev:db-id` (`CUSTOM_RECORD_ID_ATTRIBUTE`) gives
67
- records stable ids; choose ids that say intent (`orphaned-ref`, `a-without-child`). Comment the
68
- elements that are deliberately absent. No `<?xml ?>` declaration in template literals.
69
- - **Expectations are structural XPath with meaningful predicates.** `//default:AA_1` alone proves
70
- nothing; a case can pass because ANOTHER element matched. Do not assert on `dev:db-id` in XPath.
71
- Pair `unexpectedQueries` with the commented-out source when asserting a deletion.
69
+ then the next case. Remove every `only` before the red is recorded (`dialecte tdd red` refuses a
70
+ focused file), then write the minimum code. Record per case where it failed (`<case title>: failed on
71
+ <assertion>`); a case that never failed is listed with its reason (existing behaviour the change
72
+ touches). A table written after the implementation only pins what the code happens to do.
73
+ - **One table per behaviour.** `.withExport` with `expectedQueries` when the outcome is XML;
74
+ `.withoutExport` only for a returned value or warnings; a refusal's table runs `.withExport` (see
75
+ Refusals below). A case field that picks which assertions run (`check: 'children' | 'references'`,
76
+ `if (testCase.setup === 'x')`) means two behaviours share a table: split it. A flag marking the
77
+ refusal cases of a table is fine.
78
+
79
+ ```ts
80
+ // BAD: one table, the case picks what the act checks
81
+ if (testCase.check === 'children') expect(children).toHaveLength(1)
82
+ else expect(references).toEqual([])
83
+
84
+ // GOOD: one table per behaviour, the same act for every case of a table
85
+ runTestCases.withExport({ testCases: addsChildCases, act: addsChildAct })
86
+ runTestCases.withExport({ testCases: keepsReferencesCases, act: keepsReferencesAct })
87
+ ```
88
+
89
+ - **Case keys** read `<initial state> → <outcome>`. No plan, round, station or ticket tags in titles
90
+ or file names (`round 2`, `step 3`, an issue number).
91
+ - **Only the behaviour the change touches.** No preservation cases for code the change leaves alone.
92
+ Each case is written once, for the Node tier; no browser copy of a case. The package's whole-suite
93
+ runs on other tiers or stores are its gate, not duplicates.
94
+ - **Readable size.** Aim for 200-300 lines per test file, one verb or behaviour per file. A file
95
+ growing past that holds two behaviours: split it by behaviour, not by line count.
96
+
97
+ ### One call of the unit
98
+
99
+ A verb's test proves that verb's behaviour, nothing else. The `act` makes **one call of the unit under
100
+ test** - the function itself (`fn(tx, params)`, `fn(source.query, params)`) or its bound method
101
+ `tx.<ns>.<verb>`; either is one call, never both in one test - then returns or asserts the outcome.
102
+
103
+ - No second or third call, no retry, no loop over the call.
104
+ - No capture before the call: no snapshot of the staged operations, no copy of the document's metadata,
105
+ to compare after. The outcome is what the call left: `expectedQueries`, a returned value, a staged
106
+ list compared with a literal. "Unchanged" is a case of its own only where it is the behaviour.
107
+ - No XPath on the source inside the `act`, no branch on the case.
108
+ - Not in a verb's test: the transaction's rollback, source-document purity, id or date formats, timestamps,
109
+ provenance details. Their owners test them (core's transaction, the writer of the timestamp, schema
110
+ validation, one acceptance case for source purity); a verb re-proving them breaks when the owner
111
+ changes, not when the verb does.
112
+ - A **reshape** keeps every behaviour covered **by the test that owns it**. List each check it removes
113
+ with the test that covers it, and open that test: it must really assert it, or the reshape adds the case
114
+ there first.
115
+
116
+ ```ts
117
+ // BAD: a state captured before the call, compared after it
118
+ const before = structuredClone(await source.query.getDocumentInfo())
119
+ await source.transaction(async (tx) => addFirstChild(tx, testCase.params))
120
+ expect(await source.query.getDocumentInfo()).not.toEqual(before)
121
+
122
+ // GOOD: one call; the case's expectedQueries carry the outcome
123
+ async function act({
124
+ testCase,
125
+ source,
126
+ }: ActParams<TestDialecteConfig, TestCase>): Promise<ActResult> {
127
+ await source.transaction(async (tx) => addFirstChild(tx, testCase.params))
128
+ return { assertOn: 'source' }
129
+ }
130
+ ```
131
+
132
+ ### Refusals assert outcomes, never spies
133
+
134
+ A refusal is proven by what it leaves: the error key, nothing staged, the documents unchanged.
135
+
136
+ - Catch the refusal inside its transaction, assert its key, then `tx.getStagedOperations()` equal to
137
+ `[]`, still inside that transaction.
138
+ - Run the table `.withExport` so the source's `expectedQueries` show the document unchanged.
139
+ - No `vi.spyOn` on a transaction or query method: a spy tests how the code writes, and passes while the
140
+ document is wrong.
141
+
142
+ ```ts
143
+ // BAD: a spy on how it writes
144
+ const addChild = vi.spyOn(tx, 'addChild')
145
+ await expect(addFirstChild(tx, params)).rejects.toThrow()
146
+ expect(addChild).not.toHaveBeenCalled()
147
+
148
+ // GOOD: the key, nothing staged, the document checked by expectedQueries
149
+ await source.transaction(async (tx) => {
150
+ await expect(addFirstChild(tx, testCase.params)).rejects.toMatchObject({
151
+ cause: { key: 'ELEMENT_NOT_FOUND' },
152
+ })
153
+ expect(tx.getStagedOperations()).toEqual([])
154
+ })
155
+ return { assertOn: 'source' }
156
+ ```
157
+
158
+ ### Real records, no fakes
159
+
160
+ - Test through the public interfaces: no mocks of internal collaborators, no hand-written fake `Query`,
161
+ no mock of a query module.
162
+ - A step that reads real records gets one real-engine case asserting the values it read, before any
163
+ mock is considered.
164
+
165
+ ### Fixtures are whole documents
166
+
167
+ Each case's document is written out in full, so a reader sees the exact input without assembling it in
168
+ their head, and holds only the elements the case needs.
169
+
170
+ - **In the test file** (inline in the case, or one file-level constant used whole) when it is at most
171
+ **40 lines**. Above, a sibling file
172
+ `fixtures/<what-it-holds>.xml` read with a static `?raw` import. A fixture file is at most **120 lines**
173
+ and its name says what it holds (`a-with-two-aa-1.xml`, not `fixture2.xml`).
174
+ - **A variant is its own document**, never a copy edited at runtime: no `.replace()` on a fixture, no
175
+ shared XML constant spliced into a case's document (`${CHILDREN}`, `${HEADER}`), no fixture builder
176
+ functions, no `*.test-fixtures.ts` module. Substitutions inside a tag are fine: names (`${ns}`),
177
+ attribute values (`aA="${UUID}"`), namespace declarations. A shared empty target is fine.
178
+ - A case reading a fixture file carries **one comment** naming what matters in that document
179
+ (`// A "one" holds no AA_1; A "two" holds one without bAA_1`).
180
+ - `dev:db-id` (`CUSTOM_RECORD_ID_ATTRIBUTE`) gives records stable ids; choose ids that say intent
181
+ (`orphaned-ref`, `a-without-child`). Comment the elements that are deliberately absent. No
182
+ `<?xml ?>` declaration in template literals.
183
+
184
+ ```ts
185
+ // BAD: assembled from pieces
186
+ sourceXml: `<Root ${XMLNS_DEFAULT_NAMESPACE}>${A_WITH_CHILDREN.replace('aA="one"', 'aA="two"')}</Root>`,
187
+
188
+ // GOOD: the whole document, inline or as its own file
189
+ import aWithTwoAa1 from './fixtures/a-with-two-aa-1.xml?raw'
190
+ // A "one" holds two AA_1; the second has no bAA_1
191
+ sourceXml: aWithTwoAa1,
192
+ ```
193
+
194
+ **Making a variant: a throwaway script, never calls in the test.**
195
+
196
+ 1. Start from `dialecte fixture <Tag>` (the smallest valid document holding the tag) or an existing whole
197
+ fixture.
198
+ 2. Write the variant with a throwaway script:
199
+ `dialecte run make-<what>.ts --in <base>.xml --out fixtures/<what-it-holds>.xml --seed <n> --now <iso>`
200
+ (seed and clock fixed, so the same command gives the same file). The script may use any tested
201
+ feature, the verb being changed included: its current output is a valid starting document.
202
+ 3. Check the result: valid against the schema, and `--expect <file>` (a JSON array of
203
+ `{ tag, where?, under?, count? }`) for what the case needs the document to hold.
204
+ 4. Commit only the `.xml`. The script is never committed, and never thrown away before the review
205
+ either: a fixture whose making cannot be rerun cannot be reviewed. Keep it with the change's
206
+ hand-over notes (the folder handed to the reviewer) as `fixture-scripts/make-<what>.ts`, listed in
207
+ `fixture-scripts.md` (`| fixture | script | command |`), and delete it with the notes once the change
208
+ lands. A document written whole by hand needs no script; say so on its line.
209
+
210
+ ### Expectations are written by hand
211
+
212
+ - **Expectations are structural XPath with meaningful predicates**, written in the test file from the
213
+ requirement. `//default:AA_1` alone proves nothing; a case can pass because ANOTHER element matched.
214
+ Do not assert on `dev:db-id` in XPath. Pair `unexpectedQueries` with the commented-out source when
215
+ asserting a deletion.
216
+ - **Never generated.** For a document outcome, a document produced by the code under test is never
217
+ the expected result: comparing an export with a saved `.xml` is a snapshot in disguise, it passes on
218
+ whatever the code did the day it was saved.
219
+
220
+ ```ts
221
+ // BAD: the code's own output as the truth
222
+ expect(exported).toBe(expectedAfterAddXml)
223
+
224
+ // GOOD: what the requirement says, as queries
225
+ expectedQueries: ['//default:A[@aA="one"]/default:AA_1[@aAA_1="added"]'],
226
+ ```
227
+
228
+ ### The runner and the assertions
229
+
72
230
  - **Fixtures are validated against the schema before a case runs** by the runners `createTestHelpers`
73
231
  builds - every dialecte package's `run<Name>TestCases` / `create<Name>TestProject` (a dialecte may
74
232
  add its own checks, see its skill). Opt out of those extra checks only with a written reason
75
233
  (`skipExtraChecks: '<why>'` on `createTestProject`). Core's own `runTestCases` on the dummy
76
234
  dialecte (`createTestRunner`) validates nothing.
77
- - **Test the function directly** (`fn(source.query, params)` / `fn(tx, params)`), not through the
78
- bound extension method. Use public interfaces; no mocks of internal collaborators.
79
235
  - **No test names its store: the runner chooses it.** A node vitest project whose setup file calls
80
236
  `useTestStorage({ type: 'inMemory' })` (core 0.8+) runs your tests in Node on the in-memory store;
81
237
  without one, the runner's default store. A second project whose setup imports
@@ -121,7 +121,7 @@ what the station records for the hand-over card).
121
121
  5. **Independent review**: have a fresh reviewer (a sub-agent with only the spec, the diff and
122
122
  these rules, or a human) read the diff against the previous version before hand-over. Self-review
123
123
  re-reads your own intent and misses weakened assertions and unusable types.
124
- 6. **Hand over** through `npx dialecte handover <lots.json>`: the agent writes only the lots file, the
124
+ 6. **Hand over** through `npx dialecte handover write <lots.json>`: the agent writes only the lots file, the
125
125
  command writes the hand-over folder and prints the card the user gets
126
126
  ([references/landing-changes.md](references/landing-changes.md)). The agent never commits or pushes.
127
127
 
@@ -12,6 +12,7 @@ does not name.
12
12
  - Files and types
13
13
  - Naming
14
14
  - Comments and committed text
15
+ - Tests
15
16
  - Self-check before hand-over
16
17
 
17
18
  ## Reuse and sources of truth
@@ -87,6 +88,12 @@ does not name.
87
88
  - Do not create documentation that was not asked for, beyond the docs + CHANGELOG the Definition
88
89
  of Done requires.
89
90
 
91
+ ## Tests
92
+
93
+ Tests follow the rules of the `dialecte` skill's references/testing.md (section Rules): the one source for
94
+ how a table, a case, a fixture and an expectation are written. Read it before writing a case; this page
95
+ does not repeat it.
96
+
90
97
  ## Self-check before hand-over
91
98
 
92
99
  ```bash
@@ -8,7 +8,7 @@ script may be waiting or running in the human's terminal.
8
8
 
9
9
  ## The hand-over
10
10
 
11
- The agent writes only a lots file; `npx dialecte handover <lots.json>` does the rest. One lots file per lot
11
+ The agent writes only a lots file; `npx dialecte handover write <lots.json>` does the rest. One lots file per lot
12
12
  group, in an uncommitted hand-over folder (for example `_commit-plan/<topic>-<yyyy-mm-dd>/lots.json` at the
13
13
  workspace root, outside every repo); the hand-over files are written next to it (`--out <dir>` elsewhere):
14
14
 
@@ -64,13 +64,13 @@ workspace root, outside every repo); the hand-over files are written next to it
64
64
  and is for when the person asks. A heavy hook that does run is rehearsed while another suite loads the
65
65
  machine, as it will be when the user commits; when that is not possible, the hand-over notes the machine's
66
66
  load at rehearsal time (`uptime`). The real commit's hook always runs.
67
- - Edited a file after the snapshot (a review fix)? `npx dialecte handover --rehash <lots.json>`.
67
+ - Edited a file after the snapshot (a review fix)? `npx dialecte handover write <lots.json> --rehash` (a cli from 0.2.0 to 0.3: `npx dialecte handover <lots.json> --rehash`).
68
68
  - `npx dialecte handover list` shows every hand-over folder with its state (pending, partly committed,
69
69
  committed, stale); after a merge, `npx dialecte worktree prune` lists the committed ones with the merged
70
70
  worktrees and prints their removal commands: an agent hands them (or the `--yes` command, which asks on
71
71
  a terminal) to the user with their `cd`.
72
- - `handover` comes with `@dialecte/cli` 0.2.0 or later; with an older cli in the package, run
73
- `npx @dialecte/cli@^0.2.0 handover <lots.json>` (never `@latest`, which can be older).
72
+ - `handover write` comes with `@dialecte/cli` 0.4.0 or later; a cli from 0.2.0 to 0.3 takes
73
+ `npx dialecte handover <lots.json>` (the same command 0.4.0 keeps as an alias).
74
74
 
75
75
  ## Commit messages
76
76
 
@@ -52,7 +52,7 @@ follows the hand-over, and the branch goes up by pull request through that repos
52
52
  (`dialecte-contributing`). In an app that consumes a dialecte, the lot ends at
53
53
  Station 6: what comes after belongs to the app's own process - its PR skill and template for the pull
54
54
  request, its own retro and clean-up if it has them - and no further station of this skill runs (no Station 7,
55
- no `dialecte worktree prune`, no dialecte PR or release step). The hand-over's records (`dialecte recap`
55
+ no `dialecte worktree prune`, no dialecte PR or release step). The hand-over's records (`dialecte lot recap <folder>`
56
56
  prints a PR's details part from them) feed the app's template only where its rules ask for it. Pushing and
57
57
  posting stay the user's (or wait for their approval).
58
58
 
@@ -71,9 +71,27 @@ wrong.
71
71
 
72
72
  Before the test station, the plan holds what the tests will pin; a gap found by a reviewer costs a round.
73
73
 
74
+ - **Discovery first**, before any test or code: list what already covers the concept with the whole cli
75
+ surface - `dialecte api find` and the API index, `explain`, `schema`, `fixture`, `standard find` / `show`, the
76
+ package skills - then a code search for what they do not index. The list goes into the lot's `decisions.md`,
77
+ each hit reused or with why not; a shared read found wrong is fixed in place, never forked. Reviewers check the
78
+ diff against that list. The list records evidence: it never limits what a station may use.
79
+ - **An automatic destructive action** (a command or hook that deletes, moves or resets without the person
80
+ running it): the brief opens with its invariant - "never <act> on <thing> unless it is proven <X>, <Y>, <Z>" -
81
+ and a list of adversarial cases written before the tests: for checkouts, other lots, reused or switched
82
+ worktrees and branches, locks, live processes, links; for content, what the target touches, references to
83
+ it, a target emptied and refilled; for both, concurrent runs and an interrupted step. Where proving costs more than
84
+ asking, print the command for the person instead of acting. Keep rules alone let every round find one more
85
+ state.
86
+ - **A step that removes content** (a prune, a redaction, a dedupe): a preservation check per unit (file, page,
87
+ element) proves only the targeted content went, loss counted per unit and never by a summary that hides a
88
+ one-off, and a boundary case (the target touching what must stay) is written before the code.
89
+ - **A rename across many cases** (an output prefix, a message: behaviour, so never an exempt lot): the
90
+ test-station brief states the mapping as a rule (`<call>` → `<expected text>`) and asks for one audit pass
91
+ of every touched file against it before `tdd red`; a text replace without it swaps names in both directions.
74
92
  - **The standard** (a lot whose rules come from a standard the code implements):
75
93
  [references/standard-check.md](references/standard-check.md) - how it is read, the cross-check for a
76
- decision, and what the station records for the hand-over card (`dialecte record standard`).
94
+ decision, and what the station records for the hand-over card (`dialecte lot standard`).
77
95
  - **A tooling lot with shared state or an external tool**: the plan carries the store's transition table, its
78
96
  invariant and the interleavings, the test-station brief asks one forced-order test per interleaving, and the
79
97
  hand-over folder gets a `SMOKE.md` with the lot's one real run; how and where that run happens, and citing
@@ -131,7 +149,13 @@ test edited after red; the test code changed since its freeze; the cases the cha
131
149
  --fast` and `--full` check the receipts in their `tdd` step: every test-code file of the change as frozen,
132
150
  every new or changed case red then green (or unchanged), the flags and deleted cases listed.
133
151
 
134
- **Exempt lots.** A lot that changes no behaviour - a wording, a rename, a doc-only or a CHANGELOG-only
152
+ **Checks run per step, not at the end of the lot**: the other tests related to the touched files (`vitest
153
+ related` or `--changed <base>`), `check` (its type-check covers the tests) and, in a dialecte package, `dialecte
154
+ rules` on the touched files. The whole suite runs once, at `gate --full`. After any edit of a test (a test-review
155
+ return included, a type-only one too), the test station takes `tdd red` again before `tdd green`: a case greened
156
+ over an unproven edit ends up accepted, not proven.
157
+
158
+ **Exempt lots.** A lot that changes no behaviour - a wording, a rename nothing outputs, a doc-only or a CHANGELOG-only
135
159
  lot - has no red to prove. The orchestrator declares it in the brief and writes
136
160
  `{ "tdd": "exempt: <reason>" }` to `.dialecte/lot.json` at the package root of the worktree; the gate's
137
161
  `tdd` step passes with the reason and what the change touches ("exempt: <reason> (n production files, m
@@ -152,7 +176,8 @@ Before touching code (the implement station; the test station does the same befo
152
176
  can assume things that changed. A contradiction stops you: say it, do not reinterpret the plan.
153
177
  2. `git status -sb` in your worktree: right branch, clean (or dirty only with your own earlier work).
154
178
  Note the HEAD sha for your report.
155
- 3. Load `dialecte` (and `dialecte-contributing` in a dialecte package); run the reuse ladder before writing anything.
179
+ 3. Load `dialecte` (and `dialecte-contributing` in a dialecte package); run the reuse ladder before writing anything,
180
+ starting from the lot's discovery list in `decisions.md` (Station 0).
156
181
 
157
182
  Then, within the step:
158
183
 
@@ -261,8 +286,10 @@ copy of every file `git status --porcelain -z --untracked-files=all` lists at th
261
286
  untracked, at its path relative to the repository root, and beside it `round-<n>.deleted`, NUL-separated, naming
262
287
  each deleted file and the old path of each rename (beside the copy, never in it: it is no file of the lot). A copy,
263
288
  not a patch: nothing to apply, binary files included. The lot stays uncommitted until the hand-over, so HEAD is the
264
- base for the whole loop: these snapshots are the only refs the later rounds have. From cli 0.2.0,
265
- `dialecte record snapshot <folder> --round <n>` takes it (and `dialecte review delta` reads it); the text here is
289
+ base for the whole loop: these snapshots are the only refs the later rounds have. Every round gets one, round 1
290
+ and a verify-only round included: without it a throwaway worktree holds the only copy of that round's state,
291
+ lost when the throwaway is removed. From cli 0.4.0,
292
+ `dialecte lot snapshot <folder> --round <n>` takes it (and `dialecte lot delta` reads it); the text here is
266
293
  the manual fallback for an older cli.
267
294
 
268
295
  - **Round 1 - full review**, blind to the implementer's conclusions, every severity. The checklist, each angle
@@ -276,8 +303,8 @@ the manual fallback for an older cli.
276
303
  the findings ledger. The last round reports what its class acts on; anything below is logged as a follow-up, not
277
304
  fixed in the lot. A finding the last round acts on is fixed; the round that would verify the fix goes to the user
278
305
  (above). The floor rises so the rounds converge. The review has three parts:
279
- - (a) **Each fix verified on the old code.** From cli 0.2.0,
280
- `dialecte review delta <folder> --round <n> --worktree` makes the throwaway worktree holding `round-<n-1>/`
306
+ - (a) **Each fix verified on the old code.** From cli 0.4.0,
307
+ `dialecte lot delta <folder> --round <n> --worktree` makes the throwaway worktree holding `round-<n-1>/`
281
308
  (the fixer's, when it made one: Station 4, reused, never a second); with an older cli, the orchestrator makes one by hand on its own branch
282
309
  (never the lot's), copies the snapshot over it, removes its deleted files - the one place a `git rm` is
283
310
  allowed, never in the lot's worktree - and passes the path (`worktree prune` lists it after the lot):
@@ -313,13 +340,15 @@ the manual fallback for an older cli.
313
340
  - (c) **One line naming what round 1 did not look at.**
314
341
 
315
342
  **The findings ledger** (`findings-ledger.md` in the hand-over folder) is the lot's memory across rounds. Only the
316
- orchestrator writes it, by hand or, from cli 0.2.0, with `dialecte record review` (a new row, `open`) and
317
- `dialecte record decision` (the states `open`, `fix to verify`, `fixed <n> [note]`, `accepted`, `logged`). The
318
- cli writes neither the risk-class line nor `reopened`, and its `fix to verify` carries no blast radius: write
319
- those by hand, or keep the blast-radius lines in a fixes file beside the ledger. Reviewers propose rows. It
320
- opens with the risk class (`Risk class: <class> - <rounds> rounds, last round acts on <floor>, a further round
321
- only with the user - set by <files>`). One row per finding: id, round
322
- raised, severity, file:line, state - `open`, `fix to verify - <blast radius>` (the fixer says fixed; its
343
+ orchestrator writes it, by hand or, from cli 0.4.0, with
344
+ `dialecte lot finding <folder> --round <n> --severity <blocking|should-fix|nit> --at <file:line> <finding>`
345
+ (a new row, `open`) and
346
+ `dialecte lot decide <folder> <finding> <open | fix-to-verify | fixed <round> [note] | accepted <reason> | logged <where>>`
347
+ (the ledger row spells the state `fix to verify`). The cli writes neither the risk-class line nor `reopened`,
348
+ and its `fix-to-verify` carries no blast radius: write those by hand, or keep the blast-radius lines in a fixes
349
+ file beside the ledger. Reviewers propose rows. It opens with the risk class (`Risk class: <class> - <rounds>
350
+ rounds, last round acts on <floor>, a further round only with the user - set by <files>`). One row per finding:
351
+ id, round raised, severity, file:line, state - `open`, `fix to verify - <blast radius>` (the fixer says fixed; its
323
352
  blast-radius line comes with it), `fixed <n>` (n = the round that verified the fix; `fixed <n> (reasoned)` when the
324
353
  guard was not run), `reopened <n> by <id>` (a later fix broke it again), `accepted <reason>`, `logged <follow-up>`.
325
354
  Every reviewer reads it; a finding fixed or accepted is not raised again unless the fix is wrong. Outside every
@@ -362,8 +391,8 @@ that fixes one finding and hides another, a fix that undoes an earlier one.
362
391
 
363
392
  **The red of a fix's new test comes from the tree the round reviewed, never from production put back.** Write the
364
393
  test before the fix and take its red with `npx dialecte tdd red` while the code it pins is still the reviewed one.
365
- When production already holds the fix, run the new test where the old code still lives: `npx dialecte review delta
366
- <folder> --round <n+1> --worktree` (n = the round whose findings you fix; cli 0.2.0, else the orchestrator's
394
+ When production already holds the fix, run the new test where the old code still lives: `npx dialecte lot delta
395
+ <folder> --round <n+1> --worktree` (n = the round whose findings you fix; cli 0.4.0, else the orchestrator's
367
396
  hand-made throwaway of Station 3 (a)) makes a throwaway worktree holding `round-<n>/` and prints its removal
368
397
  command; copy the new test file in, with the test code it imports that the round changed, run it there - it must
369
398
  fail on its assertion - and in the lot's worktree, where it passes. The report says the red was taken on
@@ -381,12 +410,13 @@ it from a test-first red (it flags `red after green` only when the case already
381
410
 
382
411
  ## Station 6 - hand over
383
412
 
384
- Write only the lots file and run `npx dialecte handover
385
- <lots.json>`: it writes the hand-over folder and prints its card. Never a hand-written script, nor a file per
386
- commit. When the repository has a commit hook (`core.hooksPath`), rehearse first with `npx dialecte handover
387
- run <folder> --rehearse`: a scratch clone holding your tree, the hooks folder and `node_modules`, so the hook
388
- runs as it will for the user. Rehearse a heavy hook while another suite loads the machine, or at least note
389
- the load at rehearsal time (`uptime`) after the card: a quiet rehearsal hides a time bound that fails under load.
413
+ Write only the lots file and run
414
+ `npx dialecte handover write <lots.json>`: it writes the hand-over folder and prints its card. Never a
415
+ hand-written script, nor a file per commit. When the repository has a commit hook (`core.hooksPath`),
416
+ rehearse first with `npx dialecte handover run <folder> --rehearse`: a scratch clone holding your tree, the
417
+ hooks folder and `node_modules`, so the hook runs as it will for the user. Rehearse a heavy hook while another
418
+ suite loads the machine, or at least note the load at rehearsal time (`uptime`) after the card: a quiet
419
+ rehearsal hides a time bound that fails under load.
390
420
 
391
421
  **A check's result belongs to the tree it ran on.** From cli 0.2.0 every gate and hook result is recorded with
392
422
  what it checked - the tree, the installed lock, the linked `file:` dependencies and the command - and a run with
@@ -411,7 +441,9 @@ never a pasted or retyped card, which goes stale as soon as a record changes. Wi
411
441
  **is the card** `handover` printed - repository, worktree, branch, HEAD, gates, the lots with their files and
412
442
  messages, and the command with its absolute `cd` - pasted as printed, never retyped. After it: what was not verified, and one usage line (the lot's sub-agent tokens, in total
413
443
  and by station: [references/retro.md](references/retro.md)). A refusal is a finding: fix the file or the
414
- message, do not loosen the lots. `npx dialecte handover list` shows every hand-over waiting on the machine.
444
+ message, do not loosen the lots. Before reporting, stop every background task you started and check with `ps`
445
+ that none is left: a forgotten wait loop holds the worktree, and `lot close` keeps it, for as long as it runs.
446
+ `npx dialecte handover list` shows every hand-over waiting on the machine.
415
447
  Never commit, push, stage, or touch a repository's index while a hand-over may be waiting.
416
448
 
417
449
  ## Station 7 - retro
@@ -433,10 +465,15 @@ that is rewritten every retro never settles.
433
465
  in its worktree before anything else. Its hygiene step fails on a staged file or a stash added since the
434
466
  worktree was made, whatever wrote it. A failure is a finding: look with the read-only commands it prints
435
467
  and report it; never repair it with a git command that writes. When the agent changed nothing, the gate
436
- still runs its hygiene and `tdd` step and answers the rest from its record of that tree.
468
+ still runs its hygiene and `tdd` step and answers the rest from its record of that tree. Then check with `ps`
469
+ that no task the agent started is still running in its worktree: one left is stopped and reported.
437
470
  - Write each brief from [references/briefs.md](references/briefs.md): the brief is the agent's whole
438
471
  context; synthesise, do not say "based on the findings". Every brief carries its lines for all stations
439
- (nothing under `.git` by hand, no workflow without the user's opt-in, no `rm` on a variable path).
472
+ (nothing under `.git` by hand, no workflow without the user's opt-in, no `rm` on a variable path, files
473
+ edited with the edit tool, a rewriting script reads before it opens for writing, no ad-hoc file-writing
474
+ script with a lot's worktree as its working folder: it runs from the scratchpad or the hand-over folder; no
475
+ real person's name or handle in any file, test values included; every background task it started stopped,
476
+ checked with `ps`, before it reports).
440
477
  - Each station agent is defined once, for every host, in [references/stations.md](references/stations.md):
441
478
  its tier, its access (`read`, `review`: runs commands but edits nothing, `edit`) and its role (`test-writer`,
442
479
  `test-reviewer`, `implementer`, `implementer-strong`, `reviewer`, `fixer`, `eval-runner`, `grader`,
@@ -477,8 +514,8 @@ orchestrator launches in the background gets a **deadline** when it is launched:
477
514
  you - a scheduled check or a wake-up at the deadline, or a monitor on the run's log - so you are woken even
478
515
  if no notice comes. A host with none polls at the deadline.
479
516
  - **At the deadline, look at the run itself**: its process tree (`pgrep -P <pid>`, `ps -o pid,stat,time,command`:
480
- is its CPU time growing?), the agent's last activity or the log's last line and its time, and `dialecte runs`
481
- once the cli has it (the live runs of the machine, each with its state).
517
+ is its CPU time growing?), the agent's last activity or the log's last line and its time, and a list of the
518
+ machine's live runs, each with its state, once the cli has one.
482
519
  - **Kill on what you see, never on the clock.** No new output and no CPU time gained by the tree since the
483
520
  last look: it is stuck - tell the user what you saw (the tree, the last line, since when) and stop or
484
521
  restart it. Still gaining CPU time or writing output: set one more deadline. Past that second deadline it
@@ -521,6 +558,8 @@ hooks. What differs is how a host starts a session, spawns an agent and wakes yo
521
558
  - `rm`, `mv` or `xargs` on a variable path: leave temporary files in the hand-over folder or the scratchpad.
522
559
  - Wait on a background run with no deadline, or on its completion notice alone.
523
560
  - Hand over a hand-written commit or merge script.
561
+ - Write a real person's name or handle in any file, test values included: neutral names (`reviewer`,
562
+ `person-a`).
524
563
  - Implement two lots in one station "because they are small".
525
564
  - Reach for `any`, `@ts-ignore` or a skipped test to get green.
526
565
  - Let an output live only in the chat.