@jterrazz/test 15.3.0 → 16.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -427
- package/dist/appium.adapter.js +71 -15
- package/dist/artifacts.js +25 -1735
- package/dist/browser/index.js +304 -233
- package/dist/checker.d.ts +1 -1
- package/dist/checker.js +1103 -97
- package/dist/fixtures.js +110 -0
- package/dist/index.d.ts +956 -971
- package/dist/index.js +2159 -2281
- package/dist/literate.js +1747 -0
- package/dist/match.js +625 -84
- package/dist/oxlint.cjs +3585 -1480
- package/dist/oxlint.d.cts +60 -48
- package/dist/oxlint.d.ts +60 -48
- package/dist/oxlint.js +3585 -1480
- package/dist/playwright.adapter.js +51 -8
- package/dist/{intercept.js → server.js} +2 -2
- package/dist/stream.js +6 -6
- package/dist/{ambiguity.js → substring-warning.js} +106 -7
- package/dist/vitest.d.ts +67 -48
- package/dist/vitest.js +229 -93
- package/docs/01-architecture.md +95 -0
- package/docs/02-developing.md +351 -0
- package/docs/03-testing.md +495 -0
- package/docs/04-operating.md +101 -0
- package/docs/05-module-tests.md +152 -0
- package/docs/06-integration.md +180 -0
- package/docs/07-component.md +246 -0
- package/docs/08-website.md +404 -0
- package/docs/09-mobile.md +270 -0
- package/docs/10-api.md +264 -0
- package/docs/11-jobs.md +157 -0
- package/docs/12-cli.md +677 -0
- package/docs/13-elements.md +242 -0
- package/docs/14-assertions.md +314 -0
- package/docs/15-tokens.md +253 -0
- package/docs/16-contracts.md +438 -0
- package/docs/17-services.md +188 -0
- package/docs/18-conventions.md +305 -0
- package/docs/19-linting.md +510 -0
- package/docs/README.md +36 -0
- package/docs/decisions/001-v9-conventions-on-three-channels.md +50 -0
- package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +49 -0
- package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +84 -0
- package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +81 -0
- package/docs/decisions/005-the-doubles-ladder-is-closed.md +86 -0
- package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +245 -0
- package/docs/decisions/007-compose-mode-leaves-the-package.md +53 -0
- package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +75 -0
- package/docs/decisions/_template.md +22 -0
- package/docs/reference/classes/Matcher.md +120 -0
- package/docs/reference/classes/ProcessHandle.md +368 -0
- package/docs/reference/functions/defineContract.md +39 -0
- package/docs/reference/functions/defineContracts.md +36 -0
- package/docs/reference/functions/disabled.md +28 -0
- package/docs/reference/functions/enabled.md +20 -0
- package/docs/reference/functions/focused.md +26 -0
- package/docs/reference/functions/mockOf.md +79 -0
- package/docs/reference/functions/postgres.md +31 -0
- package/docs/reference/functions/process.md +33 -0
- package/docs/reference/functions/redis.md +28 -0
- package/docs/reference/functions/required.md +41 -0
- package/docs/reference/functions/selected.md +26 -0
- package/docs/reference/functions/sqlite.md +33 -0
- package/docs/reference/functions/testId.md +20 -0
- package/docs/reference/functions/text.md +40 -0
- package/docs/reference/functions/valued.md +28 -0
- package/docs/reference/functions/waitUntil.md +46 -0
- package/docs/reference/functions/within.md +29 -0
- package/docs/reference/index.md +196 -0
- package/docs/reference/interfaces/BaseResult.md +87 -0
- package/docs/reference/interfaces/CallResult.md +160 -0
- package/docs/reference/interfaces/CaptureScope.md +82 -0
- package/docs/reference/interfaces/CliResult.md +253 -0
- package/docs/reference/interfaces/ContainerAccessor.md +166 -0
- package/docs/reference/interfaces/DirectoryAccessor.md +33 -0
- package/docs/reference/interfaces/FetchResult.md +180 -0
- package/docs/reference/interfaces/FilesystemAccessor.md +46 -0
- package/docs/reference/interfaces/HttpResult.md +126 -0
- package/docs/reference/interfaces/JsonAccessor.md +46 -0
- package/docs/reference/interfaces/PageResult.md +356 -0
- package/docs/reference/interfaces/RenderResult.md +72 -0
- package/docs/reference/interfaces/RenderedText.md +110 -0
- package/docs/reference/interfaces/ResponseAccessor.md +47 -0
- package/docs/reference/interfaces/ScreenResult.md +128 -0
- package/docs/reference/interfaces/TableAccessor.md +26 -0
- package/docs/reference/interfaces/TextAccessor.md +96 -0
- package/docs/reference/type-aliases/ApiHandle.md +68 -0
- package/docs/reference/type-aliases/ApiSpecification.md +213 -0
- package/docs/reference/type-aliases/ApiSpecificationOptions.md +68 -0
- package/docs/reference/type-aliases/BrowserConsoleMessage.md +30 -0
- package/docs/reference/type-aliases/BrowserLinkElement.md +50 -0
- package/docs/reference/type-aliases/BrowserMetaElement.md +40 -0
- package/docs/reference/type-aliases/BrowserOpenOptions.md +87 -0
- package/docs/reference/type-aliases/BrowserPage.md +132 -0
- package/docs/reference/type-aliases/BrowserPort.md +51 -0
- package/docs/reference/type-aliases/CliEnv.md +11 -0
- package/docs/reference/type-aliases/CliHandle.md +80 -0
- package/docs/reference/type-aliases/CliInput.md +36 -0
- package/docs/reference/type-aliases/CliOutput.md +46 -0
- package/docs/reference/type-aliases/CliPort.md +58 -0
- package/docs/reference/type-aliases/CliSpecification.md +144 -0
- package/docs/reference/type-aliases/CliSpecificationOptions.md +134 -0
- package/docs/reference/type-aliases/ComponentChain.md +156 -0
- package/docs/reference/type-aliases/ComponentScenario.md +20 -0
- package/docs/reference/type-aliases/ComponentUi.md +47 -0
- package/docs/reference/type-aliases/ComponentVisitor.md +10 -0
- package/docs/reference/type-aliases/ContainerPort.md +113 -0
- package/docs/reference/type-aliases/Contract.md +68 -0
- package/docs/reference/type-aliases/ContractInput.md +13 -0
- package/docs/reference/type-aliases/ContractRequest.md +95 -0
- package/docs/reference/type-aliases/ContractResponder.md +23 -0
- package/docs/reference/type-aliases/ContractResponse.md +78 -0
- package/docs/reference/type-aliases/ContractResponseValue.md +13 -0
- package/docs/reference/type-aliases/Contracts.md +48 -0
- package/docs/reference/type-aliases/DatabaseKeys.md +16 -0
- package/docs/reference/type-aliases/DatabasePort.md +72 -0
- package/docs/reference/type-aliases/DeviceOpenOptions.md +46 -0
- package/docs/reference/type-aliases/DevicePort.md +51 -0
- package/docs/reference/type-aliases/DeviceScreen.md +32 -0
- package/docs/reference/type-aliases/DeviceTimeouts.md +39 -0
- package/docs/reference/type-aliases/DockerSpecConfig.md +43 -0
- package/docs/reference/type-aliases/DomMount.md +21 -0
- package/docs/reference/type-aliases/ElementKind.md +27 -0
- package/docs/reference/type-aliases/ElementMatch.md +74 -0
- package/docs/reference/type-aliases/ElementOptions.md +29 -0
- package/docs/reference/type-aliases/ElementRef.md +128 -0
- package/docs/reference/type-aliases/ExecOptions.md +37 -0
- package/docs/reference/type-aliases/FileAccessor.md +54 -0
- package/docs/reference/type-aliases/HonoApp.md +31 -0
- package/docs/reference/type-aliases/HttpContractFilter.md +49 -0
- package/docs/reference/type-aliases/HttpResponseInit.md +46 -0
- package/docs/reference/type-aliases/HttpStreamInit.md +58 -0
- package/docs/reference/type-aliases/IntegrationHandle.md +46 -0
- package/docs/reference/type-aliases/IntegrationSpecification.md +111 -0
- package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +42 -0
- package/docs/reference/type-aliases/Intercept.md +19 -0
- package/docs/reference/type-aliases/InterceptOptions.md +28 -0
- package/docs/reference/type-aliases/InterceptScope.md +11 -0
- package/docs/reference/type-aliases/IsolationStrategy.md +75 -0
- package/docs/reference/type-aliases/JobHandle.md +34 -0
- package/docs/reference/type-aliases/JobsHandle.md +57 -0
- package/docs/reference/type-aliases/JobsResult.md +16 -0
- package/docs/reference/type-aliases/JobsSpecification.md +94 -0
- package/docs/reference/type-aliases/JobsSpecificationOptions.md +56 -0
- package/docs/reference/type-aliases/LandmarkKind.md +20 -0
- package/docs/reference/type-aliases/LiterateRunFlags.md +27 -0
- package/docs/reference/type-aliases/LiterateServeRegistration.md +70 -0
- package/docs/reference/type-aliases/MatchFixtureOptions.md +27 -0
- package/docs/reference/type-aliases/MatchableRequest.md +61 -0
- package/docs/reference/type-aliases/MatcherKind.md +34 -0
- package/docs/reference/type-aliases/MobileBackendOptions.md +28 -0
- package/docs/reference/type-aliases/MobileElementKind.md +14 -0
- package/docs/reference/type-aliases/MobileElementMatch.md +60 -0
- package/docs/reference/type-aliases/MobileElementRef.md +20 -0
- package/docs/reference/type-aliases/MobileHandle.md +65 -0
- package/docs/reference/type-aliases/MobileScenario.md +20 -0
- package/docs/reference/type-aliases/MobileSpecification.md +51 -0
- package/docs/reference/type-aliases/MobileSpecificationOptions.md +126 -0
- package/docs/reference/type-aliases/MobileVisitor.md +81 -0
- package/docs/reference/type-aliases/MockOfOptions.md +26 -0
- package/docs/reference/type-aliases/MockPort.md +10 -0
- package/docs/reference/type-aliases/PinnedClock.md +10 -0
- package/docs/reference/type-aliases/PostgresOptions.md +32 -0
- package/docs/reference/type-aliases/ProcessOptions.md +112 -0
- package/docs/reference/type-aliases/RedisOptions.md +20 -0
- package/docs/reference/type-aliases/RenderSubject.md +12 -0
- package/docs/reference/type-aliases/ScreenNode.md +67 -0
- package/docs/reference/type-aliases/ServerPort.md +36 -0
- package/docs/reference/type-aliases/ServerResponse.md +46 -0
- package/docs/reference/type-aliases/ServerSpec.md +21 -0
- package/docs/reference/type-aliases/ServiceHandle.md +216 -0
- package/docs/reference/type-aliases/ServiceRecord.md +12 -0
- package/docs/reference/type-aliases/SpecDocument.md +84 -0
- package/docs/reference/type-aliases/SpecEnvToken.md +21 -0
- package/docs/reference/type-aliases/SpecFileAssertion.md +27 -0
- package/docs/reference/type-aliases/SpecFixture.md +30 -0
- package/docs/reference/type-aliases/SpecKind.md +8 -0
- package/docs/reference/type-aliases/SpecRun.md +108 -0
- package/docs/reference/type-aliases/SpecServeEntry.md +40 -0
- package/docs/reference/type-aliases/SpecStream.md +32 -0
- package/docs/reference/type-aliases/SpecificationConfig.md +290 -0
- package/docs/reference/type-aliases/SqliteOptions.md +36 -0
- package/docs/reference/type-aliases/SseEvent.md +58 -0
- package/docs/reference/type-aliases/StreamBody.md +61 -0
- package/docs/reference/type-aliases/TextFilter.md +22 -0
- package/docs/reference/type-aliases/VisitScenario.md +20 -0
- package/docs/reference/type-aliases/Visitor.md +215 -0
- package/docs/reference/type-aliases/WaitUntilOptions.md +46 -0
- package/docs/reference/type-aliases/WebsiteBackendOptions.md +37 -0
- package/docs/reference/type-aliases/WebsiteHandle.md +51 -0
- package/docs/reference/type-aliases/WebsiteSpecification.md +118 -0
- package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +40 -0
- package/docs/reference/variables/anthropic.md +19 -0
- package/docs/reference/variables/banner.md +10 -0
- package/docs/reference/variables/button.md +10 -0
- package/docs/reference/variables/clock.md +16 -0
- package/docs/reference/variables/complementary.md +10 -0
- package/docs/reference/variables/component.md +10 -0
- package/docs/reference/variables/content.md +10 -0
- package/docs/reference/variables/contentinfo.md +10 -0
- package/docs/reference/variables/dialog.md +10 -0
- package/docs/reference/variables/field.md +10 -0
- package/docs/reference/variables/form.md +10 -0
- package/docs/reference/variables/heading.md +10 -0
- package/docs/reference/variables/http.md +40 -0
- package/docs/reference/variables/intercept.md +11 -0
- package/docs/reference/variables/link.md +10 -0
- package/docs/reference/variables/listitem.md +10 -0
- package/docs/reference/variables/main.md +10 -0
- package/docs/reference/variables/match.md +8 -0
- package/docs/reference/variables/navigation.md +10 -0
- package/docs/reference/variables/openai.md +21 -0
- package/docs/reference/variables/option.md +17 -0
- package/docs/reference/variables/region.md +10 -0
- package/docs/reference/variables/row.md +10 -0
- package/docs/reference/variables/search.md +10 -0
- package/docs/reference/variables/specification.md +8 -0
- package/docs/reference/variables/status.md +10 -0
- package/docs/reference/variables/table.md +10 -0
- package/package.json +32 -11
- package/dist/spec-document.js +0 -651
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
# 19 — Linting
|
|
2
|
+
|
|
3
|
+
> **Maintenance note** — This documentation is living: when implementation or usage reveals a new edge case, document it in the relevant chapter in the same change.
|
|
4
|
+
|
|
5
|
+
`@jterrazz/test` ships the static enforcement channel of its own conventions: an **oxlint JS plugin** (`@jterrazz/test/oxlint`) carrying every **statique** convention rule (the constitution is [18 — Conventions](18-conventions.md)), plus a small **conventions checker** binary for the file kinds oxlint never visits (data fixtures). Together they make the conventions programmatic instead of review-borne.
|
|
6
|
+
|
|
7
|
+
## Enabling the plugin
|
|
8
|
+
|
|
9
|
+
The composable model — `testing` is the fragment every consumer composes into its base preset. With `@jterrazz/typescript` the whole wiring is two lines in `oxlint.config.ts`:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { testing } from '@jterrazz/test/oxlint';
|
|
13
|
+
import { compose, node } from '@jterrazz/typescript/oxlint';
|
|
14
|
+
|
|
15
|
+
export default compose(node, testing);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`node` is one of the toolchain's profiles, and a project names the one it is: `compose(<profile>, testing)`. The profile answers for the base rulebook and the framework plugins; `testing` adds this package's catalogue on top of whichever one you named. Which profile a project is, and what each adds, is [`@jterrazz/typescript`'s own chapter](https://github.com/jterrazz/package-typescript/blob/main/docs/07-lint-presets.md).
|
|
19
|
+
|
|
20
|
+
A published package names `library`, and its `isolatedDeclarations` refuses an inferred default export, so the config states its own type once:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { testing } from '@jterrazz/test/oxlint';
|
|
24
|
+
import { compose, library, type OxlintConfig } from '@jterrazz/typescript/oxlint';
|
|
25
|
+
|
|
26
|
+
const config: OxlintConfig = compose(library, testing);
|
|
27
|
+
|
|
28
|
+
export default config;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`compose(...fragments)` merges deterministically: `jsPlugins`/`plugins`/`ignorePatterns` concatenated + deduped, `rules`/`categories` shallow-merged (last wins), `overrides` concatenated. To deviate on a rule, compose an extra fragment last:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
export default compose(node, testing, {
|
|
35
|
+
rules: {
|
|
36
|
+
// A paginated envelope is read field by field: raise D12's cluster
|
|
37
|
+
// Threshold once here instead of suppressing the warning per case.
|
|
38
|
+
'jterrazz/d12w-response-body-probe': ['warn', { threshold: 6 }],
|
|
39
|
+
// The one module this repository doubles, with the reason in the line.
|
|
40
|
+
'jterrazz/i4-no-module-doubles': [
|
|
41
|
+
'error',
|
|
42
|
+
{ modules: ['@react-native-async-storage/async-storage'] },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A rule that takes options is declared, not suppressed — an `off` loses the check everywhere, a raised threshold keeps it. Five rules take one: `c1-domain-structure` (`depth` — the four tree shapes are [18 — Conventions](18-conventions.md#c1--the-folder-follows-the-assets)), `d12w-response-body-probe` (`threshold`, default 3), `d19w-probe-cluster` (`threshold`, default 3), `i1-layer-boundaries` (`layers`, the rule being inert without it) and `i4-no-module-doubles` (`modules`, the specifiers this repository doubles, each with its reason in the line above it).
|
|
49
|
+
|
|
50
|
+
- `testing` = `{ jsPlugins: ['@jterrazz/test/oxlint'], rules: recommendedRules, overrides }`, and it is DECLARED as oxlint's own `OxlintConfig`. That annotation is what makes `compose(<profile>, testing)` type-check with no assertion: an inferred `rules` widens every severity to `string`, which oxlint's closed union refuses. Its `overrides` relaxes `import/exports-last` for `**/*.specification.ts` — the A4 idiom (`export const { cli, cleanup } … ; afterAll(cleanup)`) legitimately ends a spec file on a non-export statement, so the relaxation ships here instead of being hand-rolled in every strict consumer. The same idiom has a second consequence the fragment cannot carry, because it belongs to a different tool: `cleanup` is exported and used only inside the file that exports it, which Knip reports as an unused export. One line settles it for the whole repository — `"ignoreExportsUsedInFile": true` in `knip.json`.
|
|
51
|
+
- Without `@jterrazz/typescript`, spread the fragment into your own config (`defineConfig({ ...testing })`) — it is a plain object.
|
|
52
|
+
- **Deviation fragments are plain objects — no extra imports.** The trailing `compose(<profile>, testing, { … })` argument is a bare object literal; `compose()` accepts it directly. Everything a config names arrives from `@jterrazz/typescript` — the profile, `compose`, `defineConfig`, the config types — because a project declaring `oxlint` or `oxfmt` itself would hold two opinions about its own toolchain. This matters most under a **local `file:` link** (developing the framework and a consumer side by side): pulling a linked package's config internals can resolve to an unbuilt path, whereas plain objects composed by the base preset never do.
|
|
53
|
+
- `recommendedRules` (also exported standalone) enables the whole catalogue in one spread: hard conventions at `error`, redundancy heuristics (ids ending in `w-…`) at `warn`.
|
|
54
|
+
- Wiring is **explicit** — the preset never auto-detects `@jterrazz/test` from your dependencies. If you don't compose `testing`, no `jterrazz/*` rule runs. When and how `typescript check` decides to RUN the checker is the toolchain's, not this chapter's: `@jterrazz/typescript` `docs/06-quality-checks.md` § The Test Conventions pass.
|
|
55
|
+
- Rules are individually addressable as `jterrazz/<rule-id>` if you want to deviate — prefer scoped `overrides` with a comment over global downgrades.
|
|
56
|
+
- `@jterrazz/test/oxlint` is **tool-facing only**: referenced from `oxlint.config.ts` (or a shared oxlint preset), never imported by test or production code. It is the one specifier exempt from rule F2 — a zero-runtime lint entry, so a preset that wires the plugin may import it from anywhere. F1 and F3 exempt every subpath the package's `exports` map publishes, this one among them.
|
|
57
|
+
- **The oxlint version is the toolchain's to pin.** `@jterrazz/typescript` owns the range every profile runs the binary at; a repository whose own code imports oxlint — this one does, from every rule test, through `oxlint/plugins-dev` — declares `oxlint` in its `devDependencies` at exactly that range, so the line here MIRRORS the pin instead of holding a second opinion about it and a plain `npm ci` hoists one oxlint rather than two. `src/lint/oxlint-pin.test.ts` fails the day the two diverge.
|
|
58
|
+
|
|
59
|
+
### Standalone — without `@jterrazz/typescript`
|
|
60
|
+
|
|
61
|
+
A repo that is not primarily TypeScript (a Go service with a thin TS test suite, say) can adopt just the conventions without inheriting any formatting or base-preset opinions. The `testing` fragment is a **complete oxlint config on its own** — `jsPlugins` + `rules` + the A4 `overrides`, no `extends` — so it can be the whole config:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// oxlint.config.ts — conventions only, no @jterrazz/typescript
|
|
65
|
+
import { testing } from '@jterrazz/test/oxlint';
|
|
66
|
+
|
|
67
|
+
export default testing;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This registers the plugin and enables the whole `jterrazz/*` catalogue at its intended severities; oxlint's own default rules still run, but no `@jterrazz/typescript` formatting/style preset is imposed. To deviate, spread and override: `export default { ...testing, rules: { ...testing.rules, 'jterrazz/c1-domain-structure': 'off' } }`.
|
|
71
|
+
|
|
72
|
+
The static plugin is only one of the seven channels. The **conventions checker** (token/HTTP grammar, the document passes, and the cross-file and tree passes — D4, C8/C9, C12/C16/C18/C20/C21w, A7, B5, J9) ships as a standalone binary; run it as the minimal second channel with no `@jterrazz/typescript` orchestration:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx jterrazz-test-check specs
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
It walks `specs/` (or the path you pass), reports token/reference/cross-file violations, and exits non-zero on an error — wire it into CI next to `oxlint`.
|
|
79
|
+
|
|
80
|
+
## What oxlint's own `vitest` plugin owns
|
|
81
|
+
|
|
82
|
+
This catalogue holds only what no upstream rule already holds. Oxlint ships a `vitest` plugin, and `@jterrazz/typescript` turns it on over the test globs in every profile — so a consumer composing `compose(<profile>, testing)` gets both layers and each convention is enforced exactly once.
|
|
83
|
+
|
|
84
|
+
Four hygiene rules used to live here and now live there: no committed `.only`/`.skip` (`vitest/no-focused-tests`, `vitest/no-disabled-tests`), an `expect()` in every test (`vitest/expect-expect`), no two literal titles alike in a file (`vitest/no-identical-title`), a lowercase title (`vitest/prefer-lowercase-title`). The ids `jterrazz/j1-no-only-skip`, `jterrazz/j3-no-expectless-test`, `jterrazz/j4-unique-test-names` and `jterrazz/j5-lowercase-title` no longer exist; a config still naming one fails oxlint's unknown-rule check.
|
|
85
|
+
|
|
86
|
+
One behaviour does not carry over, and nothing restores it. `prefer-lowercase-title` flags a title whose first word is an all-caps identifier (`VALID_CATEGORIES …`, `HTTP …`, `DI …`), which the deleted J5 exempted; the rulebook sets no `allowedPrefixes` — a title starts lowercase, whatever its first word. An existing title that opens on an identifier is RENAMED (`the VALID_CATEGORIES table …` reads as the sentence it always was), and a consumer who disagrees overrides the rule in its own deviation fragment, which is theirs to justify — the form is [§ Enabling the plugin](#enabling-the-plugin) above, and what a fragment may deviate on is [`@jterrazz/typescript`'s own chapter](https://github.com/jterrazz/package-typescript/blob/main/docs/07-lint-presets.md).
|
|
87
|
+
|
|
88
|
+
## The rule catalogue
|
|
89
|
+
|
|
90
|
+
The catalogue below is **generated** from `src/lint/manifest.ts` (each rule carries its normative text as `meta.docs`) — do not edit it by hand; run `npm run docs`. It is the **full seven-channel catalogue** (statique, upstream, checker, runtime, type, meta, process), grouped by convention family. The same set, trimmed to what an agent needs to apply and cite a rule, is regenerated alongside it into [`skills/jterrazz-test/references/rules.md`](../skills/jterrazz-test/references/rules.md).
|
|
91
|
+
|
|
92
|
+
One convention code may carry more than one ROW, when one convention is held over two shapes: `d5-spec-volatile-literal` and `d5w-spec-pinned-value` are D5's two halves, and `c12-spec-file-name` is one row with two clauses on two runs. A finding is published as `jterrazz-check(<code>)`, where the code is the id's first segment (`src/lint/rule-code.ts`) — so a ratchet key names a convention, never a pass, and one defect is one line of debt.
|
|
93
|
+
|
|
94
|
+
Seven columns. **Code** and **Implementation** are the two names a rule answers to — the convention id you cite in a review, and the rule id you address in a config; the Implementation cell also carries the row's anchor, so a rule MESSAGE ending in `docs/19-linting.md#<rule-id>` lands on the row. **Channel** is which of the seven enforces it. **Reach** is which files it looks at, in the vocabulary of `roleOf` (`src/lint/role.ts`): `module` a `*.test.ts`, `component` a `*.test.tsx`, `specification` a `*.specification.ts`, `config` a `vitest.config.*`, `ground` a `_fixtures`/`_expected`/`_requests`/`_seeds` tree, plus the two shapes that are not a role — `tests` (any file a test conventions bind) and `all`. **Convention** is the normative sentence, **Fix** the one imperative line that resolves it, **Rationale** why the rule exists. A `—` in Reach or Fix is a row that states none.
|
|
95
|
+
|
|
96
|
+
The catalogue is grouped **model first, then one section per facet**, because a
|
|
97
|
+
family is a kind of convention and not an audience: `C — Files & folders` mixes
|
|
98
|
+
a rule every facet obeys with one only the literate door can break. A reader
|
|
99
|
+
adopting one constructor reads `model` — the tree every facet is made of — and
|
|
100
|
+
their own section, and stops. The grouping is the `facet` field of each manifest
|
|
101
|
+
row — required, like `fix` and `reach` — and for a statique rule it is also
|
|
102
|
+
where the implementation sits
|
|
103
|
+
(`src/lint/rules/model/`, `src/lint/rules/api/`, …). The checker's passes carry
|
|
104
|
+
the field without the folder: several passes share one module because they share
|
|
105
|
+
one walk of the tree, and splitting the walk to split the file would cost a
|
|
106
|
+
second traversal per pass.
|
|
107
|
+
|
|
108
|
+
<!-- GENERATED:catalog — do not edit by hand; run `npm run docs`. Source: src/lint/manifest.ts -->
|
|
109
|
+
|
|
110
|
+
Every rule the framework enforces, across its enforcement channels — **statique** — 56 (oxlint rules), **upstream** — 3 (options set on an upstream rule), **checker** — 29 (bundled passes), **runtime** — 13 (execution-time refusals), **type** — 7 (what the compiler refuses), **meta** — 8 (what a meta-test holds), **process** — 3 (review-borne rules) — sourced from one manifest so the code and the catalogue can never drift.
|
|
111
|
+
|
|
112
|
+
## model — every facet, whichever constructor you reach for
|
|
113
|
+
|
|
114
|
+
### A — Creating runners
|
|
115
|
+
|
|
116
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
117
|
+
| ---- | ----------------------------------------------------------------------- | -------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
118
|
+
| A1 | <a id="a1-specification-file"></a>`a1-specification-file` | statique | tests | A runner is created only in a `*.specification.ts(x)` file under `specs/`: calling `specification.*` anywhere else is an error. The `.tsx` spelling is the same file with a `wrap` written in JSX. A `component`-role file is out of reach: a rendered unit starts nothing. | Move the call into `specs/<facet>/<facet>.specification.ts` and export the handle from there. | Anchoring runners to a recognisable filename makes the entry point discoverable and keeps tests declarative. |
|
|
119
|
+
| A2 | <a id="a2-known-constructors"></a>`a2-known-constructors` | statique | all | Six constructors and only six: `specification.api()`, `specification.jobs()`, `specification.cli(bin)`, `specification.integration()`, `specification.website()`, `specification.mobile()`. Any other member (`.app`, `.http`, `.stack`…) is an error. A rendered component starts nothing: it is a chain, not a member of this record. | Name one of the six, or reach for the `component` chain — a seventh subject is a decision before it is a constructor. | A closed surface stops parallel constructors being invented and keeps the API memorable. |
|
|
120
|
+
| A3 | <a id="a3-no-destructure-alias"></a>`a3-no-destructure-alias` | statique | all | The return is destructured under the constructor's canonical name, unaliased (`{ api, cleanup, docker }`); renaming (`{ api: myApi }`) is an error. | Drop the alias and keep the canonical name. | One instance name per facet makes every spec readable with no local context. |
|
|
121
|
+
| A4 | <a id="a4-cleanup-afterall"></a>`a4-cleanup-afterall` | statique | specification | The specification file passes `cleanup` to `afterAll`; a `cleanup` destructured but never handed over is an error. | Add `afterAll(cleanup)` at the end of the specification file. | Guaranteeing teardown stops containers and connections leaking between files. |
|
|
122
|
+
| A9 | <a id="a9w-redundant-root"></a>`a9w-redundant-root` | statique | all | A `root` pointing at the directory the automatic walk would have found is redundant → warning. | Drop the option: the walk finds the nearest `package.json` by itself. | Detection by convention has to stay the default; an explicit `root` is only justified where it fails. |
|
|
123
|
+
| A7 | <a id="a7-database-property"></a>`a7-database-property` | checker | tests | With ≥ 2 databases, `database:` is required on every `.seed()`/`.table()`; with exactly one it is forbidden — checked by crossing the `services:` record with the calls in the tests. | State `database:` everywhere or nowhere, whichever the record calls for. | The number of databases fixes the call API; the cross-file analysis catches the omission before run time. |
|
|
124
|
+
| A7 | <a id="a7-database-runtime"></a>`a7-database-runtime` | runtime | tests | The framework throws at run time if `database:` is absent with ≥ 2 databases, or present with exactly one (it doubles the checker channel). | State `database:` everywhere or nowhere, whichever the record calls for. | The runtime keeps the guarantee even where static analysis abstains. |
|
|
125
|
+
| A8 | <a id="a8-services-type-the-factory"></a>`a8-services-type-the-factory` | type | specification | The `services` record types the factory argument: `server: (services) => …` sees exactly the keys the record declares. | Declare the service in the record, or read the key that is there. | A service read by a name nothing started is a startup failure with no line to blame; the compiler has the record in hand and can say so at the call. |
|
|
126
|
+
| A11 | <a id="a11-server-xor-url"></a>`a11-server-xor-url` | type | specification | `server` and `url` are mutually exclusive on `specification.website()` — the pair is inexpressible, not runtime-checked. | State `server` for a site this run starts, `url` for one already running. | They name two different subjects, and a specification that states both leaves the reader to guess which site the spec is about. |
|
|
127
|
+
|
|
128
|
+
### B — Spec chains
|
|
129
|
+
|
|
130
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
131
|
+
| ---- | ----------------------------------------------------------------- | -------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
132
|
+
| B2 | <a id="b2-known-fixture-marker"></a>`b2-known-fixture-marker` | statique | tests | An unknown `$…` marker in a literal passed to `.fixture()` is an error (`$FIXTURES` is the only one). | Write `$FIXTURES/<name>` for the shared pool, or a path relative to the spec for local ground. | Catching a wrong marker statically avoids a fixture path resolved at random at run time. |
|
|
133
|
+
| B4 | <a id="b4-given-then"></a>`b4-given-then` | statique | tests | Every test carries `// Given -` then `// Then -` (both, in that order); a Given declared after a Then is an error. A `test.each` table is judged ONCE, on the table, not once per row. A `<case>.spec.yaml` document has no comments: its narration is its `description:`. | Write the two markers, in order, as sentences about the subject — not about the code (chapter 18 § The narration). | Given/Then narration makes a test's intent readable without reading its assertions. |
|
|
134
|
+
| B10 | <a id="b10-when-between-markers"></a>`b10-when-between-markers` | statique | tests | A `// When -` marker is optional; written, it sits after `// Given -` and before `// Then -`. | Move it to the action it narrates, or drop it when the chain is the action. | The optional marker is the one the order slips on: a `When` above the Given or below the Then tells a story the code does not follow. |
|
|
135
|
+
| B11 | <a id="b11-marker-one-line"></a>`b11-marker-one-line` | statique | tests | A marker is exactly one line: a `//` comment directly under one, at the same indentation, is a wrapped marker and an error. A tool’s directive is not a sentence — a linter’s `-disable`/`-enable`, a formatter’s `-ignore`, a `@ts-` pragma and a coverage line stand where they are. | Fold the continuation into the sentence, or separate it with a blank line. | The narration is a sentence about the subject; a paragraph hides its second half from every reader that only looks at the marker line. |
|
|
136
|
+
| B5 | <a id="b5-await-using-inference"></a>`b5-await-using-inference` | checker | tests | B5's one channel: docker-aware runners are inferred from the `docker:` option of the imported specification file, and every `.exec()` result bound without `await using` is reported. | Bind the result with `await using`, so the containers the run spawned are disposed with the scope. | Inferring the runners removes the hand-maintained list an oxlint rule would need. |
|
|
137
|
+
| B2 | <a id="b2-unknown-marker-runtime"></a>`b2-unknown-marker-runtime` | runtime | tests | The framework refuses at run time an unknown `$…` marker, or a `$FIXTURES` with no ancestor `specs` directory (with a guiding message). | Write `$FIXTURES/<name>`, from a spec that sits under a `specs/` tree. | A runtime message guides the author where the fault escapes the static channel (a non-literal argument). |
|
|
138
|
+
| B7 | <a id="b7-sequence-by-seed"></a>`b7-sequence-by-seed` | runtime | tests | A sequence is expressed by SEEDING the state the second run would have found: every chain starts on databases the framework has reset. | Seed the state the earlier step would have left, and open one chain per action. | A spec that depends on a previous one passes alone, passes in order, and fails the day the runner reorders them. |
|
|
139
|
+
| B1 | <a id="b1-one-terminal-action"></a>`b1-one-terminal-action` | type | tests | Exactly ONE terminal action closes a chain: what it answers is a result, and a result carries no verbs to continue with. | Open a second chain for a second action. | Two actions in one chain make a test with two Whens, and the reader cannot tell which one the Then is about. |
|
|
140
|
+
| B3 | <a id="b3-no-label-on-a-chain"></a>`b3-no-label-on-a-chain` | type | tests | A chain carries no label: the test name is the sentence. | Name the test; the chain states what it does, not what it is called. | A second place to read the intent is a second place for it to be stale. |
|
|
141
|
+
|
|
142
|
+
### C — Files & folders
|
|
143
|
+
|
|
144
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
145
|
+
| ---- | ----------------------------------------------------------------------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
146
|
+
| C1 | <a id="c1-domain-structure"></a>`c1-domain-structure` | statique | specs | A specs tree DECLARES its shape through the `depth` option: `facet-domain` (the default — a test at facet/domain depth, a `*.specification.ts` at the facet root), `facet` (a test at the facet root OR one domain folder down, never deeper), `mirror` (a test at any depth ≥ 1, named after its directory), `off`. In every mode, `off` included: a directory with a leading underscore is GROUND, never a domain — no spec lives inside one. One exception, and only one: ground may be CODE, and a `<module>.test.ts` sitting BESIDE the `<module>.ts` it tests is legal there (that is I2). A test with no neighbouring module, or a `*.specification.ts`, is still a violation. A `.test.tsx` is out of reach: a rendered unit is specified beside its component, never in a facet/domain tree — I2 holds its neighbourhood. | Move the file to the depth the tree declares, or declare the depth the tree actually has. | A fixed depth makes every file's place predictable and statically checkable. |
|
|
147
|
+
| C4 | <a id="c4-contract-shape"></a>`c4-contract-shape` | statique | contract | Under `specs/**/contracts/`: the ROOT carries only `*.contracts.ts` facades (a default export built by `defineContracts(...)` or a composition re-export; named exports are scenarios) and the provider folders `http` \| `openai` \| `anthropic`. A unit contract is `<provider>/<kebab-name>.ts` with an `export default defineContract(...)` (or a factory returning one), and its `request` builder must name the provider of its own folder. A provider folder is flat and holds only `*.ts` and `*.response.json`. | Move the file to the half it belongs to: a facade at the root, a unit under its provider. | A fixed tree separates the public facade from the internal units and makes the FOLDER carry the provider — filenames go back to being domain words, and bulky data sits beside the contract that serves it. |
|
|
148
|
+
| C6 | <a id="c6-tomatch-extension"></a>`c6-tomatch-extension` | statique | tests | `toMatch`'s argument carries its extension (`'help.txt'`, `'<case>.aria.yaml'` for a tree golden), except for a directory snapshot; a file subject with no extension is an error. | Write the fixture's full name, extension included. | The extension is part of the expected file's name — dropping it breaks `_expected/` resolution. |
|
|
149
|
+
| C7 | <a id="c7-seeds-sql-only"></a>`c7-seeds-sql-only` | statique | ground | `_seeds/` holds `*.sql` files only; any other file is an error. | Write the seed as SQL, or move the file to `_fixtures/` as the material it is. | `.seed()` carries database state only — no seed handlers, no prefix dispatch. |
|
|
150
|
+
| C8 | <a id="c8-referenced-fixture-exists"></a>`c8-referenced-fixture-exists` | statique | tests | A literal passed to `.request`/`.seed`/`.fixture`/`toMatch` must exist on disk under its conventional root; a missing path is an error. | Create the file, or fix the name — the message prints the path it looked for. | Catching a typo statically avoids a failure that would only surface at run time. |
|
|
151
|
+
| C10 | <a id="c10-contracts-boundary"></a>`c10-contracts-boundary` | statique | tests | Outside `specs/**/contracts/`, an import resolving into a provider folder (`contracts/{http,openai,anthropic}/…`) is an error: a test imports only the `*.contracts.ts` facades. | Add a named scenario export to the facade and import that instead. | Unit contracts are composition details; going through the facade keeps each scenario named in the same place as the world it derives from. |
|
|
152
|
+
| C11 | <a id="c11-contract-data-pairing"></a>`c11-contract-data-pairing` | statique | contract | Inside a provider folder, every `*.response.json` and every `*.request.ts` has a `<stem>.ts` sibling — the stem is the name up to the FIRST dot (`events.fr.response.json` → `events.ts`). Orphan data is an error. | Write the contract that serves the payload, or delete the payload. | Data exists only as served by a contract — a file with no owner is dead weight no test ever loads. |
|
|
153
|
+
| C13 | <a id="c13-underscored-ground"></a>`c13-underscored-ground` | statique | specs | Under `specs/`, a spec's ground carries a leading underscore: `_fixtures/`, `_expected/`, `_requests/`, `_seeds/`. A `fixtures/`, `expected/`, `requests/` or `seeds/` directory is a pre-14 name — an error naming the rename. | Rename the directory with the leading underscore the resolvers read. | No resolver reads the underscore-less name any more: left as is, the tree goes invisible rather than wrong, and the failure lands one step past its cause. |
|
|
154
|
+
| C9 | <a id="c9-dead-fixtures"></a>`c9-dead-fixtures` | checker | ground | No dead fixtures: every file under `_seeds/`/`_requests/`/`_fixtures/` and every top-level entry of `_expected/` must be referenced; a feature directory with no test file and no `*.spec.yaml` is an orphan (a warning when the argument is not a literal). A `<case>.spec.yaml` references the fixtures its `fixture:` entries name. | Reference it from the spec it stands under, or delete it. | The C8 mirror — a fixture nothing references is dead weight that misleads the reader. |
|
|
155
|
+
| C12 | <a id="c12-spec-file-name"></a>`c12-spec-file-name` | checker | specs | The suffix says the kind, and the tree agrees with it. A `.spec.ts` — the ASSEMBLED product — lives under `specs/<facet>/`; anywhere else is an error. A `.test.ts` — the UNIT — lives beside its module; under a facet folder (`api`, `cli`, `integration`, `jobs`, `mobile`, `website`) it is an error, fixable by `--fix` (a `git mv`). A `.test.ts` that reaches no runner is C18’s, whose fix is the opposite move, and the mover leaves it alone. A first level that is NOT a facet is a repository suite: it covers a tree, keeps `.test.ts`, and C1’s declared depth is what judges it — unless the tree carries its `*.specification.ts` at its own ROOT, which makes the whole tree one facet’s and every `.test.ts` below it a spec. A document is named `<case>.spec.yaml`: `<case>` in kebab-case, without the words `test`/`spec`/`cli` the suffix already carries, and never the bare name of its directory. | Move the `.spec.ts` under `specs/<facet>/` (or rename it `.test.ts` beside its module); rename the facet `.test.ts` to `.spec.ts` — `jterrazz-test-check <root> --fix` does it with `git mv`, and names every `include`/`exclude` entry of the member the rename broke. | With no agreement between the suffix and the tree, a reader cannot say what a test proves without opening it; and the filename is the first sentence they read — `rm/rm.spec.yaml` says nothing twice. |
|
|
156
|
+
| C14 | <a id="c14-pool-fixture-shared"></a>`c14-pool-fixture-shared` | checker | ground | A pool fixture is SHARED, or it is local: a directory of `<specs>/_fixtures/` referenced (`$FIXTURES/<name>`) from a single spec directory — two documents of one leaf count as one — must live beside that leaf, at `<leaf>/_fixtures/<name>/`, referenced by the relative form. Zero references is the dead-fixture error (C9). Fixed by `--fix`: the directory is moved and the literals rewritten. | Run `jterrazz-test-check <root> --fix`; it moves the directory and rewrites what named it. | A single scenario's fixture parked in the pool reads as shared ground: everyone assumes someone else depends on it and nobody dares touch it. |
|
|
157
|
+
| C15 | <a id="c15-local-fixture-reach"></a>`c15-local-fixture-reach` | checker | ground | A local fixture is read only from its own directory: a `fixture:`/`.fixture()` path containing `..`, or whose target leaves the `_fixtures/` of the spec naming it, is an error — ground shared by several leaves goes in the `$FIXTURES` pool. | Copy it under this leaf's own `_fixtures/`, or promote it to the pool. | A leaf reaching into a neighbour's fixtures is the pool through another door, without the pool's visibility: the neighbour breaks a spec two folders away without knowing. |
|
|
158
|
+
| C18 | <a id="c18-module-test-under-facet"></a>`c18-module-test-under-facet` | checker | specs | A test under `specs/<facet>/`, where the facet root holds a `*.specification.ts(x)`, imports that module or constructs a runner itself. Ground is out of reach — nothing under a leading-underscore folder is a spec, a `.test.ts` under `_expected/` being the GOLDEN a fixer writes. So is any first-level folder that is not a facet: a specification with no module to sit beside and no runner to reach lives in a repository suite there. | Import `{ <facet> }` from `<facet>.specification.js`, move a module test beside its module, or move a repository suite to a first-level folder of its own under `specs/`. | A test reaching no runner under a facet is a module test paying the facet's budget and services to prove something that belongs beside its module. |
|
|
159
|
+
| C20 | <a id="c20-facet-folder"></a>`c20-facet-folder` | checker | specs | A first-level folder named for one of the six facets holds a `*.specification.ts(x)` calling `specification.<facet>(`. Positive-only: any other first-level name is a repository suite, judged by C1's declared depth alone. | Create the specification the folder promises, or rename the folder. | Folder = constructor is the law the fork rests on; a facet folder with no runner tells the reader something false before they open a file. |
|
|
160
|
+
| C21 | <a id="c21w-ground-owned-by-one"></a>`c21w-ground-owned-by-one` | checker | ground | Ground in a leaf holding ≥ 2 node tests (`.test.ts`, `.spec.ts`) that a single one of them reads is a warning. A `.test.tsx` tree is out of reach, as it is for C1. | Give that spec its own domain folder, with the ground beside it. | Ground one spec reads, parked where several sit, reads as shared: everyone assumes someone else depends on it and nobody dares touch it. |
|
|
161
|
+
| C23 | <a id="c23-seed-no-clock-read"></a>`c23-seed-no-clock-read` | checker | ground | A `_seeds/**/*.sql` states absolute timestamps: `CURRENT_TIMESTAMP`/`CURRENT_DATE`/`CURRENT_TIME`, `NOW()` and the quoted `'now'` SQLite's date functions take (`datetime('now')`, `strftime(…, 'now')`, `date('now', '-1 day')`) are refused. A comment is not state, so a clock read inside one is not a finding. | Write the instant the case is about, and pin the run's clock with `.clock()` on the chain. | A seed that reads the clock makes the row's date the date the suite happened to run: a case about "published yesterday" proves something different every day, and under SQLite `datetime('now')` writes a space-separated text where an ORM writes ISO `T`, so the two misorder across a calendar day. The R8 dry run counts 36, every one of them signews-api's. |
|
|
162
|
+
| C5 | <a id="c5-slash-is-a-subfolder"></a>`c5-slash-is-a-subfolder` | runtime | ground | A slash in a fixture name is a SUBFOLDER of `_expected/`: `toMatch('build/verbose.txt')` resolves to `_expected/build/verbose.txt`. | Name the subfolder in the fixture name; the resolver creates it under `_expected/`. | A flat `_expected/` stops being readable at a dozen files, and the alternative — a second option naming a directory — would be a second way to say where a golden lives. |
|
|
163
|
+
| C1 | <a id="c1-asset-grouping"></a>`c1-asset-grouping` | process | specs | The folder follows the assets: a test with asset directories of its own gets its own domain; tests with no local assets group as sibling `<aspect>` files. The static rule only checks the depth. | Read what the assets say: a test that stands on its own ground earns a folder. | It is the assets that settle the grouping — a criterion no channel can decide alone. |
|
|
164
|
+
|
|
165
|
+
### D — Assertions
|
|
166
|
+
|
|
167
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
168
|
+
| ---- | ----------------------------------------------------------------------------------- | -------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
169
|
+
| D2 | <a id="d2-await-io-matcher"></a>`d2-await-io-matcher` | statique | tests | An IO matcher (`toMatchRows`/`toBeEmpty`/`toBeRunning`) must be awaited or returned; otherwise the assertion never runs → error. | Put `await` in front of the `expect(...)`. | An un-awaited IO assertion passes silently — the worst failure mode a test has. |
|
|
170
|
+
| D2 | <a id="d2w-await-sync-matcher"></a>`d2w-await-sync-matcher` | statique | tests | `await` on an always-synchronous matcher (`toBe`/`toEqual`/`toContain`/`toHaveLength`) is redundant → warning. | Drop the `await`. | A pointless await hides the signal that marks the real IO matchers. |
|
|
171
|
+
| D6 | <a id="d6w-transform-token-equivalent"></a>`d6w-transform-token-equivalent` | statique | tests | A `transform` that only rewrites into equivalents of the standard tokens duplicates the grammar → warning. | Delete the transform and write the token the grammar already has. | `transform` is the escape hatch for noise the tokens do not cover — not a second copy of them. |
|
|
172
|
+
| D8 | <a id="d8w-text-bypass"></a>`d8w-text-bypass` | statique | tests | `expect(x.text).toContain/toMatch` short-circuits the typed accessor subject → warning. A RegExp argument is exempt: `toMatch` on an accessor takes a fixture NAME and throws on a regex, so reaching through to `.text` is the sanctioned form for a raw pattern (chapter 14). | Assert on the accessor itself (`expect(x)`), which carries the tokens and the fixture resolution. | Asserting on `.text` throws away the token grammar and the subject's `toMatch('file')` resolution. |
|
|
173
|
+
| D9 | <a id="d9w-single-use-ref"></a>`d9w-single-use-ref` | statique | tests | A capture ref (`match.ref`, `{{kind#ref}}`) appearing exactly once in the whole file (code plus the referenced `_expected/` fixtures) is named for nothing → warning. | Drop the ref name and keep the bare token. | A ref only earns its name when it asserts equality across at least two occurrences. |
|
|
174
|
+
| D13 | <a id="d13w-unfrozen-negative-fixture"></a>`d13w-unfrozen-negative-fixture` | statique | tests | A `toMatch` whose FAILURE is the subject of the test must carry `{ frozen: true }` → otherwise `TEST_UPDATE=1` silently rewrites the deliberately-wrong fixture instead of throwing → warning. Two shapes: the exact wrapper (`expect(() => …).toThrow()` / `expect(…).rejects.toThrow()`) and the bounded heuristic — a golden inside a helper whose body also asserts a throw, which is where a helper owning the try/catch puts it. | Pass `{ frozen: true }` so update mode never overwrites the fixture the test is about. | In update mode an unfrozen matcher writes instead of throwing: the negative fixture is corrupted by its own real output and the assertion stops testing anything. `frozen` pins it. |
|
|
175
|
+
| D16 | <a id="d16-sampled-oracle"></a>`d16-sampled-oracle` | statique | tests | No sampled value under an oracle: `new Date()` (no argument), `Date.now()`, `performance.now()`, `Math.random()` and `randomUUID()` are an error as a direct argument of `expect` or of its matcher, and anywhere inside a structural matcher's expected shape. A test that pins the clock (`clock.at`, `clock.advance`) is out of reach — the reading is then the constant it chose — and so is a `*.specification.ts(x)`, where a runner legitimately samples at startup. | Pin it with `clock.at()`, or match it with `{{iso8601}}`/`match.uuid`. | An assertion that reads the machine twice holds whatever the subject does, and the day it fails it fails for the clock. |
|
|
176
|
+
| D16 | <a id="d16w-ambient-value"></a>`d16w-ambient-value` | statique | tests | A sampled value anywhere in a test callback is a warning — the wider net around D16, silent where D16 already refuses and on a name built in a template literal. A `*.specification.ts(x)` is out of reach. | Reach for `clock.at()`, `clock.advance()`, or a token in the golden. | A value sampled into the Given travels into the subject, and the assertion that reads it back is the same tautology one step removed. |
|
|
177
|
+
| D17 | <a id="d17w-double-only-oracle"></a>`d17w-double-only-oracle` | statique | module | A module test whose every assertion reads the call log of a double it built itself (`mockOf`, `vi.fn`, `vi.spyOn`) is a warning. The reach is the `module` role: on a component a callback prop IS the contract with its parent. It is a warning, and not an error, because one subject answers this way honestly — one whose PRODUCT is a call it was handed (a listener, an output port) — and the shape is the same one either way. | Assert the returned value or the resulting state — or say in a line that the call IS the product. | The subject can return anything, raise anything, or return nothing at all, and every assertion still passes. |
|
|
178
|
+
| D18 | <a id="d18w-existence-only-oracle"></a>`d18w-existence-only-oracle` | statique | tests | A test whose ONE assertion is an existence check (`toBeDefined`, `toBeTruthy`, `not.toBeNull`, `not.toBeUndefined`, a bare `toThrow` or a bare `toHaveBeenCalled`) is a warning. Beside a real assertion the same check is a precondition and stays silent, `expect.soft` counts as an assertion, and a NEGATED bare matcher (`not.toHaveBeenCalled`) is a precise proof rather than a loose one. | Assert the value, a golden, or a row. | `toBeDefined()` passes for `0`, `[]`, the wrong object and the right one: as a whole proof it records that the call returned, not what it answered. |
|
|
179
|
+
| D19 | <a id="d19w-probe-cluster"></a>`d19w-probe-cluster` | statique | tests | Three probes (`threshold`, default 3) on one goldenable subject — `stdout`, `stderr`, `content`, `head`, `meta()`, `canonical`, `alternates`, `tree`, `html` — with no `toMatch('<file>')` on it is a warning. A negated probe states an absence no golden can carry and is not counted; `value` and `error` are single readings and are not goldenable subjects. | Golden the subject: `expect(result.stdout).toMatch('<case>.txt')`, tokens for what moves. | Three greps state three lines, leave everything between them unstated, and still have to be rewritten one by one the day the output changes. |
|
|
180
|
+
| D20 | <a id="d20-golden-not-snapshot"></a>`d20-golden-not-snapshot` | upstream | tests | No vitest snapshot matcher: `toMatchSnapshot`, `toMatchInlineSnapshot`, `toMatchFileSnapshot`, `toMatchAriaSnapshot`, `toMatchScreenshot`, `toThrowErrorMatchingSnapshot` and `toThrowErrorMatchingInlineSnapshot` are refused by `vitest/no-restricted-matchers`, whose option the `testing` fragment sets (ADR-005). | Write the golden: `expect(subject).toMatch('<name>.<ext>')` under `_expected/`; under `src/` compare to a literal with `toStrictEqual`. | A snapshot is written by the run that was supposed to be judged by it: nobody reads the diff, and a wrong answer becomes the expectation the moment someone re-records. |
|
|
181
|
+
| D4 | <a id="d4-malformed-ref"></a>`d4-malformed-ref` | checker | ground | A malformed ref of a known kind (`{{iso8601#}}`, `{{uuid #id}}`) in a text file under `_expected/` is an error. | Write `{{kind#name}}` with no space, and a name for the ref. | A malformed capture would fail silently — naming it makes it visible early. |
|
|
182
|
+
| D4 | <a id="d4-unknown-token"></a>`d4-unknown-token` | checker | ground | Every `{{token}}` in an `_expected/` fixture — or in an expected stream of a `<case>.spec.yaml` document — belongs to the frozen vocabulary; an unknown token is an error. | Write a token the grammar knows; the message prints the full list. | A closed grammar shared with the runtime matcher stops the two channels drifting. |
|
|
183
|
+
| D3 | <a id="d3-tomatch-resolves-under-expected"></a>`d3-tomatch-resolves-under-expected` | runtime | tests | Every `toMatch` subject resolves against `_expected/` — response, stream, json, directory, tree. Only `.request()` reads `_requests/`. | Put the golden under `_expected/` and name it with its extension. | One resolution rule for every subject means a reader knows where a golden lives without knowing which accessor produced it. |
|
|
184
|
+
| D7 | <a id="d7-strict-intercepts"></a>`d7-strict-intercepts` | runtime | tests | As soon as an `api`/`jobs` chain declares an intercept, any unmatched outgoing request (an exhausted queue included) fails the spec with an explicit error. | Declare the contract the request needs, or stop making the request. | A guarded network makes external interactions exhaustive and intentional. |
|
|
185
|
+
| D14 | <a id="d14-tomatch-fixture-name"></a>`d14-tomatch-fixture-name` | runtime | tests | `toMatch` on an accessor subject (`stream`/`json`/`response`/tree) expects a fixture NAME (extension included): passing a `RegExp` (or any non-string) throws immediately, naming the subject and the `expect(x.text).toMatch(/re/)` escape hatch. | Write the fixture's name, or assert on `.text` when a pattern is genuinely what you mean. | The instinct inherited from vitest (`toMatch(/re/)`) would otherwise hit the extension error or coerce the regex into `"/re/"`. The accessor argument is never a value-side literal, and a static heuristic would confuse the legitimate `expect(string).toMatch(/re/)` (D3/D8) — only the runtime channel refuses it cleanly, with no false positives. |
|
|
186
|
+
| D1 | <a id="d1-read-only-accessors"></a>`d1-read-only-accessors` | type | tests | A result accessor is read-only. | Assert on what the run produced; change the Given if the answer is wrong. | The answer a run produced is evidence, and a test that can rewrite its own evidence proves nothing. |
|
|
187
|
+
| D11 | <a id="d11-golden-file"></a>`d11-golden-file` | process | tests | A tool's output is asserted as a full golden per scoped use case, not as a cluster of greps; `.grep()` stays the scalpel for targeted probes. | Golden the stream, and keep `.grep()` for the one line a case is genuinely about. | A review judgement — the static channel cannot tell a legitimate grep from a lazy one. |
|
|
188
|
+
|
|
189
|
+
### E — Environment & runner configuration
|
|
190
|
+
|
|
191
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
192
|
+
| ---- | ------------------------------------------------------------------- | -------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
193
|
+
| E2 | <a id="e2-preset-config"></a>`e2-preset-config` | statique | config | A `vitest.config.*` default-exports `defineSpecConfig(...)`; the call is resolved through an `export default <Identifier>` declarator, through a `satisfies`/`as` annotation, and through one `mergeConfig(defineSpecConfig(…), …)` — vitest’s own way of layering over the preset. | Start from `defineSpecConfig()` — budgets, the artefact dir and the `_fixtures` exclusion are the preset's. | A config off the preset runs on vitest's five-second budget, writes artefacts at the repository root and collects `_fixtures/` as specs — none of it visible in the file that caused it. |
|
|
194
|
+
| E4 | <a id="e4w-project-binding"></a>`e4w-project-binding` | statique | config | A project literal collecting `specs/<facet>/` is named `<facet>`, and one named for a facet is rooted there; `unit` collects outside `specs/`. What an include collects is read from the GLOB and the config’s own place in its package, never from the absolute path. Any other name is out of reach — a repository suite names its projects as it likes. | Name the project after the facet it collects; `{ include, exclude }` stay yours. | `--project component` has to mean the same thing in every repository: it is the one word a CI job, a Makefile target and an agent all type. |
|
|
195
|
+
| E7 | <a id="e7w-include-prefix-exists"></a>`e7w-include-prefix-exists` | statique | config | An `include` glob of a PROJECT — an object under `test` or in `projects`, never `optimizeDeps.include` and its kin — whose static prefix (the segments before the first wildcard, brace or bracket), resolved from the config, is not a directory is a warning. | Point the glob at the folder that exists, or delete the project that collects nothing. | A project that collects nothing passes: the suite never ran, and the only trace is a zero nobody reads. |
|
|
196
|
+
| E9 | <a id="e9w-env-assignment-in-test"></a>`e9w-env-assignment-in-test` | statique | tests | A test writes the process environment through `vi.stubEnv` alone: a raw assignment onto a variable of it (named or computed), a replacement of `process.env` itself, and a `delete` of one of its variables are each a warning; a `*.specification.ts(x)` is out of reach by role. | Use `vi.stubEnv('X', 'y')`, which restores itself; a CLI default belongs to `specification.cli({ defaults })`. | The assignment outlasts the test: the next file in the same worker inherits it, and the failure lands somewhere else. |
|
|
197
|
+
| E3 | <a id="e3-config-present"></a>`e3-config-present` | checker | member | Every workspace member with tests OF ITS OWN (a `test` script that does not delegate to members, or a `*.test.ts(x)` outside another package’s tree) declares a vitest config (`vitest.config.*`). The MEMBER pass judges the member itself, with or without a `specs/` root; a root that only delegates owes nothing. | Write `export default defineSpecConfig()` in the member — the preset carries the budgets, the artefact directory and the `_fixtures/` exclusion. | Without a config, vitest's 5 s budget and an unexcluded `_fixtures/` are running the member's tests: the default is not chosen, it is inherited. |
|
|
198
|
+
| E1 | <a id="e1-env-allowlist"></a>`e1-env-allowlist` | meta | all | Every environment variable the framework reads is on the allow-list, and a dynamic read carries its sanction comment. | Add the variable to `env-allowlist.ts`, or read it through the option that already exists. | An env read nobody declared is a behaviour a consumer cannot see, cannot set, and cannot turn off. |
|
|
199
|
+
|
|
200
|
+
### F — Imports & protecting production
|
|
201
|
+
|
|
202
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
203
|
+
| ---- | --------------------------------------------------------------------- | -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
204
|
+
| F1 | <a id="f1-no-subpath-import"></a>`f1-no-subpath-import` | statique | all | Everything is imported from `@jterrazz/test`; an import of `@jterrazz/test/<subpath>` is an error, except the subpaths the package's `exports` map publishes — the entries are `.`, `./vitest`, `./oxlint` and `./schema`, and the three named ones are exempt everywhere. The list is READ from the manifest, never copied into the rule, so a published entry is exempt the day it ships. | Import the name from `@jterrazz/test`; it is re-exported there by design. | A single entry point keeps the public API explicit and the internal subpaths invisible. |
|
|
205
|
+
| F2 | <a id="f2-no-test-imports-in-prod"></a>`f2-no-test-imports-in-prod` | statique | all | A production file never imports `vitest`, `@jterrazz/test`, a `*.test.*`, a `*.fixtures.*` or `mockOf` (one exception: `@jterrazz/test/oxlint`). | Move the code that needs the import into a test, or inject what it needs as a port. | Keeping test artefacts out of production protects the consumer's application bundle. |
|
|
206
|
+
| F3 | <a id="f3-specs-public-entry"></a>`f3-specs-public-entry` | statique | specs | From `specs/`, only a deep import of the framework's INTERNALS is forbidden: a relative path resolving into the framework repository's `src/{model,facets,seams,runner,lint}/`, or any `@jterrazz/test/<subpath>` the package's `exports` map does not publish — the published subpaths are exempt, READ from the manifest as in F1, which holds the list. A consumer importing ITS OWN app's source is always allowed (that is the documented pattern). No folder exception: a probe that cannot reach its subject through the public entry is a module test beside its module, not a spec. | Reach the framework through `@jterrazz/test`, or move the probe beside the module it covers. | Testing through the public surface keeps specs decoupled from the framework's internal paths, without getting in the way of a consumer importing its own app. |
|
|
207
|
+
| F4 | <a id="f4-no-test-to-test-import"></a>`f4-no-test-to-test-import` | statique | tests | A test file never imports another test file. | Move what the two share into a `*.fixtures.ts` neighbour. | Sharing between tests goes through `*.fixtures.ts`, not through test-to-test imports that couple the files. |
|
|
208
|
+
| F5 | <a id="f5-fixtures-only-from-tests"></a>`f5-fixtures-only-from-tests` | statique | all | A `*.fixtures.ts` is importable only from a test file. | Move the value into production code if production needs it; otherwise import it from a test. | Confining fixtures to tests stops test data leaking into production code. |
|
|
209
|
+
| F6 | <a id="f6-no-foreign-test-runtime"></a>`f6-no-foreign-test-runtime` | statique | tests | A test file imports no second test runtime: `@testing-library/*`, `happy-dom`, `jsdom`, `vitest/browser`, `vitest-browser-*`, `@vitest/browser*`, `msw`, `msw/*`, `nock`, `sinon`, `vitest-mock-extended`, `jest`, `@jest/*`, `mockdate`, `playwright`, `playwright-core`, `@playwright/test`, `webdriverio` and `appium` are errors, each named with what replaces it; `react-dom/server` is one too, in the `module` role alone. Reach: the roles that RUN a test (`module`, `component`, `spec`, `specification`) — a fixture project’s config and a providers module under `specs/` legitimately name the adapter. | Go through what replaces the seam: a facet, the element vocabulary, `intercept()`, `clock` or `mockOf`. | A test importing the adapter speaks the adapter's dialect: a repository ends up with as many vocabularies as it has seams. Every group names the seam this vocabulary already owns — the runner, the browser, the simulator, the network, the clock and the doubles. |
|
|
210
|
+
| F8 | <a id="f8-no-seam-dependency"></a>`f8-no-seam-dependency` | checker | member | A member declares no dependency `@jterrazz/test` already carries (`msw`, `vitest-mock-extended`; `yaml` in devDependencies only — as a production dependency it is the product’s own library) and no retired seam (`happy-dom`, `jsdom`, `@testing-library/*`, `@playwright/test`, `mockdate`) — except `@testing-library/react-native` where jest runs the tests of the member — the only vocabulary that runtime has until the React Native answer lands. Jest runs them when the member DECLARES it (a `jest`/`jest-expo` dependency, a `jest` field, a `test` script calling it) or when a `jest.config.*` elsewhere in the workspace ROOTS into the member, which is the shape a library co-locating its render tests takes when the app beside it owns the only working toolchain. The optional peers — `playwright`, `vite`, `react`, `better-sqlite3`, `pg`, `redis`, `testcontainers`… — are declared by the consumer, by design. | Remove the declaration: a transitive is already resolved by the package; a retired seam has a facet that replaces it (`component()`, `website()`, `clock`, `intercept()`). The diagnostic carries a sentence for each case. | A duplicate declaration lets two versions of one seam coexist, and a retired seam keeps a vocabulary the framework has replaced. |
|
|
211
|
+
|
|
212
|
+
### G — Infrastructure
|
|
213
|
+
|
|
214
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
215
|
+
| ---- | ------------------------------------------------------------- | ------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
216
|
+
| G2 | <a id="g2-per-worker-isolation"></a>`g2-per-worker-isolation` | runtime | tests | Isolation is per WORKER: each worker takes its own schema, database index or file copy, so two workers never share state. | Take the handle the runner gives the chain; a client opened at the default index talks to another worker. | Parallelism is the default, and a shared database turns it into a source of failures that never reproduce alone. |
|
|
217
|
+
| G3 | <a id="g3-docker-aware-capture"></a>`g3-docker-aware-capture` | runtime | tests | The capture is docker-aware: every container a run spawned carries that run label, and the scope exit removes them whether the test asked about them or not. | Bind the result with `await using`, and declare `docker` on the runner. | A container that outlives its run is a machine filling up quietly, and the next run inherits its state. |
|
|
218
|
+
|
|
219
|
+
### I — Source architecture
|
|
220
|
+
|
|
221
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
222
|
+
| ---- | ----------------------------------------------------------- | -------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
223
|
+
| I1 | <a id="i1-layer-boundaries"></a>`i1-layer-boundaries` | statique | all | A project DECLARES its layers under `src/` through the `layers` option (allowed packages, internal imports, one folder per dependency, seams); any import outside the declared edges is an error. With no layer map the rule is inert. | Declare the edge in the layer map, or route the import through the layer that owns it. | An architecture belongs to the project: declared boundaries are checkable, an assumed one is worked around. |
|
|
224
|
+
| I2 | <a id="i2-sibling-test-naming"></a>`i2-sibling-test-naming` | statique | tests | The test of `<file>.ts` is `<file>.test.ts` beside it; a misnamed `.test.ts`, a `__tests__/` directory or a package's `tests/` or `test/` root is an error. A RENDERED unit answers to the same law wherever it lives: a `.test.tsx` is the neighbour of the `.tsx` it renders, or of the `.ts` of the hook or DOM function whose Host it carries. A `module`-role file under a `specs/` tree is a repository suite — it covers a tree, so it has no neighbour to miss. | Put the test beside the unit it covers, under the same basename. | Neighbouring tests (parity with Go's `foo_test.go`) keep a test and its code together and discoverable. |
|
|
225
|
+
| I4 | <a id="i4-no-module-doubles"></a>`i4-no-module-doubles` | statique | tests | A test doubles a PORT, never a module: `vi.mock`/`vi.doMock` of a specifier outside the `modules` allow-list (a config comment states why each entry is there), a `__mocks__/` or `__fixtures__/` directory, and — in a `module`-role test — importing a data asset (`.json`, `.sql`, `.yaml`, …) are all errors. The specifier is read in BOTH spellings — `vi.mock('<spec>')` and the `vi.mock(import('<spec>'))` form `vitest/prefer-import-in-mock` requires — so an allow-listed module has a green spelling under both rules. A dotted specifier (`./dashboard.post`) is still code. The `vi.stubGlobal` clause moved to M3 in 16.0: one owner per convention. | Double the port with `mockOf<Port>()`, keep a payload in a `*.fixtures.ts` neighbour, and allow-list a native module with its reason (chapter 05 § Doubles). | A module test that reaches for a real file or a module mock is describing an assembled product, and that has a facet of its own. |
|
|
226
|
+
|
|
227
|
+
### J — Hygiene
|
|
228
|
+
|
|
229
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
230
|
+
| ---- | ------------------------------------------------------------------------- | -------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
231
|
+
| J2 | <a id="j2-no-sleep"></a>`j2-no-sleep` | statique | tests | No arbitrary sleep (`setTimeout`/`setInterval`/`Atomics.wait`, or a `node:timers/promises` import) in any test file — synchronisation is `see()`/`gone()` inside a scenario and `waitUntil(predicate)` everywhere else. A timer inside a test DOUBLE's implementation (`vi.fn(…)`, `mockImplementation(…)`, a `mockOf` handler) is out of reach: the double is staging the world, not the test waiting on it. | Wait for the condition, not for a duration. | A fixed sleep makes tests slow and flaky; waiting on a condition is deterministic. |
|
|
232
|
+
| J6 | <a id="j6w-given-in-the-test"></a>`j6w-given-in-the-test` | statique | tests | A `beforeEach`/`beforeAll` in a test file is a warning, and so is an `afterEach`/`afterAll` whose body does more than restore (`cleanup`, `vi.useRealTimers`, `vi.restoreAllMocks`, `vi.resetAllMocks`, `vi.unstubAllGlobals`, `vi.unstubAllEnvs`) — handed over by name (`afterEach(vi.restoreAllMocks)`) or called in the body. `afterAll(cleanup)` in a `*.specification.ts` is A4's own idiom and out of reach by role. | Call a function from each test instead of a hook; `clock.at()` and `intercept()` restore themselves. | A hook moves the setup out of the only place a reader looks, shares it with every test in the file, and half of them come to depend on it quietly. |
|
|
233
|
+
| J10 | <a id="j10-one-describe-level"></a>`j10-one-describe-level` | upstream | tests | At most one level of `describe` — `vitest/max-nested-describe` with `{ max: 1 }`, set by the `testing` fragment. | Flatten the nesting: the test name is the sentence, and one level groups the file. | A tree of contexts is state a reader has to hold in their head to know what a test is about, and the name of the test stops being the sentence it was. |
|
|
234
|
+
| J9 | <a id="j9-checker-suppression-reason"></a>`j9-checker-suppression-reason` | checker | specs | Every `checker-disable-next-line` / `checker-disable-line` directive carries ` -- <reason>`, wherever a checker pass can be silenced: a test, a document, a fixture under ground. The word inside a string is prose, not a directive. | Write `checker-disable-next-line <id> -- <reason>`. | A suppression with no reason is a rule turned off by someone no longer here: the next reader cannot tell whether the pass was wrong or simply in the way, so the line survives every review. The toolchain's own gate reads `oxlint-disable*` and nothing else. |
|
|
235
|
+
|
|
236
|
+
### K — Retro-propagation
|
|
237
|
+
|
|
238
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
239
|
+
| ---- | --------------------------------------------------------------------------- | ------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
240
|
+
| K2 | <a id="k2-cited-ids-resolve"></a>`k2-cited-ids-resolve` | meta | all | Every rule id cited in `docs/**`, `skills/**`, `README.md` or a rule message resolves to a catalogue row. | Cite a row that exists, or write the row. | A chapter citing "rule X" that resolves to nothing is worse than silence: the reader goes looking for a guard that was never written. |
|
|
241
|
+
| K3 | <a id="k3-message-anchors"></a>`k3-message-anchors` | meta | all | Every diagnostic ends with its own generated anchor, and the anchor exists in the generated chapter. | Let `withAnchors()` generate the tail; never type one. | The anchor is the only route most readers ever take into the catalogue, and forty hand-written tails drifted the first time a heading moved. |
|
|
242
|
+
| K4 | <a id="k4-reach-per-path"></a>`k4-reach-per-path` | meta | all | The REACH of every row is proven on one fixture project laid out with every path role, judged by a single oxlint run. | Add the path to the reach fixture and state what each rule says about it. | A stated reach nothing checks is a sentence: the gates that drifted from their rows were the ones no fixture ever put a file in front of. |
|
|
243
|
+
| K5 | <a id="k5-english-catalogue"></a>`k5-english-catalogue` | meta | all | No catalogue sentence has gone back to French — a deny-list of the tokens the manifest actually carried. | Write the row in English, like every sentence the catalogue publishes. | A message a consumer's CI prints and a chapter their agent reads are not a private notebook, and a half-translated catalogue is worse than either. |
|
|
244
|
+
| K6 | <a id="k6-matrix-no-empty-cell"></a>`k6-matrix-no-empty-cell` | meta | specs | Every capability a facet DECLARES — a constructor option, a setup, a terminal action, a verb, a result accessor, a golden kind, a `{{token}}` family — is exercised by at least one of the package’s own test files in that facet, or its row names the reason it is not. | Write the spec, or state the reason on the capability row — a blank cell says nothing and a zero with no reason is a surface nobody holds. | A suite proves what it runs, never what it does not: a facet can grow a method and be green forever without one test touching it. |
|
|
245
|
+
| K7 | <a id="k7-siblingless-module-declared"></a>`k7-siblingless-module-declared` | meta | module | A source module with no `<file>.test.ts` beside it falls into one declared kind, and the kind states where its proof is instead. | Write the sibling test, or write the kind that claims it and the one line saying what covers it. | I2 says a module test lives beside its module; it never said every module has one. What was missing is the list of the ones that do not, and why. |
|
|
246
|
+
| K1 | <a id="k1-retro-propagation"></a>`k1-retro-propagation` | process | all | Every defect class discovered produces, in the same change, the guard that stops it coming back (a static rule, a meta-test or a runtime error) — or documents why no channel can hold it. | Write the guard in the same change, or write down why no channel can hold it. | This is the rule that grows the other channels instead of letting them rot. |
|
|
247
|
+
|
|
248
|
+
### M — Doubles, time & the proof of a seam
|
|
249
|
+
|
|
250
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
251
|
+
| ---- | ----------------------------------------------------------------------- | -------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
252
|
+
| M3 | <a id="m3-no-vi-time-or-global-stub"></a>`m3-no-vi-time-or-global-stub` | upstream | tests | `vi.stubGlobal`, `vi.useFakeTimers`, `vi.setSystemTime` and `vi.useRealTimers` are refused by `vitest/no-restricted-vi-methods`, each with the primitive that replaces it — a subject that fetches a relative URL states its base with `intercept(contracts, { origin })`. `vi.stubEnv` is sanctioned; `vi.mock`/`vi.doMock` stay on I4, whose allow-list the option cannot express. One escape is sanctioned, because the option carries no allow-list and the seam does not answer yet: a subject whose OWN code calls `response.body.cancel()` on an intercepted reply hangs under msw's node interceptor, so that one test keeps `vi.stubGlobal('fetch')` behind an `oxlint-disable-next-line vitest/no-restricted-vi-methods` written on the call, naming the defect (chapter 16, § A cancelled body does not settle). It leaves with the seam fix. | Reach for `intercept()` and `clock` — both give back what they took at the end of the scope. | A stubbed global is the network replaced by a function the test wrote, and a fake timer taken without `using` outlasts the test that took it. |
|
|
253
|
+
| M2 | <a id="m2-mockof-requires-the-port"></a>`m2-mockof-requires-the-port` | type | tests | `mockOf<Port>()` needs its port: `T` is constrained to `object` with no default, so a double built without one has no surface and every call on it is refused. | State the port: `mockOf<PaymentGateway>()`. | The type argument IS the contract the double stands for; without it the double agrees with anything. |
|
|
254
|
+
| M1 | <a id="m1-constructor-has-a-tree"></a>`m1-constructor-has-a-tree` | meta | all | Every constructor has a `specs/<facet>/` tree in this package. Two subjects answer differently, each with its reason stated: `component` is a CHAIN, not a constructor, and is proven beside its fixture app under `specs/component-app/`; `mobile` has no tree at all, because CI provisions no simulator. | Specify the facet on the package itself before a rule presumes it. | A facet the framework never ran against itself is a surface nobody has met, and P10 says the seam is proven before the rule wave. |
|
|
255
|
+
|
|
256
|
+
### W — Website & mobile specs
|
|
257
|
+
|
|
258
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
259
|
+
| ---- | ------------------------------------------------------------------------------- | -------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
260
|
+
| W1 | <a id="w1-scenario-pure"></a>`w1-scenario-pure` | statique | tests | A scenario (`.visit()` on website, `.open()` on mobile, `.render()` on component) is the When: the visitor acts and the capture reflects the settled state; no `expect()` in the callback — assertions live in the Then, on the returned result. | Move the assertion out of the callback, onto the result the terminal action resolves to. | Separating interaction from assertion keeps the setup → action → result grammar intact and the scenarios replayable. |
|
|
261
|
+
| W2 | <a id="w2-testid-states-what-is-missing"></a>`w2-testid-states-what-is-missing` | statique | tests | In a test file, a scenario's elements are user-facing (`button`, `link`, `field`, `heading`, `content`; on mobile `button`, `field`, `content`). `testId()` is the one escape hatch, and the line STATES what the element lacks: a `// testId: <what is missing>` comment on the call's own line or the one directly above. It is an invariant, not a rationale. | Write `// testId: <no accessible name \| no role \| …>` on the call’s line or the one above — or name the element with `button()`/`link()`/`field()`/`heading()`/`content()`. | Testing what the user sees (roles, labels) keeps specs robust to DOM rewrites; a test id sidesteps that guarantee, and without the invariant written down nobody can tell whether the hatch is still needed. |
|
|
262
|
+
| W5 | <a id="w5w-scenario-settles"></a>`w5w-scenario-settles` | statique | specs | A scenario under `specs/` that ACTS (`click`, `fill`, `select`, `check`, `press`, `tap`, `rerender`) on the visitor it was handed ends on `visitor.see(…)`, `visitor.gone(…)` or `visitor.unmount()` — through the block, branch, loop or `.then()` it ends in. A scenario that only reads is out of reach, and so is a component test, whose action often produces a CALL rather than a screen. | End on `visitor.see(<what the action produced>)` or `visitor.gone(<what it removed>)`. | The capture is taken when the callback returns: a scenario ending on a click hands the golden whatever was on the screen at that instant, and the failure reads as flakiness rather than as a missing wait. |
|
|
263
|
+
| W3 | <a id="w3-unambiguous-element"></a>`w3-unambiguous-element` | runtime | tests | An element descriptor must designate exactly ONE element when a verb ACTS on it (`click`/`tap`/`fill`): if several match, the action is refused with an error enumerating the candidates and offering the rewrites (`within(...)`, another descriptor). The framework never acts on "the first". A name designates the WHOLE accessible name since 16.0 — `{ exact: false }` is the opt-out — and a descriptor that finds nothing whole but would have found something as a substring is still RESOLVED as a substring through 16.x, level by level, with a runtime warning naming the whole name to write, once per descriptor; in 17.0 it is simply not found. It holds on both scenario facets, website and mobile; on mobile, `see()` — which acts on nothing — is satisfied by any visible match (the XCUITest tree legitimately duplicates a label between a container and its child). | Scope it with `within(<landmark>, …)`, or name the element with a descriptor that designates one thing. | Acting on the first match makes a spec green while the visitor clicks something else, and nothing ever says so — an ambiguity is a writing fault, not a case to be settled by DOM order. The counting exists only at run time: no static analysis can do it, which is why the channel is runtime. |
|
|
264
|
+
| W6 | <a id="w6-verb-subset-per-facet"></a>`w6-verb-subset-per-facet` | type | tests | The verb set is the boundary between the facets: `goto` is the page's, `rerender`/`unmount` the component's, `tap` the screen's, and `focused`/`disabled`/`selected`/`valued` reach only where `see` and `gone` do. | Reach for the verb the facet offers; a missing one is a decision, not an omission. | One vocabulary across three facets is memorable only while each facet refuses the words it cannot mean. |
|
|
265
|
+
|
|
266
|
+
## api — the HTTP app, met through its entry
|
|
267
|
+
|
|
268
|
+
### C — Files & folders
|
|
269
|
+
|
|
270
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
271
|
+
| ---- | ----------------------------------------------------------- | -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
272
|
+
| C2 | <a id="c2-http-only-requests"></a>`c2-http-only-requests` | statique | ground | `_requests/` holds `.http` files only; any other extension is an error. | Write the request as a complete `.http` document, or move the file to `_fixtures/`. | A request input is a complete `.http` — one format per folder forbids ad-hoc ones. |
|
|
273
|
+
| C3 | <a id="c3-expected-http-shape"></a>`c3-expected-http-shape` | runtime | ground | An `_expected/*.http` starts with a status line, and its headers are matched as a SUBSET: the ones listed must match, unlisted response headers are unconstrained. | List the headers the case is about; leave the rest out. | A response carries headers no test is about (dates, lengths, proxies); matching them all would make every golden a record of the machine that wrote it. |
|
|
274
|
+
|
|
275
|
+
### D — Assertions
|
|
276
|
+
|
|
277
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
278
|
+
| ---- | --------------------------------------------------------------- | -------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
279
|
+
| D12 | <a id="d12w-response-body-probe"></a>`d12w-response-body-probe` | statique | tests | A test accumulating a CLUSTER of raw probes on `.response.body` (≥ `threshold`, default 3; a variable cast from `.response.body` counts its reads) → warning: this case wants a full golden (`expect(result.response).toMatch('case.http')`). One or two probes stay silent (a legitimate scalpel). | Golden the response: `expect(result.response).toMatch('<case>.http')`, with tokens for the parts that move. | A full golden captures the whole shape and its token grammar; a cluster of raw probes replaces it with ad-hoc checks that drift (it mechanises the D11 boundary for API responses). |
|
|
280
|
+
| D15 | <a id="d15w-status-only-probe"></a>`d15w-status-only-probe` | statique | tests | A test whose ONLY assertions are HTTP status probes (`expect(X.status).toBe(N)` / `.toEqual(N)`, N a numeric literal 100–599) → warning: this case wants a full golden (`expect(result.response).toMatch('case.http')`). A status probe BESIDE a real assertion (a golden, `toMatchRows`, `toContain`…) stays silent (a legitimate scalpel). | Assert what the response carries, not only the code it came back with. | A lone status pins the response code and throws away the whole payload; a full golden captures the shape and its token grammar (it completes d12w, which needs a cluster of body probes and misses the solitary status probe). |
|
|
281
|
+
| D4b | <a id="d4b-http-first-line"></a>`d4b-http-first-line` | checker | ground | The first line of a depth-1 `.http` follows its grammar: a request line (`METHOD /path`) under `_requests/`, a status line (`HTTP/1.1 <status>`) under `_expected/`. | Write the opening line the folder calls for, or move the file to the other one. | The opening line is what tells a request from a response — constraining it catches misplaced files. |
|
|
282
|
+
| D10 | <a id="d10w-tokens-in-requests"></a>`d10w-tokens-in-requests` | checker | ground | A known token in a file under `_requests/` → warning: requests are inputs, never matched. | Write the literal value the request actually sends. | A token in an input is neither validated nor substituted — it is almost always a mistake. |
|
|
283
|
+
|
|
284
|
+
## cli — the command binary and its literate documents
|
|
285
|
+
|
|
286
|
+
### B — Spec chains
|
|
287
|
+
|
|
288
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
289
|
+
| ---- | --------------------------------------------------------- | -------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
290
|
+
| B6 | <a id="b6w-redundant-env-url"></a>`b6w-redundant-env-url` | statique | tests | `.env({ <SERVICE>_URL: ….connectionString })` repeats the framework's own injection → warning. | Drop the line: the record key already injects `<KEY>_URL` into the child process. | The injection already covers service URLs; rewriting them by hand invites drift. |
|
|
291
|
+
| B9 | <a id="b9w-product-command"></a>`b9w-product-command` | statique | all | A `specification.cli(bin)` whose binary resolves inside a dependency's `node_modules/.bin` tests the third-party tool, not the product command → warning (a suppression with a reason is accepted). | Point the runner at the product's own binary; assert on a tool's output through `result.grep`. | A spec has to exercise the product's real command; per-tool assertions go through `result.grep`. |
|
|
292
|
+
| B6 | <a id="b6-url-injection"></a>`b6-url-injection` | runtime | tests | In `cli` mode with `services`, the framework injects `<KEY>_URL` (camel-aware CONSTANT_CASE) plus the unambiguous aliases `DATABASE_URL`/`REDIS_URL`; `.env()` overrides (`null` unsets). | Read the variable the record key injects; state `.env()` only to override or unset it. | Injecting the URLs avoids the repeated hand-wiring and its drift (see b6w). |
|
|
293
|
+
|
|
294
|
+
### C — Files & folders
|
|
295
|
+
|
|
296
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
297
|
+
| ---- | --------------------------------------------------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
298
|
+
| C8 | <a id="c8-spec-registered-name"></a>`c8-spec-registered-name` | checker | document | A document's bare `env:` words and `serve:` names are keys of the `env`/`serve` objects of a `specification.cli(…)` in the nearest directory carrying `*.specification.ts` files (literals read statically; ignored otherwise). | Register the name in the specification, or fix the spelling. | A typo (`serve: dashbord`) fails at run time today, a test file later; here it is one lint line. |
|
|
299
|
+
| C16 | <a id="c16-document-outside-ground"></a>`c16-document-outside-ground` | checker | document | A `<case>.spec.yaml` is a SPEC, not ground: a document under `_expected/`, `_requests/`, `_seeds/` or `_fixtures/` is an error. | Move the document beside the test that runs it. | A document IS the scenario; filed under ground it reads as material a scenario stands on, and the reader cannot tell which files are run. |
|
|
300
|
+
|
|
301
|
+
### D — Assertions
|
|
302
|
+
|
|
303
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
304
|
+
| ---- | --------------------------------------------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
305
|
+
| D4b | <a id="d4b-spec-block-scalar"></a>`d4b-spec-block-scalar` | checker | document | `stdout`, `stderr`, `stdin` and `files.*.equals` of a `<case>.spec.yaml` are written as block scalars (`\|` keeps the trailing newline, `\|-` drops it), never as a quoted string carrying `\n`. Fixable (`--fix`). | Run `jterrazz-test-check <root> --fix`; it rewrites the scalar. | Expected output has to look like output — a golden on one line is unreadable and no diff can show it. |
|
|
306
|
+
| D4b | <a id="d4b-spec-key-order"></a>`d4b-spec-key-order` | checker | document | The keys of a `<case>.spec.yaml` follow the canonical order — `kind, description, fixture, env, serve, runs` at document level, `command, stdin, timeout, waitFor, exit, stdout, stderr, files` inside a run. Fixable (`--fix`). | Run `jterrazz-test-check <root> --fix`; it rewrites the order. | The setting before the session, the input before the output: one order makes documents comparable and removes the diffs that change nothing. |
|
|
307
|
+
| D4b | <a id="d4b-spec-shape"></a>`d4b-spec-shape` | checker | document | A `<case>.spec.yaml` document follows its grammar: closed keys at document level (`kind`, `description`, `fixture`, `env`, `serve`, `runs`) and inside a run (`command`, `stdin`, `timeout`, `waitFor`, `exit`, `stdout`, `stderr`, `files`), `description:` and `runs:` required, `command:` and `exit:` required in every run, `exit:` an integer literal, `waitFor:` only on the last run and never with `stdin:`, a `files:` path relative and under the workdir. | Write the key the grammar names; the message prints the accepted set. | The grammar is read by the runner's own parser: the document lint accepts is exactly the one the runner runs. |
|
|
308
|
+
| D5 | <a id="d5-spec-volatile-literal"></a>`d5-spec-volatile-literal` | checker | ground | No expected output carries a literal volatile value: a loopback origin with a port (`127.0.0.1:8080`, `localhost:3000`), an absolute temporary path (`/tmp/`, `/private/tmp/`, `/var/folders/`) or a home path (`/Users/…`, `/home/…`). Both halves of the ground are judged by the one list — an expected stream of a `<case>.spec.yaml`, and every text file under `_expected/`. A `{{token}}` is not a literal, and neither is a value the DOCUMENT pinned: in a stream, a word the same document states in a command argument, an `env:`/`serve:` value, a `stdin:`, a `fixture:` it names or its own sibling `_fixtures/` is excluded before it is judged. | Write the token the message names — `{{workdir}}`, `{{url}}` — in place of the literal. | This is exactly the shape a `TEST_UPDATE` run would have tokenised: finding one means a token was overwritten by hand. |
|
|
309
|
+
| D5 | <a id="d5w-spec-pinned-value"></a>`d5w-spec-pinned-value` | checker | document | A literal ISO-8601 timestamp or uuid in an expected stream of a `<case>.spec.yaml` → warning, unless the same value appears in a `fixture:`, the document's sibling `_fixtures/`, a `stdin:`, a command argument or an `env:`/`serve:` value of the same document (it is then pinned, not volatile). Under `_expected/` the same class waits for D21w, whose criterion chapter 18 carries. | Token it, or seed it — a value the document pins is a value it may assert. | The defect is not the literal: it is the literal nothing pinned. |
|
|
310
|
+
| D11 | <a id="d11w-spec-silent-refusal"></a>`d11w-spec-silent-refusal` | checker | document | A run whose `exit:` is non-zero with neither `stdout:` nor `stderr:` → warning: the document does not say why the command refused. | Write the refusal the command prints — that is what the case proves. | A silent refusal is either a defect in the product, or a golden that forgot the words went to stderr. |
|
|
311
|
+
|
|
312
|
+
### E — Environment & runner configuration
|
|
313
|
+
|
|
314
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
315
|
+
| ---- | ------------------------------------------------------------------------------- | -------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
316
|
+
| E8 | <a id="e8-literate-specification-exists"></a>`e8-literate-specification-exists` | statique | config | `literate.specification`, resolved from the config directory, names a file that exists. The config directory is what the path is read against; a project that moves Vite’s `root` states its door relative to the config all the same. | Point it at the `*.specification.ts` that exports `cli`. | A path that resolves to nothing fails at collection time with a message about an import, several layers from the line that caused it. |
|
|
317
|
+
|
|
318
|
+
### J — Hygiene
|
|
319
|
+
|
|
320
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
321
|
+
| ---- | ------------------------------------------------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
322
|
+
| J3 | <a id="j3w-spec-empty-assertion"></a>`j3w-spec-empty-assertion` | checker | document | An expected stream whose whole content is `{{any}}` → warning: the run executes and proves nothing. | Regenerate with `TEST_UPDATE=1` and keep tokens only for the parts that move. | The J3 mirror for documents — an assertion that accepts anything is an absent assertion. |
|
|
323
|
+
| J4 | <a id="j4-spec-description-unique"></a>`j4-spec-description-unique` | checker | document | Two `<case>.spec.yaml` documents in one directory do not share a `description:`. | Name what each case proves; two titles that read alike leave two files to open. | The reporter names the title: two identical titles leave two files to open. |
|
|
324
|
+
| J5 | <a id="j5-spec-description"></a>`j5-spec-description` | checker | document | A document's `description:` is a title: one line, lowercase initial (an all-caps identifier exempted), no trailing period, under 100 characters. | Write it as a title, not as a sentence. | The description IS the vitest title — a title's rules apply to it, not a paragraph's. |
|
|
325
|
+
|
|
326
|
+
## component — a rendered unit, in a page
|
|
327
|
+
|
|
328
|
+
### E — Environment & runner configuration
|
|
329
|
+
|
|
330
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
331
|
+
| ---- | ----------------------------------------------------------------------------------- | -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
332
|
+
| E5 | <a id="e5-no-simulated-dom"></a>`e5-no-simulated-dom` | statique | tests | A test file declares no simulated DOM: the `@vitest-environment happy-dom\|jsdom` pragma is an error. Reach: the roles that RUN a test (`module`, `component`, `specification`) — a project config is E5b’s. | A rendered thing is a `.test.tsx` beside its component, collected by the `component()` project. | A simulated DOM behaves ALMOST like a browser, and what ships is judged by a real one: the component facet renders in the Chromium the website facet already drives. |
|
|
333
|
+
| E6 | <a id="e6-component-project-helper"></a>`e6-component-project-helper` | statique | config | A browser project comes from `component()`: in a vitest config, a `browser` key under `test` that is not inside a `component(...)` call is an error. A `browser` key elsewhere (a `define`, an env registration) is not a project block and stays out of reach. | Replace the block with `component({ vite })` and keep only what belongs to the project. | The provider pinned to the runner's exact version, the service worker served from the framework's own install, the JSX transform the current Vite uses, a cold cache's pre-bundling and the artefact directories are all runs that pass here and fail on the next machine. |
|
|
334
|
+
| E5b | <a id="e5b-no-simulated-dom-config-member"></a>`e5b-no-simulated-dom-config-member` | checker | member | `environment: 'happy-dom'\|'jsdom'` in a `vitest.config.*` is an error. The MEMBER pass is the one channel that holds it: a config is not always a file the repository's oxlint configuration collects, while the member pass starts from the root manifest and reaches every declared member — and one finding is one line in one report. | Remove `environment` and collect the rendered tests with `component()`. | The static pass sees only what the repository's oxlint configuration hands it; the member pass starts from the root manifest and reaches every declared member. |
|
|
335
|
+
|
|
336
|
+
### G — Infrastructure
|
|
337
|
+
|
|
338
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
339
|
+
| ---- | --------------------------------------------------------------- | -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
340
|
+
| G4 | <a id="g4-no-dom-in-module-test"></a>`g4-no-dom-in-module-test` | statique | module | A module test touches no DOM global: `document`, `window`, `navigator`, `HTMLElement` referenced in VALUE position in a `module`-role file OUTSIDE `specs/` is an error. A name in type position (`x as HTMLElement`) and a `typeof window` guard reach no document and stay out of reach. | A rendered thing is a `.test.tsx` beside its component, collected by `component()`. | A module test runs under node, where `document` does not exist: a test asking for one is not testing a module, it is rendering something — and it now has somewhere to go. |
|
|
341
|
+
|
|
342
|
+
## jobs — what a name triggers in-process
|
|
343
|
+
|
|
344
|
+
### B — Spec chains
|
|
345
|
+
|
|
346
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
347
|
+
| ---- | ----------------------------------------------- | -------- | ----- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
348
|
+
| B8 | <a id="b8-kebab-trigger"></a>`b8-kebab-trigger` | statique | tests | `.trigger(name)` takes a stable kebab-case identifier; a non-kebab-case `name` is an error. | Write the job name in kebab-case, the same spelling the application registers it under. | The job name is a contract between the app and its tests — a stable identifier forbids divergent translations. |
|
|
349
|
+
|
|
350
|
+
## mobile — a native screen on a simulator
|
|
351
|
+
|
|
352
|
+
### W — Website & mobile specs
|
|
353
|
+
|
|
354
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
355
|
+
| ---- | ----------------------------------------------------------------- | ------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
356
|
+
| W4 | <a id="w4-no-landmarks-on-mobile"></a>`w4-no-landmarks-on-mobile` | runtime | tests | The element vocabulary is ONE between the website and mobile facets; an ARIA landmark (`main()`, `navigation()`…) passed to a mobile verb is refused at run time — an iOS screen has no ARIA regions; scope with `within(testId(…), …)`. | Scope with `within(testId(…), …)`: a screen has no landmarks, and any descriptor works as the scope. | One vocabulary keeps the API memorable; refusing the part with no iOS equivalent outright avoids a silent approximation. |
|
|
357
|
+
|
|
358
|
+
<!-- /GENERATED:catalog -->
|
|
359
|
+
|
|
360
|
+
Conventions with no oxlint rule are covered by another channel — the catalogue above names each one: the **checker step** (D4 / D4b / D10, below), the **cross-file checker passes** (C9, B5-by-inference, A7), and the **runtime / process** channels (D7, D11, K1). The **type channel** (C4 trigger members, D1 read-only accessors) carries no catalogue row — the type system enforces it without a rule.
|
|
361
|
+
|
|
362
|
+
Rules marked _fs-checked_ probe the filesystem, anchored on files oxlint is already visiting (a feature's sibling `requests/`, `seeds/`, `expected/`, a module test's neighbour), with per-run caching — they never scan the tree blindly.
|
|
363
|
+
|
|
364
|
+
## Writing B4 markers under `--fix`
|
|
365
|
+
|
|
366
|
+
B4 asks every test for a `// Given -` line and a `// Then -` line, in that order. A marker is a comment, and two rules of the base preset rewrite comments when the fixer runs — either can turn a well-placed marker into something that means nothing, with the lint green afterwards, because the marker text is still in the file. Whether either rule is on, and in which mode, is `@jterrazz/typescript`'s rulebook to state; what they do to a marker is this chapter's.
|
|
367
|
+
|
|
368
|
+
**A marker goes between STATEMENTS, never between two declarators of one chain.** `one-var` exists in oxlint from **1.83** — 1.74 answers `Rule 'one-var' not found in plugin 'eslint'` — and only its `always` mode arms the hazard. Asked for one `const` statement per scope, the fixer satisfies it by FUSING adjacent declarations, swallowing whatever comment stood between them into the chain, where it now separates two declarators instead of two steps of the test:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
// Given - the seeded row
|
|
372
|
+
const before = await cli.exec('seed');
|
|
373
|
+
// Then - the row the command wrote
|
|
374
|
+
const after = await cli.exec('read');
|
|
375
|
+
|
|
376
|
+
// After `oxlint --fix` — the marker has been pulled inside a declaration chain
|
|
377
|
+
const before = await cli.exec('seed'),
|
|
378
|
+
// Then - the row the command wrote
|
|
379
|
+
after = await cli.exec('read');
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
B4 still passes on the second form, which is what makes it worth knowing: nothing fails, the narration has simply stopped separating anything. The rule's `never` mode is the opposite hazard-free shape — one declarator per statement, so its fixer splits chains and never moves a comment.
|
|
383
|
+
|
|
384
|
+
**A marker is EXACTLY one line.** `capitalized-comments` capitalises the first word of every `//` line, a wrapped continuation included, so a marker spilling onto a second line is capitalised mid-sentence:
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
// Given - a runner primed with a fixture and a seeded database,
|
|
388
|
+
// and the command line that asks for the second shop
|
|
389
|
+
|
|
390
|
+
// After `oxlint --fix`
|
|
391
|
+
// Given - a runner primed with a fixture and a seeded database,
|
|
392
|
+
// And the command line that asks for the second shop
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
`@jterrazz/typescript` v10 turns that rule off — 198 of 198 hits on real estate code were false positives on wrapped `//` prose — so the rewrite stops happening under the preset. The one-line discipline outlives it anyway: a marker is the title of a step, and reasoning that does not fit on one line belongs in a docblock above the test, never in the marker.
|
|
396
|
+
|
|
397
|
+
## The conventions checker (D4)
|
|
398
|
+
|
|
399
|
+
Oxlint only parses JS/TS. The D4 token grammar also constrains **data fixtures** under `requests/**` and `expected/**`, so that channel ships as a separate step, bundled as `dist/checker.js` and exposed as the `jterrazz-test-check` bin:
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
jterrazz-test-check specs # one specs tree
|
|
403
|
+
jterrazz-test-check apps/console # a project root: every specs tree below it
|
|
404
|
+
jterrazz-test-check --member packages/web # one workspace member
|
|
405
|
+
jterrazz-test-check --format json # every root, every member
|
|
406
|
+
npx jterrazz-test-check specs # or `node node_modules/@jterrazz/test/dist/checker.js specs`
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**A path is read for what it IS.** A directory with a `specs` segment in its
|
|
410
|
+
path is a tree, walked as one. Anything else is a project root, and takes the
|
|
411
|
+
path-less run anchored there — every `specs/` tree the workspaces declare
|
|
412
|
+
below it, plus every member. The tree passes that read the FIRST level of what
|
|
413
|
+
they are handed (C12, C18, C20) need the anchor to be the tree: pointed at a
|
|
414
|
+
project root they used to find no facet folder there and report nothing, so
|
|
415
|
+
`jterrazz-test-check .` passed silently on a tree `jterrazz-test-check specs`
|
|
416
|
+
failed. A named root holding no `specs/` tree at all is refused with a non-zero
|
|
417
|
+
exit, because the run the operator asked for had nothing to walk.
|
|
418
|
+
|
|
419
|
+
What it checks:
|
|
420
|
+
|
|
421
|
+
- **D4** — every `{{token}}` in an `expected/` fixture belongs to the frozen vocabulary. It scans **all text files** under `expected/` (not just `.http`/`.json`/`.txt` — it decodes UTF-8 and skips binary), and flags **malformed captures** of a known kind (`{{iso8601#}}`, `{{uuid #id}}`).
|
|
422
|
+
- **D4b** — depth-1 `requests/*.http` must start with a request line (`METHOD /path`); depth-1 `expected/*.http` with a status line (`HTTP/1.1 <status>`).
|
|
423
|
+
- **D10** (warning) — a `{{token}}` inside a `requests/` file: requests are inputs, never matched, so a token there is almost always a mistake. Advisory only — it does not fail the run.
|
|
424
|
+
- **the tree passes** — what a folder says about itself, which no single file can answer for: a facet folder holds the runner its name promises (C20), a spec under it reaches that runner (C18), a `.test.ts` under a facet folder wears the wrong word and a `.spec.ts` outside every specs tree wears the other one (C12, fixable), a document is never filed under ground (C16), and ground a single spec of a leaf reads belongs beside that spec (C21w, warning).
|
|
425
|
+
- **J9** — every `checker-disable-next-line` / `checker-disable-line` carries its ` -- <reason>`. The toolchain's own suppression gate reads `oxlint-disable*` and nothing else, so this channel carries its own.
|
|
426
|
+
|
|
427
|
+
It shares `TOKEN_KINDS` with the runtime matcher (the two cannot drift), skips `fixtures/` trees (verbatim `.fixture()` cwd material — file state, not assertions), and ignores non-identifier braces (`{{.Server.Version}}`-style template output). Exit 1 on any error, 0 when only warnings remain.
|
|
428
|
+
|
|
429
|
+
## The member pass, and the JSON contract
|
|
430
|
+
|
|
431
|
+
Three findings are about a MEMBER rather than about a spec tree, and a member with no `specs/` root has to be judged all the same — the first of the three is precisely "this member has tests and no configuration for them".
|
|
432
|
+
|
|
433
|
+
| Id | Finds |
|
|
434
|
+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
435
|
+
| E3 | a member with tests of its OWN — a `test` script that does not delegate, or a `*.test.ts(x)` outside another package's tree — and no `vitest.config.*` |
|
|
436
|
+
| E5b | a `vitest.config.*` naming `environment: 'happy-dom'` or `'jsdom'` |
|
|
437
|
+
| F8 | a dependency the framework carries (`msw`, `vitest-mock-extended`; `yaml` in devDependencies only) or a seam it replaced |
|
|
438
|
+
|
|
439
|
+
A workspace ROOT is a member like any other, and owes a config only for tests that are its own: the walk stops at the next `package.json`, and a `test` script that hands the work to the members (`--workspaces`, `pnpm -r`, `turbo run`) is the members' tests, not the root's. `yaml` is read from devDependencies alone — in `dependencies` it is the product's own YAML library, which the framework never ships to it.
|
|
440
|
+
|
|
441
|
+
F8 says which of the two it found: a carried dependency is "a transitive of `@jterrazz/test` — remove the declaration", a retired seam is "a seam `@jterrazz/test` replaced — the facet is `component()`, `website()`, `clock` or `intercept()`". Removing a transitive changes nothing a test can see; dropping a retired seam means writing its facet.
|
|
442
|
+
|
|
443
|
+
**The jest allowance follows the tests, not the manifest.** `@testing-library/react-native` stands where jest runs the member's tests, and jest is not always the member's own: a library co-locating its render tests in `src/` while the app beside it owns the only working React Native toolchain runs them from THAT app's `jest.config.*`. So the pass reads two sources — the member's own declaration (a `jest`/`jest-expo` dependency, a `jest` field, a `test` script calling it), and a `jest.config.*` anywhere in the workspace whose `roots`/`testMatch` resolve INTO the member (`<rootDir>` is jest's own token for the config's directory). A config that reaches another member clears nothing here: the allowance is about the files jest collects.
|
|
444
|
+
|
|
445
|
+
`jterrazz-test-check --member <dir>` runs them over one member. `jterrazz-test-check` with NO path runs both halves over the whole project: every `specs/` root discovered from the root manifest's `workspaces`, and every member. That is the run the toolchain's ratchet rests on, so the contract is exactly "a path-less run reports what the per-root and per-member runs report" — and the discovery is the toolchain's, to the segment: a `workspaces` glob expands with `*` for one segment and `**` for any depth (bounded at six), and each member is walked for the spec roots it owns, a nested `web/specs` included. A shape `@jterrazz/typescript` reaches and this run did not would be a finding the ratchet could neither record nor clear.
|
|
446
|
+
|
|
447
|
+
`--format json` prints `[{ code, file, line, severity, message }]` on stdout and nothing else. `code` is `jterrazz-check(<id>)` — `jterrazz-check(d4)`, `jterrazz-check(f8)` — which is the namespace `@jterrazz/typescript`'s ratchet records the counts under, flat, in `oxlint.baseline.json` beside the oxlint ones. Severity travels but is never read by the ratchet: a finding a release ships at `warn` is exactly the debt a ratchet exists to hold down.
|
|
448
|
+
|
|
449
|
+
The toolchain's side of this — when the pass runs, which install it resolves the binary from, and what the floor version is — is `@jterrazz/typescript` `docs/06-quality-checks.md` § The Test Conventions pass.
|
|
450
|
+
|
|
451
|
+
## Cross-file checker passes
|
|
452
|
+
|
|
453
|
+
The same binary also runs the analyses oxlint structurally cannot — those that need to read **two files at once** (a `*.specification.ts` record and the `specs/` tests importing it) or a **whole feature tree** at once. They live in the checker channel and follow its doctrine: **precision over recall** — a heuristic that cannot decide stays silent (under-reports) rather than block a legitimate shape. All are string/scan based (no TS parser in the light lint bundle). A `$FIXTURES` pool (`specs/_fixtures`) is pruned from the per-feature walks (verbatim fixture material, including the on-purpose violation trees) and inspected only at its top level.
|
|
454
|
+
|
|
455
|
+
- **C9 — dead fixtures & orphan dirs.** The mirror of C8: a fixture file no test literal references is dead weight, and a domain directory with conventional subdirs but no `*.test.ts` is an orphan. A referenced `expected/<name>/` (or `fixtures/<name>/`) tree counts entirely as used; a `$FIXTURES` pool entry unreferenced across all of `specs/**` is dead. A non-literal fixture argument (a template with an expression, a variable) makes the reference set incomplete, so that feature is downgraded to **warning**.
|
|
456
|
+
- **B5 — await-using (inference).** The **primary** B5 channel. It reads the docker-aware runners straight from the `docker:` option of the imported spec file, then flags every `<runner>….exec()` result bound without `await using`. No hand-maintained runner list — unlike the oxlint rule, which stays for explicitly-configured runners.
|
|
457
|
+
- **A7 — database property.** A spec's `services:` record fixes how many SQL databases exist; the importing tests must (≥ 2) or must not (== 1) pass `{ database }` to every `.seed()` / `.table()`. Non-literal records (a handle variable in `services`) and files importing more than one enforced runner are skipped (ambiguous). Doubled by the runtime channel (the framework throws).
|
|
458
|
+
|
|
459
|
+
## The generated catalogue (docs-as-code)
|
|
460
|
+
|
|
461
|
+
The rule catalogue is **generated from the code**, not hand-maintained. Each rule carries its normative sentence as `meta.docs` (sourced from `src/lint/manifest.ts`), and the manifest assembles all seven channels — statique (the oxlint rules), upstream (the options this vocabulary sets on rules the `vitest` plugin owns), checker (the passes above), runtime (execution-time refusals), type (what the compiler refuses, proven in `src/type-channel.test-d.ts`), meta (what this package's own tests hold), and process (review-borne rules like D11/K1). A generator (`src/lint/catalog.ts`, bundled as `dist/catalog.js`, chained into `npm run docs` **before** `typescript docs`) renders two committed projections deterministically:
|
|
462
|
+
|
|
463
|
+
- the **full catalogue above**, spliced between GENERATED markers in this file;
|
|
464
|
+
- **[`skills/jterrazz-test/references/rules.md`](../skills/jterrazz-test/references/rules.md)** — the same set, trimmed for agents.
|
|
465
|
+
|
|
466
|
+
The narrative constitution lives in [18 — Conventions](18-conventions.md): principles, the enforcement channels, non-mechanizable criteria, and design rationales — no per-rule normative lines, so there is no duplication. A meta-test (`src/lint/plugin.test.ts`) guards the contract: **freshness** (re-running the generator reproduces both committed projections byte-for-byte) and **completeness** (every shipped rule carries `meta.docs`; every manifest entry maps to a plugin rule, a checker pass, or a documented runtime/process rule), plus the standing E2E inventory (every rule has a `specs/lint/**` spec + fixture pair).
|
|
467
|
+
|
|
468
|
+
## Doc typechecking (meta-test)
|
|
469
|
+
|
|
470
|
+
A vitest meta-test (`src/lint/docs-typecheck.ts`) extracts the ` ```typescript ` blocks from `docs/*.md` that import the framework, rewrites `@jterrazz/test` to the repo source, and runs the real `tsc --noEmit` over them — so a sample drifting from the API (a removed `.spawn()`, a renamed accessor) fails CI as a type error. Precision over coverage: only self-contained framework blocks are checked; prose fragments leaning on ambient vitest globals, and blocks importing consumer app code (`createApp`, `../../src/app.js`), are skipped.
|
|
471
|
+
|
|
472
|
+
## Suppressions
|
|
473
|
+
|
|
474
|
+
Standard oxlint disable comments work, and every suppression must carry its reason:
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// oxlint-disable-next-line jterrazz/a1-specification-file -- negative constructor spec: the runner must fail to start
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
The framework's own repo keeps its sanctioned exemptions visible: config `overrides` for the constructor unit tests (`src/**/*.test.ts` may call `specification.*` — testing the constructors is their purpose) and for `src/runner/**`, `src/seams/vitest-browser/**`, `src/model/{clock,doubles,goldens}/**` and every `<facet>.project.ts` (the runner coupling imports `vitest` by design).
|
|
481
|
+
|
|
482
|
+
The **cross-file checker passes** carry their own suppression comment (oxlint disable comments don't reach them — they are a separate binary):
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
// checker-disable-next-line a7 -- negative spec: the omitted database is the behaviour under test (runtime channel)
|
|
486
|
+
expect(() => api.seed('one-user.sql')).toThrow(/* … */);
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`// checker-disable-next-line <id>[,<id>] -- reason` suppresses the named passes (`a7`, `b5`, `c9`, or `*`) on the next non-blank line; `// checker-disable-line <id>` suppresses its own line. The text after `--` is stripped before the ids are matched, so it never affects suppression — it is required by house convention (every suppression explains itself), not by the checker. The idiomatic use is a negative spec that deliberately violates a convention to exercise the runtime channel.
|
|
490
|
+
|
|
491
|
+
## Build-before-lint
|
|
492
|
+
|
|
493
|
+
The plugin is loaded from the **built bundle**. In this repo `jsPlugins: ['./dist/oxlint.js']` means `npm run build` must precede `npm run lint` (Node's TS type-stripping cannot resolve `.js` specifiers to `.ts` sources). Consumers load `@jterrazz/test/oxlint` from `node_modules` — no ordering constraint.
|
|
494
|
+
|
|
495
|
+
## Stability
|
|
496
|
+
|
|
497
|
+
Oxlint's JS-plugin API is **alpha** (oxlint 1.74): the plugin declares the small structural slice it uses locally (`src/lint/types.ts`) instead of importing oxlint types, so upstream churn surfaces as a compile break in one file, not across the rules. Expect to re-validate the plugin on oxlint majors; the E2E specs (`specs/lint/**`, one violation/compliant fixture pair per rule) run the real binary and are the canary.
|
|
498
|
+
|
|
499
|
+
## Pitfalls
|
|
500
|
+
|
|
501
|
+
- **Suppressing instead of restructuring.** Most hits have a conventional home: an inline runner belongs in a `*.specification.ts`, shared test data in a `*.fixtures.ts`, a file-reading module test in `specs/`. Suppress only what is structurally impossible (negative constructor specs).
|
|
502
|
+
- **Forgetting the checker step.** Oxlint passing does not validate `.http`/`.json`/`.txt` fixtures — wire `jterrazz-test-check` into your lint chain.
|
|
503
|
+
- **Linting generated fixture pools.** Violation fixtures (like this repo's `specs/_fixtures/lint-violations/**`) must be excluded via `ignorePatterns` — they violate on purpose.
|
|
504
|
+
- **Stale plugin in the IDE after a rebuild (LSP staleness, not a real failure).** The oxlint language server loads `dist/oxlint.js` **into memory once** and holds it for the life of the server process. After you rebuild the plugin (`npm run build`), the editor keeps linting against the _old_ bundle: rules you just changed still fire (or fail to fire), and the in-editor squiggles disagree with a fresh CLI run. The command line is authoritative — if `npm run lint` passes, the code is clean. To resync the IDE, **restart the editor's oxlint server** (reload the window, or restart the oxlint/oxc language-server process); a plain file re-save is not enough. Suspect this whenever the IDE reports a plugin diagnostic that the CLI does not.
|
|
505
|
+
- **CJS config silently drops the plugin (loudest pitfall).** The plugin is ESM-only. If your `oxlint.config` is CommonJS (or resolves the plugin through a CJS path), oxlint **prints a load warning and then exits 0** — the plugin is simply not registered, so **none of the `jterrazz/*` rules run** and your lint stays green while enforcing nothing. There is no error to fail CI. Use an ESM config (`oxlint.config.ts`/`.mjs`), and after wiring, sanity-check that a known violation is actually reported (e.g. a `.seed('x.json')` under a `_seeds/` directory, which C7 rejects).
|
|
506
|
+
- **What the `./oxlint` `require` condition does — and does not — solve.** The `@jterrazz/test/oxlint` export ships both an `import` (`dist/oxlint.js`) and a `require` (`dist/oxlint.cjs`) condition. The `require` condition exists so a **shared preset or config module can pull the config fragment synchronously** — `const { testing } = require('@jterrazz/test/oxlint')` — instead of forcing top-level `await import(...)` (and thus a top-level-await, ESM-only preset). This is purely about how **your own preset code** reads `testing`/`recommendedRules`. It does **not** change the pitfall above: oxlint still loads the _plugin itself_ (the `jsPlugins: ['@jterrazz/test/oxlint']` entry) as ESM, and oxlint still requires _your_ `oxlint.config` to be ESM to register any JS plugin. The `require` condition makes sync wiring possible in a preset; it does not make a CommonJS oxlint config work.
|
|
507
|
+
|
|
508
|
+
## Related
|
|
509
|
+
|
|
510
|
+
[18 — Conventions](18-conventions.md) · [15 — Tokens](15-tokens.md) · [02 — Developing](02-developing.md)
|