@jterrazz/test 16.1.1 → 17.0.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 +45 -171
- package/dist/browser/index.js +2 -2980
- package/dist/browser/services.js +2 -0
- package/dist/browser/stubs.js +2 -0
- package/dist/checker.d.ts +6 -1
- package/dist/checker.js +4534 -2628
- package/dist/config.d.ts +40 -0
- package/dist/config.js +309 -0
- package/dist/in-place.global-setup.d.ts +11 -0
- package/dist/in-place.global-setup.js +21 -0
- package/dist/index.d.ts +1653 -3004
- package/dist/index.js +5226 -3845
- package/dist/mobile-session.global-setup.d.ts +14 -0
- package/dist/mobile-session.global-setup.js +31 -0
- package/dist/oxlint.cjs +6069 -5634
- package/dist/oxlint.d.cts +43 -7212
- package/dist/oxlint.d.ts +43 -7212
- package/dist/oxlint.js +6127 -5693
- package/dist/route-coverage.global-setup.d.ts +42 -0
- package/dist/route-coverage.global-setup.js +2 -0
- package/dist/services.d.ts +3 -0
- package/dist/services.global-setup.d.ts +43 -0
- package/dist/services.global-setup.js +2 -0
- package/dist/services.js +597 -0
- package/dist/shared-appium.adapter.js +428 -0
- package/dist/shared-artifacts.js +32 -0
- package/dist/shared-chapter.js +7 -0
- package/dist/shared-container.port.d.ts +31 -0
- package/dist/shared-derived-projects.js +58 -0
- package/dist/shared-device.port.d.ts +189 -0
- package/dist/shared-exchange.js +447 -0
- package/dist/shared-in-place.js +172 -0
- package/dist/shared-index.browser.js +3191 -0
- package/dist/shared-index.services.d.ts +346 -0
- package/dist/shared-index.stubs.d.ts +287 -0
- package/dist/shared-json-shape.js +17 -0
- package/dist/shared-landmarks.js +71 -0
- package/dist/shared-ledger.js +68 -0
- package/dist/shared-literate-plugin.js +183 -0
- package/dist/shared-manifest.js +1321 -0
- package/dist/shared-pause.js +11 -0
- package/dist/shared-peer.js +88 -0
- package/dist/shared-playwright.adapter.js +330 -0
- package/dist/shared-process.adapter.js +74 -0
- package/dist/shared-refusal.js +35 -0
- package/dist/shared-resolve.js +24 -0
- package/dist/shared-route-coverage.global-setup.js +187 -0
- package/dist/shared-scenario-failure.js +198 -0
- package/dist/shared-serve.js +365 -0
- package/dist/shared-service-images.js +10 -0
- package/dist/shared-services.global-setup.js +179 -0
- package/dist/shared-session-ledger.js +301 -0
- package/dist/shared-setup-entry.js +12 -0
- package/dist/shared-source-scan.js +133 -0
- package/dist/shared-spec-document.js +365 -0
- package/dist/shared-specifications.js +26 -0
- package/dist/shared-stream-body.js +130 -0
- package/dist/shared-stub-server.js +190 -0
- package/dist/shared-temp-root.global-setup.js +80 -0
- package/dist/shared-test-baseline.js +26 -0
- package/dist/shared-test-section.js +58 -0
- package/dist/shared-testcontainers.adapter.js +203 -0
- package/dist/shared-tokens.js +55 -0
- package/dist/shared-yaml-document.js +119 -0
- package/dist/stubs.d.ts +2 -0
- package/dist/stubs.js +657 -0
- package/dist/temp-root.global-setup.d.ts +7 -0
- package/dist/temp-root.global-setup.js +2 -0
- package/dist/vitest.d.ts +97 -163
- package/dist/vitest.js +373 -525
- package/docs/README.md +82 -36
- package/docs/concepts/architecture.md +118 -0
- package/docs/concepts/conventions.md +333 -0
- package/docs/guides/api-specs.md +374 -0
- package/docs/guides/cli-specs.md +564 -0
- package/docs/guides/component-specs.md +252 -0
- package/docs/guides/develop.md +94 -0
- package/docs/guides/integration-specs.md +181 -0
- package/docs/guides/job-specs.md +180 -0
- package/docs/guides/lint-specs.md +174 -0
- package/docs/guides/mobile-specs.md +319 -0
- package/docs/guides/module-tests.md +149 -0
- package/docs/guides/release.md +43 -0
- package/docs/guides/spec-documents.md +189 -0
- package/docs/guides/website-specs.md +413 -0
- package/docs/reference/assertions.md +345 -0
- package/docs/reference/glossary.md +20 -0
- package/docs/reference/linting.md +483 -0
- package/docs/reference/services.md +236 -0
- package/docs/reference/stubs.md +498 -0
- package/docs/reference/time-and-doubles.md +137 -0
- package/docs/reference/tokens.md +258 -0
- package/docs/reference/visitor.md +221 -0
- package/docs/start/getting-started.md +187 -0
- package/docs/start/set-up-a-project.md +214 -0
- package/package.json +50 -30
- package/schema/spec.schema.json +43 -17
- package/dist/appium.adapter.js +0 -489
- package/dist/artifacts.js +0 -32
- package/dist/fixtures.js +0 -110
- package/dist/literate.js +0 -1747
- package/dist/match.js +0 -804
- package/dist/playwright.adapter.js +0 -438
- package/dist/server.js +0 -160
- package/dist/stream.js +0 -321
- package/dist/substring-warning.js +0 -282
- package/docs/01-architecture.md +0 -95
- package/docs/02-developing.md +0 -351
- package/docs/03-testing.md +0 -495
- package/docs/04-operating.md +0 -101
- package/docs/05-module-tests.md +0 -152
- package/docs/06-integration.md +0 -180
- package/docs/07-component.md +0 -246
- package/docs/08-website.md +0 -404
- package/docs/09-mobile.md +0 -270
- package/docs/10-api.md +0 -264
- package/docs/11-jobs.md +0 -157
- package/docs/12-cli.md +0 -677
- package/docs/13-elements.md +0 -248
- package/docs/14-assertions.md +0 -314
- package/docs/15-tokens.md +0 -253
- package/docs/16-contracts.md +0 -438
- package/docs/17-services.md +0 -188
- package/docs/18-conventions.md +0 -332
- package/docs/19-linting.md +0 -512
- package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
- package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
- package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
- package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
- package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
- package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
- package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
- package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
- package/docs/decisions/_template.md +0 -22
- package/docs/reference/classes/Matcher.md +0 -120
- package/docs/reference/classes/ProcessHandle.md +0 -368
- package/docs/reference/functions/defineContract.md +0 -39
- package/docs/reference/functions/defineContracts.md +0 -36
- package/docs/reference/functions/disabled.md +0 -28
- package/docs/reference/functions/enabled.md +0 -20
- package/docs/reference/functions/focused.md +0 -26
- package/docs/reference/functions/mockOf.md +0 -79
- package/docs/reference/functions/postgres.md +0 -31
- package/docs/reference/functions/process.md +0 -33
- package/docs/reference/functions/redis.md +0 -28
- package/docs/reference/functions/required.md +0 -41
- package/docs/reference/functions/selected.md +0 -26
- package/docs/reference/functions/sqlite.md +0 -33
- package/docs/reference/functions/testId.md +0 -20
- package/docs/reference/functions/text.md +0 -40
- package/docs/reference/functions/valued.md +0 -28
- package/docs/reference/functions/waitUntil.md +0 -46
- package/docs/reference/functions/within.md +0 -29
- package/docs/reference/index.md +0 -196
- package/docs/reference/interfaces/BaseResult.md +0 -87
- package/docs/reference/interfaces/CallResult.md +0 -160
- package/docs/reference/interfaces/CaptureScope.md +0 -82
- package/docs/reference/interfaces/CliResult.md +0 -253
- package/docs/reference/interfaces/ContainerAccessor.md +0 -166
- package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
- package/docs/reference/interfaces/FetchResult.md +0 -180
- package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
- package/docs/reference/interfaces/HttpResult.md +0 -126
- package/docs/reference/interfaces/JsonAccessor.md +0 -46
- package/docs/reference/interfaces/PageResult.md +0 -356
- package/docs/reference/interfaces/RenderResult.md +0 -72
- package/docs/reference/interfaces/RenderedText.md +0 -110
- package/docs/reference/interfaces/ResponseAccessor.md +0 -47
- package/docs/reference/interfaces/ScreenResult.md +0 -128
- package/docs/reference/interfaces/TableAccessor.md +0 -26
- package/docs/reference/interfaces/TextAccessor.md +0 -96
- package/docs/reference/type-aliases/ApiHandle.md +0 -68
- package/docs/reference/type-aliases/ApiSpecification.md +0 -213
- package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
- package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
- package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
- package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
- package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
- package/docs/reference/type-aliases/BrowserPage.md +0 -132
- package/docs/reference/type-aliases/BrowserPort.md +0 -51
- package/docs/reference/type-aliases/CliEnv.md +0 -11
- package/docs/reference/type-aliases/CliHandle.md +0 -80
- package/docs/reference/type-aliases/CliInput.md +0 -36
- package/docs/reference/type-aliases/CliOutput.md +0 -46
- package/docs/reference/type-aliases/CliPort.md +0 -58
- package/docs/reference/type-aliases/CliSpecification.md +0 -144
- package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
- package/docs/reference/type-aliases/ComponentChain.md +0 -156
- package/docs/reference/type-aliases/ComponentScenario.md +0 -20
- package/docs/reference/type-aliases/ComponentUi.md +0 -47
- package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
- package/docs/reference/type-aliases/ContainerPort.md +0 -113
- package/docs/reference/type-aliases/Contract.md +0 -68
- package/docs/reference/type-aliases/ContractInput.md +0 -13
- package/docs/reference/type-aliases/ContractRequest.md +0 -95
- package/docs/reference/type-aliases/ContractResponder.md +0 -23
- package/docs/reference/type-aliases/ContractResponse.md +0 -78
- package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
- package/docs/reference/type-aliases/Contracts.md +0 -48
- package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
- package/docs/reference/type-aliases/DatabasePort.md +0 -72
- package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
- package/docs/reference/type-aliases/DevicePort.md +0 -51
- package/docs/reference/type-aliases/DeviceScreen.md +0 -32
- package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
- package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
- package/docs/reference/type-aliases/DomMount.md +0 -21
- package/docs/reference/type-aliases/ElementKind.md +0 -27
- package/docs/reference/type-aliases/ElementMatch.md +0 -74
- package/docs/reference/type-aliases/ElementOptions.md +0 -29
- package/docs/reference/type-aliases/ElementRef.md +0 -128
- package/docs/reference/type-aliases/ExecOptions.md +0 -37
- package/docs/reference/type-aliases/FileAccessor.md +0 -54
- package/docs/reference/type-aliases/HonoApp.md +0 -31
- package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
- package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
- package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
- package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
- package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
- package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
- package/docs/reference/type-aliases/Intercept.md +0 -19
- package/docs/reference/type-aliases/InterceptOptions.md +0 -28
- package/docs/reference/type-aliases/InterceptScope.md +0 -11
- package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
- package/docs/reference/type-aliases/JobHandle.md +0 -34
- package/docs/reference/type-aliases/JobsHandle.md +0 -57
- package/docs/reference/type-aliases/JobsResult.md +0 -16
- package/docs/reference/type-aliases/JobsSpecification.md +0 -94
- package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
- package/docs/reference/type-aliases/LandmarkKind.md +0 -20
- package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
- package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
- package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
- package/docs/reference/type-aliases/MatchableRequest.md +0 -61
- package/docs/reference/type-aliases/MatcherKind.md +0 -34
- package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
- package/docs/reference/type-aliases/MobileElementKind.md +0 -14
- package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
- package/docs/reference/type-aliases/MobileElementRef.md +0 -20
- package/docs/reference/type-aliases/MobileHandle.md +0 -65
- package/docs/reference/type-aliases/MobileScenario.md +0 -20
- package/docs/reference/type-aliases/MobileSpecification.md +0 -51
- package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
- package/docs/reference/type-aliases/MobileVisitor.md +0 -81
- package/docs/reference/type-aliases/MockOfOptions.md +0 -26
- package/docs/reference/type-aliases/MockPort.md +0 -10
- package/docs/reference/type-aliases/PinnedClock.md +0 -10
- package/docs/reference/type-aliases/PostgresOptions.md +0 -32
- package/docs/reference/type-aliases/ProcessOptions.md +0 -112
- package/docs/reference/type-aliases/RedisOptions.md +0 -20
- package/docs/reference/type-aliases/RenderSubject.md +0 -12
- package/docs/reference/type-aliases/ScreenNode.md +0 -67
- package/docs/reference/type-aliases/ServerPort.md +0 -36
- package/docs/reference/type-aliases/ServerResponse.md +0 -46
- package/docs/reference/type-aliases/ServerSpec.md +0 -21
- package/docs/reference/type-aliases/ServiceHandle.md +0 -216
- package/docs/reference/type-aliases/ServiceRecord.md +0 -12
- package/docs/reference/type-aliases/SpecDocument.md +0 -84
- package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
- package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
- package/docs/reference/type-aliases/SpecFixture.md +0 -30
- package/docs/reference/type-aliases/SpecKind.md +0 -8
- package/docs/reference/type-aliases/SpecRun.md +0 -108
- package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
- package/docs/reference/type-aliases/SpecStream.md +0 -32
- package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
- package/docs/reference/type-aliases/SqliteOptions.md +0 -36
- package/docs/reference/type-aliases/SseEvent.md +0 -58
- package/docs/reference/type-aliases/StreamBody.md +0 -61
- package/docs/reference/type-aliases/TextFilter.md +0 -22
- package/docs/reference/type-aliases/VisitScenario.md +0 -20
- package/docs/reference/type-aliases/Visitor.md +0 -215
- package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
- package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
- package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
- package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
- package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
- package/docs/reference/variables/anthropic.md +0 -19
- package/docs/reference/variables/banner.md +0 -10
- package/docs/reference/variables/button.md +0 -10
- package/docs/reference/variables/clock.md +0 -16
- package/docs/reference/variables/complementary.md +0 -10
- package/docs/reference/variables/component.md +0 -10
- package/docs/reference/variables/content.md +0 -10
- package/docs/reference/variables/contentinfo.md +0 -10
- package/docs/reference/variables/dialog.md +0 -10
- package/docs/reference/variables/field.md +0 -10
- package/docs/reference/variables/form.md +0 -10
- package/docs/reference/variables/heading.md +0 -10
- package/docs/reference/variables/http.md +0 -40
- package/docs/reference/variables/intercept.md +0 -11
- package/docs/reference/variables/link.md +0 -10
- package/docs/reference/variables/listitem.md +0 -10
- package/docs/reference/variables/main.md +0 -10
- package/docs/reference/variables/match.md +0 -8
- package/docs/reference/variables/navigation.md +0 -10
- package/docs/reference/variables/openai.md +0 -21
- package/docs/reference/variables/option.md +0 -17
- package/docs/reference/variables/region.md +0 -10
- package/docs/reference/variables/row.md +0 -10
- package/docs/reference/variables/search.md +0 -10
- package/docs/reference/variables/specification.md +0 -8
- package/docs/reference/variables/status.md +0 -10
- package/docs/reference/variables/table.md +0 -10
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
import { t as SPECIFICATIONS } from "./shared-specifications.js";
|
|
2
|
+
import { byName } from "@jterrazz/typescript/rules";
|
|
3
|
+
//#region lint/consumer/catalogue/families.ts
|
|
4
|
+
/** English, like every sentence the catalogue publishes: a meta-test holds the line with a deny-list. */
|
|
5
|
+
const FAMILIES = {
|
|
6
|
+
setup: "Constructors, configuration, projects and infrastructure",
|
|
7
|
+
chain: "The chain and its markers",
|
|
8
|
+
layout: "Files, folders, sizes, names, and the suppressions a file carries",
|
|
9
|
+
expectations: "What is compared, and how",
|
|
10
|
+
stubs: "What the outside world replies, and where the stubs are filed",
|
|
11
|
+
services: "The services a run starts, and the seeds laid in them",
|
|
12
|
+
clock: "Time: the pinned instant, sleeps and sampled values",
|
|
13
|
+
mocks: "Doubles of a port",
|
|
14
|
+
visitor: "Screens, targets and scenarios",
|
|
15
|
+
imports: "Who imports whom, and protecting production",
|
|
16
|
+
architecture: "Source layers"
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region lint/consumer/catalogue/manifest.json
|
|
20
|
+
var manifest_default = {
|
|
21
|
+
"static": [
|
|
22
|
+
{
|
|
23
|
+
"audience": "consumer",
|
|
24
|
+
"channel": "static",
|
|
25
|
+
"sentence": "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.",
|
|
26
|
+
"specification": "model",
|
|
27
|
+
"family": "setup",
|
|
28
|
+
"fix": "Move the call into `specs/<specification>/<specification>.specification.ts` and export the handle from there.",
|
|
29
|
+
"name": "runner-in-specification-file",
|
|
30
|
+
"rationale": "Anchoring runners to a recognisable filename makes the entry point discoverable and keeps tests declarative.",
|
|
31
|
+
"reach": "tests"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"audience": "consumer",
|
|
35
|
+
"channel": "static",
|
|
36
|
+
"sentence": "Seven constructors and only seven: `specification.api()`, `specification.job()`, `specification.cli(bin)`, `specification.integration()`, `specification.website()`, `specification.mobile()`, `specification.lint()`. Any other member (`.app`, `.http`, `.stack`...) is an error. A rendered component starts nothing: it is a chain, not a member of this record.",
|
|
37
|
+
"specification": "model",
|
|
38
|
+
"family": "setup",
|
|
39
|
+
"fix": "Name one of the seven, or reach for the `component` chain: an eighth subject is a decision before it is a constructor.",
|
|
40
|
+
"name": "no-unknown-constructor",
|
|
41
|
+
"rationale": "A closed surface stops parallel constructors being invented and keeps the API memorable.",
|
|
42
|
+
"reach": "all"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"audience": "consumer",
|
|
46
|
+
"channel": "static",
|
|
47
|
+
"sentence": "The return is destructured under the constructor's canonical name, unaliased (`{ api, cleanup, docker }`); renaming (`{ api: myApi }`) is an error.",
|
|
48
|
+
"specification": "model",
|
|
49
|
+
"family": "setup",
|
|
50
|
+
"fix": "Drop the alias and keep the canonical name.",
|
|
51
|
+
"name": "no-runner-alias",
|
|
52
|
+
"rationale": "One instance name per specification makes every spec readable with no local context.",
|
|
53
|
+
"reach": "all"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"audience": "consumer",
|
|
57
|
+
"channel": "static",
|
|
58
|
+
"sentence": "The specification file passes `cleanup` to `afterAll`; a `cleanup` destructured but never handed over is an error.",
|
|
59
|
+
"specification": "model",
|
|
60
|
+
"family": "setup",
|
|
61
|
+
"fix": "Add `afterAll(cleanup)` at the end of the specification file.",
|
|
62
|
+
"name": "cleanup-passed-to-after-all",
|
|
63
|
+
"rationale": "Guaranteeing teardown stops containers and connections leaking between files.",
|
|
64
|
+
"reach": "specification"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"audience": "consumer",
|
|
68
|
+
"channel": "static",
|
|
69
|
+
"sentence": "A `root` pointing at the directory the automatic walk would have found is redundant, and an error.",
|
|
70
|
+
"specification": "model",
|
|
71
|
+
"family": "setup",
|
|
72
|
+
"fix": "Drop the option: the walk finds the nearest `package.json` by itself.",
|
|
73
|
+
"name": "no-redundant-root",
|
|
74
|
+
"rationale": "Detection by convention has to stay the default; an explicit `root` is only justified where it fails.",
|
|
75
|
+
"reach": "all"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"audience": "consumer",
|
|
79
|
+
"channel": "static",
|
|
80
|
+
"sentence": "A `// When -` marker is optional; written, it sits after `// Given -` and before `// Then -`.",
|
|
81
|
+
"specification": "model",
|
|
82
|
+
"family": "chain",
|
|
83
|
+
"fix": "Move it to the action it narrates, or drop it when the chain is the action.",
|
|
84
|
+
"name": "when-between-given-and-then",
|
|
85
|
+
"rationale": "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.",
|
|
86
|
+
"reach": "tests"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"audience": "consumer",
|
|
90
|
+
"channel": "static",
|
|
91
|
+
"sentence": "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.",
|
|
92
|
+
"specification": "model",
|
|
93
|
+
"family": "chain",
|
|
94
|
+
"fix": "Fold the continuation into the sentence, or separate it with a blank line.",
|
|
95
|
+
"name": "marker-on-one-line",
|
|
96
|
+
"rationale": "The narration is a sentence about the subject; a paragraph hides its second half from every reader that only looks at the marker line.",
|
|
97
|
+
"reach": "tests"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"audience": "consumer",
|
|
101
|
+
"channel": "static",
|
|
102
|
+
"sentence": "A setting every chain of a spec file states alike (the same `.clock(...)`, `.headers(...)` or `.env(...)`, per runner handle) is the file's, stated once on a view of the specification, `const events = api.with({ clock: '...' })`, and is an error at the first chain. A file of one chain is not judged; a seed and a stub are a test's Given, never a setting.",
|
|
103
|
+
"specification": "model",
|
|
104
|
+
"family": "chain",
|
|
105
|
+
"fix": "Declare the view once after the imports and the constants it reads, `const <view> = <handle>.with({ ... })`, and start every chain from it: `typescript fix` writes it, named for the file's subject so every file of one folder names it alike (`events` for `events/list/`, its name for a job, its first word for a command; `<subject>View` when the file already holds the name, `<handle>InFile` for no named subject), and drops the setting from each chain.",
|
|
106
|
+
"name": "no-repeated-chain-setting",
|
|
107
|
+
"rationale": "A setting repeated on every chain reads as a choice each test made; stated once, it reads as what it is (the condition of the whole file) and the next test cannot forget it.",
|
|
108
|
+
"reach": "spec"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"audience": "consumer",
|
|
112
|
+
"channel": "static",
|
|
113
|
+
"sentence": "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:`.",
|
|
114
|
+
"specification": "model",
|
|
115
|
+
"family": "chain",
|
|
116
|
+
"fix": "Write the two markers, in order, as sentences about the subject, not about the code (Conventions § The narration).",
|
|
117
|
+
"name": "given-and-then-markers-present",
|
|
118
|
+
"rationale": "Given/Then narration makes a test's intent readable without reading its assertions.",
|
|
119
|
+
"reach": "tests"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"audience": "consumer",
|
|
123
|
+
"channel": "static",
|
|
124
|
+
"sentence": "`.env({ <SERVICE>_URL: <service>.connectionString })` repeats the framework's own injection, and is an error.",
|
|
125
|
+
"specification": "cli",
|
|
126
|
+
"family": "chain",
|
|
127
|
+
"fix": "Drop the line: the record key already injects `<KEY>_URL` into the child process.",
|
|
128
|
+
"name": "no-redundant-service-url",
|
|
129
|
+
"rationale": "The injection already covers service URLs; rewriting them by hand invites drift.",
|
|
130
|
+
"reach": "tests"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"audience": "consumer",
|
|
134
|
+
"channel": "static",
|
|
135
|
+
"sentence": "`.trigger(name)` takes a stable kebab-case identifier; a non-kebab-case `name` is an error.",
|
|
136
|
+
"specification": "job",
|
|
137
|
+
"family": "chain",
|
|
138
|
+
"fix": "Write the job name in kebab-case, the same spelling the application registers it under.",
|
|
139
|
+
"name": "trigger-name-in-kebab-case",
|
|
140
|
+
"rationale": "The job name is a contract between the app and its tests: a stable identifier forbids divergent translations.",
|
|
141
|
+
"reach": "tests"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"audience": "consumer",
|
|
145
|
+
"channel": "static",
|
|
146
|
+
"sentence": "A `specification.cli(bin)` whose binary resolves inside a dependency's `node_modules/.bin` tests the third-party tool, not the product command, and is an error (a suppression with a reason is accepted). Reach: a specification file; a runner created anywhere else is `runner-in-specification-file`'s.",
|
|
147
|
+
"specification": "cli",
|
|
148
|
+
"family": "chain",
|
|
149
|
+
"fix": "Point the runner at the product's own binary; compare its output whole with `toMatch`, and cut a block with `result.stdout.grep(...)` only where the rest is noise.",
|
|
150
|
+
"name": "cli-runs-the-product-binary",
|
|
151
|
+
"rationale": "A spec has to exercise the product's real command, and asserts on what that command prints.",
|
|
152
|
+
"reach": "specification"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"audience": "consumer",
|
|
156
|
+
"channel": "static",
|
|
157
|
+
"sentence": "A specs tree DECLARES its shape through the `depth` option: `specification-domain` (the default, a test at specification/domain depth), `specification` (a test at the specification 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 `*.specification.ts` sits at the ROOT of its specification, the one place a runner lives. Under `specification-domain` and `specification`, a `.spec.ts` of the api or cli specification sits as deep as its subject's standard name (a route's resource and action, `events/list/`, a command's words, `db/migrate/`), which `spec-in-subject-folder` judges. A first-level folder that is no specification (a repository suite, `surface/` or `lint/`) is judged by the same depth as a specification is. In every mode, `off` included, a directory named `__fixtures__/`, `__expected__/`, `__requests__/` or `__seeds__/` is GROUND, and `__setup__/` holds the code that prepares specs: no spec lives inside either. One exception, and only one: a special folder may hold CODE, and a `<module>.test.ts` sitting BESIDE the `<module>.ts` it tests is legal there, since a specs tree holds no `__tests__/`. A test with no neighbouring module, or a `*.specification.ts`, is still a violation. A first-level folder is a specification, a special folder (what the specifications share) or a repository suite, which holds tests of its own (one test is enough). A module in one that is none of the three (a `support/` of helpers and mocks), a runner included, is refused in every mode, `off` included: a runner lives at the root of the specification it builds, a test helper in the `__setup__/` of the lowest folder its tests share, a fixture under `specs/__fixtures__/`. A `.test.tsx` is out of reach: a rendered unit is specified in the `__tests__/` of its component's folder, never in a specification/domain tree, and `test-in-its-tests-folder` holds its neighbourhood.",
|
|
158
|
+
"specification": "model",
|
|
159
|
+
"family": "layout",
|
|
160
|
+
"fix": "Move the file to the depth the tree declares, or declare the depth the tree actually has; move a runner to its specification root, and a helper into the `__setup__/` of the lowest folder its tests share.",
|
|
161
|
+
"name": "specs-at-declared-depth",
|
|
162
|
+
"rationale": "A fixed depth makes every file's place predictable and statically checkable.",
|
|
163
|
+
"reach": "specs"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"audience": "consumer",
|
|
167
|
+
"channel": "static",
|
|
168
|
+
"sentence": "Outside `specs/**/__setup__/stubs/`, an import resolving into a kind folder (`__setup__/stubs/<kind>/...`, `<kind>` a stub builder the package exports: `http`, `openai`, `anthropic`) is an error: a test imports only the `*.stubs.ts` facades.",
|
|
169
|
+
"specification": "model",
|
|
170
|
+
"family": "stubs",
|
|
171
|
+
"fix": "Add a named scenario export to the facade and import that instead.",
|
|
172
|
+
"name": "stubs-imported-through-facade",
|
|
173
|
+
"rationale": "Unit stubs are composition details; going through the facade keeps each scenario named in the same place as the world it derives from.",
|
|
174
|
+
"reach": "tests"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"audience": "consumer",
|
|
178
|
+
"channel": "static",
|
|
179
|
+
"sentence": "Inside a kind folder of `__setup__/stubs/`, every `*.request.ts` (the exact request a unit matches) has a `<stem>.ts` sibling, the stem the name up to the FIRST dot (`classify-article.ts` for `classify-article.request.ts`). Orphan data is an error, reported on the orphan itself. A payload a unit serves is data, so it sits in the ground beside the `__setup__/` (`__fixtures__/<name>.json`), which `__setup__/` never holds (`reserved-folder-in-closed-list`).",
|
|
180
|
+
"specification": "model",
|
|
181
|
+
"family": "stubs",
|
|
182
|
+
"fix": "Write the stub that matches the request, or delete the request.",
|
|
183
|
+
"name": "stub-data-has-a-builder",
|
|
184
|
+
"rationale": "Data exists only as served by a stub: a file with no owner is dead weight no test ever loads.",
|
|
185
|
+
"reach": "stub"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"audience": "consumer",
|
|
189
|
+
"channel": "static",
|
|
190
|
+
"sentence": "`__requests__/` holds what a spec SENDS: a whole request as a `.http` (`request()`), or a body a verb sends (`post`/`put`/`patch` with a `file()`), whether a `.json`, a `.txt` or any format the app reads. Every file there carries its extension; a file with none is an error.",
|
|
191
|
+
"specification": "api",
|
|
192
|
+
"family": "layout",
|
|
193
|
+
"fix": "Name the file with its extension: `.http` for a whole request, the body's own format otherwise.",
|
|
194
|
+
"name": "request-file-extension-known",
|
|
195
|
+
"rationale": "The extension is how the file is sent (a `.http` whole, a `.json` parsed, anything else verbatim), so a file without one says nothing about what the app will receive.",
|
|
196
|
+
"reach": "ground"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"audience": "consumer",
|
|
200
|
+
"channel": "static",
|
|
201
|
+
"sentence": "Under `specs/**/__setup__/stubs/`: the ROOT carries only `*.stubs.ts` facades and one folder per stub KIND, a kind being a stub builder `@jterrazz/test/stubs` exports (`http` | `openai` | `anthropic`, the closed list its exports decide). A facade exports its builders and sets BY NAME (the world and its scenarios), and one that exports nothing is an error; a default export is not required, and when a facade has one it is a composition: built by `defineStubs(...)`, a composition re-export, a scenario of a sibling facade (a name imported from a `*.stubs` module, or a call to it) or a call to a builder of the file declared to return `Stubs` (no type is read, so a builder is judged by where it comes from or what it declares). A unit stub is `<kind>/<kebab-name>.ts` with an `export default defineStub(...)` (or a factory returning one), and it calls only its own folder's builder: a `<kind>.<builder>` of another kind the file imports from `@jterrazz/test/stubs` (by name, or through a namespace import, `t.<kind>.<builder>`), in its request, its reply or anywhere else, is an error. A kind folder is flat (a file nested below it is an error), and a folder under the root that is no kind is an error at each unit it holds; each file is judged on its own, and a file that is no module is `reserved-folder-in-closed-list`'s.",
|
|
202
|
+
"specification": "model",
|
|
203
|
+
"family": "stubs",
|
|
204
|
+
"fix": "Move the file to the half it belongs to: a facade at the root, a unit under the folder of the builder it calls.",
|
|
205
|
+
"name": "stubs-root-holds-facades-and-kinds",
|
|
206
|
+
"rationale": "A fixed tree separates the public facade from the internal units and makes the FOLDER carry the kind: filenames go back to being domain words, and a unit reads as one kind of outside world. A test imports the builder it calls by name, so a required default no test reads would be dead code the unused-export pass reports.",
|
|
207
|
+
"reach": "stub"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"audience": "consumer",
|
|
211
|
+
"channel": "static",
|
|
212
|
+
"sentence": "A golden `file()`'s path carries its extension (`toMatch(file('./__expected__/help.txt'))`, `'./__expected__/<case>.aria.yaml'` for an ARIA tree); a golden file with no extension is an error. A tree golden is `toMatch(directory(...))`, whose path ends in `/`.",
|
|
213
|
+
"specification": "model",
|
|
214
|
+
"family": "layout",
|
|
215
|
+
"fix": "Write the fixture's full name, extension included.",
|
|
216
|
+
"name": "golden-path-has-extension",
|
|
217
|
+
"rationale": "The extension is part of the golden's path: the path IS the file, and a file without its extension is another file.",
|
|
218
|
+
"reach": "tests"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"audience": "consumer",
|
|
222
|
+
"channel": "static",
|
|
223
|
+
"sentence": "`__seeds__/` holds `*.sql` files only; any other file is an error.",
|
|
224
|
+
"specification": "model",
|
|
225
|
+
"family": "services",
|
|
226
|
+
"fix": "Write the seed as SQL, or move the file to `__fixtures__/` as the material it is.",
|
|
227
|
+
"name": "seeds-are-sql",
|
|
228
|
+
"rationale": "`.seed()` carries database state only: no seed handlers, no prefix dispatch.",
|
|
229
|
+
"reach": "ground"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"audience": "consumer",
|
|
233
|
+
"channel": "static",
|
|
234
|
+
"sentence": "The verb and the ground folder agree: `seed()` reads `__seeds__/`, `request()` and a request body (`post`/`put`) read `__requests__/`, `fixture()` reads `__fixtures__/`, a golden `toMatch(file|directory(...))` reads `__expected__/`, and `run()` reads a document beside the test, under no ground folder, or under `__fixtures__/` for a document that is an input a negative spec runs. The same four folders at the specs root hold what several specifications share, so a shared seed is `<specs>/__seeds__/x.sql` (`../../__seeds__/x.sql`), never `<specs>/__fixtures__/__seeds__/x.sql`: the role folder is the first ground folder the path passes through, and it is the one judged. The first ground folder a literal path passes through is the one judged, whether the `file()`/`directory()` is handed to the verb inline or bound first to a `const` of the same file (the nearest declaration of the name, as the language resolves it); a `let`, a parameter or a computed path is out of reach, and a document's `fixture:` is held to `__fixtures__/` by its grammar (`document-follows-its-grammar`).",
|
|
235
|
+
"specification": "model",
|
|
236
|
+
"family": "layout",
|
|
237
|
+
"fix": "Move the file into the folder its verb reads, the leaf's own or that of the lowest folder the leaves share (the specs root's when they span specifications), and spell the path through it.",
|
|
238
|
+
"name": "verb-reads-its-ground-folder",
|
|
239
|
+
"rationale": "Four reserved names are a vocabulary only if each means one thing: a seed parked in `__fixtures__/` or a golden in `__requests__/` makes the folder lie about what it holds, and the next reader trusts the folder.",
|
|
240
|
+
"reach": "tests"
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"audience": "consumer",
|
|
244
|
+
"channel": "static",
|
|
245
|
+
"sentence": "`file()` and `directory()` are called in a test or spec file (a `.test.ts(x)`, a `.spec.ts`, a type test) and nowhere else: a shared helper module, a specification file or a stub takes a `file()` as a parameter and never builds one. The call is recognised by its import from the framework's entry, an alias included.",
|
|
246
|
+
"specification": "model",
|
|
247
|
+
"family": "layout",
|
|
248
|
+
"fix": "Take the `file()`/`directory()` as a parameter of the helper, and build it in the test that calls it.",
|
|
249
|
+
"name": "paths-named-in-the-test",
|
|
250
|
+
"rationale": "A path resolves against the spec that starts the chain, so one built in a helper resolves per caller: the same line names a different file for every spec that reaches it, and the file it names is visible on no line of the test that stands on it.",
|
|
251
|
+
"reach": "all"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"audience": "consumer",
|
|
255
|
+
"channel": "static",
|
|
256
|
+
"sentence": "A test file (a `.spec.ts`, a `.test.ts`, a `.test.tsx`) holds one top-level `describe`, titled with what the file is about, and every test inside it. A spec file's title is its aspect: `pagination.spec.ts` holds `describe('pagination', ...)`. A module test's is its subject, read from the module it names (`<base>.ts`, `<base>.tsx` in the folder above its `__tests__/`, or beside a rendered unit under `specs/`): the name its default export declares; else its one value export; else the value export spelling the base name in camelCase or PascalCase, or the base less its role suffix (`request` for `request.action`); else the base name. So `__tests__/locale-note.test.tsx`, naming `export function LocaleNote`, holds `describe('LocaleNote', ...)`. A title that states the file's subject in other words is that subject: the file's base name read with spaces (`cli reference` for `cli-reference.spec.ts`), or the subject in another spelling (`<LocaleNote />`); a website spec's page or an api spec's route is its subject too (`describe('/articles/:id', ...)` for a spec that visits `/articles/:id`, `'GET /events/:id'` for one that requests it). A `describe.each` names its rows, so its title starts with that name, `'pagination — page %s'`; a subject keeps its case, the `test()` preset exempting the top-level `describe` from `vitest/prefer-lowercase-title` (`ignoreTopLevelDescribe`). A file with no `describe` is refused at its first test, a second `describe` or a test beside the one at itself, a title naming anything else at the title; a file that declares no test through `describe`/`test`/`it` (a rule tester's cases, a type test) is out of reach.",
|
|
257
|
+
"specification": "model",
|
|
258
|
+
"family": "layout",
|
|
259
|
+
"fix": "Title the file's one `describe` with its aspect or its subject, or with the file's name in words, and put every test inside it: `typescript fix` wraps a file that has none, moves a test beside the group into it, and merges several groups into one, every test keeping its title and the group titles leaving with their groups, when every group holds tests alone and no two tests share a title; what stands between the tests stays at module scope. It never renames a title: a wrong one, a table's included, is reported and left to its author.",
|
|
260
|
+
"name": "one-describe-per-file",
|
|
261
|
+
"rationale": "The runner reports a failure under the describe it stands in, so a title that is the aspect or the subject names the file the failure came from; a second group, or a stray test, is a second question the file answers.",
|
|
262
|
+
"reach": "tests"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"audience": "consumer",
|
|
266
|
+
"channel": "static",
|
|
267
|
+
"sentence": "A spec file is named for its aspect: kebab-case (`not-found.spec.ts`), and never a name that says the file holds what fit nowhere else (`misc`, `other`, `basic`, `general`, `more`, `edge-cases`, `tests`, `main`).",
|
|
268
|
+
"specification": "model",
|
|
269
|
+
"family": "layout",
|
|
270
|
+
"fix": "Rename the file for the question its tests ask of the subject: `pagination`, `filters`, `sorting`, `language`, `validation`, `not-found`, `auth`, `shape` are the api vocabulary the Conventions page advises.",
|
|
271
|
+
"name": "spec-named-for-its-aspect",
|
|
272
|
+
"rationale": "The folder names the subject; the file name is the only place the question asked of it is written, and a catch-all name answers none: the next test lands there because it fits nowhere, and the file grows until it holds everything.",
|
|
273
|
+
"reach": "spec"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"audience": "consumer",
|
|
277
|
+
"channel": "static",
|
|
278
|
+
"sentence": "A spec file holds at most 400 lines; a longer one is refused at its first line, the message grouping its tests by the prefix of their titles, where the cut usually goes. A file already past it is recorded on the ratchet and may only shrink.",
|
|
279
|
+
"specification": "model",
|
|
280
|
+
"family": "layout",
|
|
281
|
+
"fix": "Cut the file along the groups the message names, one aspect per file, in the same folder.",
|
|
282
|
+
"name": "spec-file-within-line-limit",
|
|
283
|
+
"rationale": "A file past a few hundred lines asks several questions of its subject, and a reader looking for one scrolls through the others; a cap written as a number is the one a reviewer does not have to argue.",
|
|
284
|
+
"reach": "spec"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"audience": "consumer",
|
|
288
|
+
"channel": "static",
|
|
289
|
+
"sentence": "A test body spans at most 60 lines of gestures, counted over the function a `test(...)`/`it(...)` runs from the line it opens on to the line it closes on, blank lines and comments included, less the lines the expected value of a `toMatch`, `toStrictEqual` or `toEqual` holds past the one it opens on (that value is the claim the test makes, and `ground-shared-or-inline` caps a `toMatch` one, not this rule); a longer one is refused at the function. A body already past it is recorded on the ratchet and may only shrink.",
|
|
290
|
+
"specification": "model",
|
|
291
|
+
"family": "layout",
|
|
292
|
+
"fix": "Split it into one test per behaviour, or move a long Given into a function the test calls.",
|
|
293
|
+
"name": "test-body-within-line-limit",
|
|
294
|
+
"rationale": "A test past a screen asks several things of its subject, and the one that fails is the one a reader has to find; the toolchain's function cap leaves a test file alone, because a file's one `describe` is a function as long as the file, so this cap is the test's own.",
|
|
295
|
+
"reach": "tests"
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"audience": "consumer",
|
|
299
|
+
"channel": "static",
|
|
300
|
+
"sentence": "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.",
|
|
301
|
+
"specification": "model",
|
|
302
|
+
"family": "expectations",
|
|
303
|
+
"fix": "Pin it with `clock.at()`, or match it with `{{iso8601}}`/`match.uuid`.",
|
|
304
|
+
"name": "no-sampled-value-in-expectation",
|
|
305
|
+
"rationale": "An assertion that reads the machine twice holds whatever the subject does, and the day it fails it fails for the clock.",
|
|
306
|
+
"reach": "tests"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"audience": "consumer",
|
|
310
|
+
"channel": "static",
|
|
311
|
+
"sentence": "A sampled value anywhere in a test callback is an error. It is the wider net around `no-sampled-value-in-expectation`, silent where that rule already refuses, on a name built in a template literal, and on a `new Date()` or `Date.now()` after a `using _ = clock.at(<instant>)` or `clock.run(<instant>)` in the same block, where the reading is the pinned instant. A pin fakes `Date` alone, so `Math.random()`, `randomUUID()` and `performance.now()` stay samples under it, and an argument-less `clock.run()` pins no instant. A `*.specification.ts(x)` is out of reach.",
|
|
312
|
+
"specification": "model",
|
|
313
|
+
"family": "clock",
|
|
314
|
+
"fix": "Reach for `clock.at()`, `clock.advance()`, or a token in the golden.",
|
|
315
|
+
"name": "no-sampled-value-in-test",
|
|
316
|
+
"rationale": "A value sampled into the Given travels into the subject, and the assertion that reads it back is the same tautology one step removed.",
|
|
317
|
+
"reach": "tests"
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"audience": "consumer",
|
|
321
|
+
"channel": "static",
|
|
322
|
+
"sentence": "A module test whose every assertion reads the call log of a double it built itself (`mockOf`, `vi.fn`, `vi.spyOn`) is an error. The reach is the `module` role: on a component a callback prop IS the contract with its parent. It is an error, 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.",
|
|
323
|
+
"specification": "model",
|
|
324
|
+
"family": "expectations",
|
|
325
|
+
"fix": "Assert the returned value or the resulting state, or say in a line that the call IS the product.",
|
|
326
|
+
"name": "no-double-only-assertion",
|
|
327
|
+
"rationale": "The subject can return anything, raise anything, or return nothing at all, and every assertion still passes.",
|
|
328
|
+
"reach": "module"
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"audience": "consumer",
|
|
332
|
+
"channel": "static",
|
|
333
|
+
"sentence": "An `expect()` in a test is not called inside a loop (a `for`, a `for...of`, a `for...in`, a `while`, a `do...while`) nor inside the callback of an array iteration (`forEach`, `map`, `flatMap`, `filter`, `every`, `some`, `find`, `reduce`) in the same function: a loop runs its assertion once per element, and not at all over an empty collection. `vitest/no-conditional-expect` holds the other assertion that may not run, one under a condition.",
|
|
334
|
+
"specification": "model",
|
|
335
|
+
"family": "expectations",
|
|
336
|
+
"fix": "Compare the whole collection in one assertion, `expect(rows.map(...)).toStrictEqual([...])`, or state each case with `test.each`.",
|
|
337
|
+
"name": "no-expect-in-a-loop",
|
|
338
|
+
"rationale": "A loop over nothing passes with no assertion made, and a loop that fails names one element where a whole comparison would show them all.",
|
|
339
|
+
"reach": "tests"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"audience": "consumer",
|
|
343
|
+
"channel": "static",
|
|
344
|
+
"sentence": "Under `specs/`, vitest's `expect` is called in a test file alone (a `*.spec.ts`, a `*.test.ts(x)`, a `*.test-d.ts`): a `__setup__/` module, a test adapter, a stub or a `*.specification.ts` that asserts is refused at the call. The binding is what is read (`expect` imported from `vitest` under any local name, statically or by `await import('vitest')`, `<namespace>.expect` on a namespace of `vitest`, and `expect.soft`, `expect.poll` on either), so a method or a local of that name is not.",
|
|
345
|
+
"specification": "model",
|
|
346
|
+
"family": "expectations",
|
|
347
|
+
"fix": "Return what the module reads, and assert it in the test that calls it.",
|
|
348
|
+
"name": "no-expect-outside-a-test",
|
|
349
|
+
"rationale": "An assertion in a helper fails far from the test that owns the claim, fires alike for every caller, and takes the Then out of the test body where a reader looks for it.",
|
|
350
|
+
"reach": "specs"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"audience": "consumer",
|
|
354
|
+
"channel": "static",
|
|
355
|
+
"sentence": "A test whose ONE assertion is an existence check (`toBeDefined`, `not.toBeNull`, `not.toBeUndefined` or a bare `toHaveBeenCalled`) is an error. 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. `toBeTruthy` and a bare `toThrow` are refused wherever they stand, by `vitest/prefer-strict-boolean-matchers` and `vitest/require-to-throw-message`.",
|
|
356
|
+
"specification": "model",
|
|
357
|
+
"family": "expectations",
|
|
358
|
+
"fix": "Assert the value, a golden, or a row.",
|
|
359
|
+
"name": "no-existence-only-assertion",
|
|
360
|
+
"rationale": "`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.",
|
|
361
|
+
"reach": "tests"
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
"audience": "consumer",
|
|
365
|
+
"channel": "static",
|
|
366
|
+
"sentence": "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 an error. 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.",
|
|
367
|
+
"specification": "model",
|
|
368
|
+
"family": "expectations",
|
|
369
|
+
"fix": "Golden the subject: `expect(result.stdout).toMatch(file('./__expected__/<case>.txt'))`, tokens for what moves.",
|
|
370
|
+
"name": "no-probe-cluster",
|
|
371
|
+
"rationale": "Three greps state three lines, leave everything between them unstated, and still have to be rewritten one by one the day the output changes.",
|
|
372
|
+
"reach": "tests"
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"audience": "consumer",
|
|
376
|
+
"channel": "static",
|
|
377
|
+
"sentence": "A test that reads a response, a json or a table (`<result>.response`, `<result>.json`, `<result>.table('<name>')` of a result it awaited) compares it WHOLE, once: `toMatch` handed the whole result written in place or its golden, or a table's `toBeEmpty()`. A precise assertion on one of its properties (`expect(result.json.value.total).toBe(1)`) is allowed only IN ADDITION, and the first read of a result no whole comparison reaches is an error. A negation, a RegExp and a read through a helper are no whole comparison; a result read through a helper, a computed table name, or a comparison made in a helper is out of reach, and the rule stays silent. A read whose value is only CARRIED into a later request of the same test (every use of it, through the names and helpers it passes, ends in the arguments of a chain off a handle the test awaits, in place or through the name that holds it: a path, `params`, a body, a header, as the cursor a page hands back) claims nothing and owes no comparison; one `expect(...)` of that value makes it a read again, and the result the test does claim is still compared whole.",
|
|
378
|
+
"specification": "model",
|
|
379
|
+
"family": "expectations",
|
|
380
|
+
"fix": "Write `expect(<result>.<subject>).toMatch('')` and run `TEST_UPDATE=1`: the whole result is written in place; keep the precise assertion beside it if one property IS the subject.",
|
|
381
|
+
"name": "result-compared-whole",
|
|
382
|
+
"rationale": "Probes are a partial oracle: what they do not name is free to regress unseen. The whole result, written where the test reads it, is the default oracle (approval testing) and a precise assertion states what the test is about on top of it.",
|
|
383
|
+
"reach": "tests"
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
"audience": "consumer",
|
|
387
|
+
"channel": "static",
|
|
388
|
+
"sentence": "A file-level `const` a spec hands to a matcher as the expected value serves two tests or more; read by one test, it is an error at its declaration. A constant read outside every test (by a helper) counts as shared, and an exported one is out of reach.",
|
|
389
|
+
"specification": "model",
|
|
390
|
+
"family": "expectations",
|
|
391
|
+
"fix": "Write the value in place, in the matcher the one test hands it to, and delete the constant: `typescript fix` inlines one read.",
|
|
392
|
+
"name": "expected-constant-shared",
|
|
393
|
+
"rationale": "A test is read where it is written: an expectation parked at the top of the file makes its one reader scroll to know what it asserts, and buys no sharing for the price.",
|
|
394
|
+
"reach": "spec"
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
"audience": "consumer",
|
|
398
|
+
"channel": "static",
|
|
399
|
+
"sentence": "A spec imports no expectation: a name imported from another module of its specs tree (neither its `*.specification` runner nor a module under `stubs/`) and handed to a matcher as the expected value, itself or a member of it, is an error at the argument. A helper is CALLED, and what it builds is the test's own; a module outside the specs tree is the product's, and a spec may compare against its data (`package.json`'s version, a constant the product exports); `no-test-to-test-import` already refuses importing a test file at all.",
|
|
400
|
+
"specification": "model",
|
|
401
|
+
"family": "expectations",
|
|
402
|
+
"fix": "Write the expected value in the test (in place, or `file(...)` when several tests of the leaf share it) and drop the import.",
|
|
403
|
+
"name": "no-imported-expectation",
|
|
404
|
+
"rationale": "An expectation written in another module asserts what nobody reviewing the test reads, and a change to that module moves the assertions of every spec that imports it without one of them changing.",
|
|
405
|
+
"reach": "spec"
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
"audience": "consumer",
|
|
409
|
+
"channel": "static",
|
|
410
|
+
"sentence": "A `transform` that only rewrites into equivalents of the standard tokens duplicates the grammar, and is an error.",
|
|
411
|
+
"specification": "model",
|
|
412
|
+
"family": "expectations",
|
|
413
|
+
"fix": "Delete the transform and write the token the grammar already has.",
|
|
414
|
+
"name": "no-transform-duplicating-tokens",
|
|
415
|
+
"rationale": "`transform` is the escape hatch for noise the tokens do not cover, not a second copy of them.",
|
|
416
|
+
"reach": "tests"
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
"audience": "consumer",
|
|
420
|
+
"channel": "static",
|
|
421
|
+
"sentence": "`expect(x.raw).toContain` compares the stream as it was captured, and is an error. A stream compared as a value reads `x.text`: ANSI stripped, the runner's `transform` applied, the text a golden compares. A RegExp argument is exempt: a pattern carries no token grammar to lose.",
|
|
422
|
+
"specification": "model",
|
|
423
|
+
"family": "expectations",
|
|
424
|
+
"fix": "Compare `x.text`; keep `.raw` for a test that proves what the comparison strips, with the suppression saying so.",
|
|
425
|
+
"name": "no-raw-bypass",
|
|
426
|
+
"rationale": "`.raw` keeps the colour codes and skips the runner's transform, so a substring found there is not the text any golden of the same stream would be compared against.",
|
|
427
|
+
"reach": "tests"
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"audience": "consumer",
|
|
431
|
+
"channel": "static",
|
|
432
|
+
"sentence": "`toMatch('')`, or `toMatch` of an empty template literal, on a plain string (a stream's `.text`, `.raw` or a file's `.content`) is vitest's substring match, which holds for any text, and is an error. The empty value is the \"not recorded yet\" mark of an accessor handed to `toMatch`, which update mode fills; on a string it only passes.",
|
|
433
|
+
"specification": "model",
|
|
434
|
+
"family": "expectations",
|
|
435
|
+
"fix": "Compare the string whole with `toBe('')`, or hand the accessor itself to `toMatch`.",
|
|
436
|
+
"name": "no-empty-substring-match",
|
|
437
|
+
"rationale": "`'abc'.includes('')` is true: the assertion cannot fail, and a stub the author meant to fill later stays green.",
|
|
438
|
+
"reach": "tests"
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"audience": "consumer",
|
|
442
|
+
"channel": "static",
|
|
443
|
+
"sentence": "A capture ref (`match.ref`, `{{kind#ref}}`) appearing exactly once in the whole file (its code, a `match.ref` and every value written in place, a string or a template, plus the golden files it references) is named for nothing, and is an error.",
|
|
444
|
+
"specification": "model",
|
|
445
|
+
"family": "expectations",
|
|
446
|
+
"fix": "Drop the ref name and keep the bare token.",
|
|
447
|
+
"name": "no-single-use-ref",
|
|
448
|
+
"rationale": "A ref only earns its name when it asserts equality across at least two occurrences.",
|
|
449
|
+
"reach": "tests"
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
"audience": "consumer",
|
|
453
|
+
"channel": "static",
|
|
454
|
+
"sentence": "A golden `toMatch(file(...))`/`toMatch(directory(...))`, or a `toMatch(<value>)` on a result (the value written in place, rewritten in the test), whose FAILURE is the subject of the test must carry `{ frozen: true }`, or `TEST_UPDATE=1` silently rewrites the deliberately-wrong expectation instead of throwing; its absence is an error. Two shapes are read: the exact wrapper (`expect(() => ...).toThrow()` / `expect(...).rejects.toThrow()`) and a bounded heuristic, a golden inside a helper whose body also asserts a throw, which is where a helper owning the try/catch puts it.",
|
|
455
|
+
"specification": "model",
|
|
456
|
+
"family": "expectations",
|
|
457
|
+
"fix": "Pass `{ frozen: true }` so update mode never overwrites the fixture the test is about.",
|
|
458
|
+
"name": "negative-golden-frozen",
|
|
459
|
+
"rationale": "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.",
|
|
460
|
+
"reach": "tests"
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
"audience": "consumer",
|
|
464
|
+
"channel": "static",
|
|
465
|
+
"sentence": "A test whose ONLY assertions are HTTP status probes (`expect(X.status).toBe(N)` / `.toEqual(N)`, N a numeric literal from 100 to 599) is an error: this case wants a full golden (`expect(result.response).toMatch(file('./__expected__/case.http'))`). A status probe BESIDE a real assertion (a whole-result `toMatch`, a table's rows, `toContain`...) stays silent (a legitimate scalpel).",
|
|
466
|
+
"specification": "api",
|
|
467
|
+
"family": "expectations",
|
|
468
|
+
"fix": "Assert what the response carries, not only the code it came back with.",
|
|
469
|
+
"name": "no-status-only-test",
|
|
470
|
+
"rationale": "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 `result-compared-whole`, which reads the response the probe stands beside).",
|
|
471
|
+
"reach": "tests"
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
"audience": "consumer",
|
|
475
|
+
"channel": "static",
|
|
476
|
+
"sentence": "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 `no-simulated-dom-in-config`'s.",
|
|
477
|
+
"specification": "component",
|
|
478
|
+
"family": "setup",
|
|
479
|
+
"fix": "A rendered thing is a `.test.tsx` in the `__tests__/` of its component's folder, collected by the `component()` project.",
|
|
480
|
+
"name": "no-simulated-dom",
|
|
481
|
+
"rationale": "A simulated DOM behaves ALMOST like a browser, and what ships is judged by a real one: the component specification renders in the Chromium the website specification already drives.",
|
|
482
|
+
"reach": "tests"
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
"audience": "consumer",
|
|
486
|
+
"channel": "static",
|
|
487
|
+
"sentence": "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 an error.",
|
|
488
|
+
"specification": "model",
|
|
489
|
+
"family": "setup",
|
|
490
|
+
"fix": "Point the glob at the folder that exists, or delete the project that collects nothing.",
|
|
491
|
+
"name": "include-points-at-a-folder",
|
|
492
|
+
"rationale": "A project that collects nothing passes: the suite never ran, and the only trace is a zero nobody reads.",
|
|
493
|
+
"reach": "config"
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
"audience": "consumer",
|
|
497
|
+
"channel": "static",
|
|
498
|
+
"sentence": "`literate.runner`, 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.",
|
|
499
|
+
"specification": "cli",
|
|
500
|
+
"family": "setup",
|
|
501
|
+
"fix": "Point it at the `*.specification.ts` that exports the `cli` its documents run through.",
|
|
502
|
+
"name": "literate-runner-exists",
|
|
503
|
+
"rationale": "A path that resolves to nothing fails at collection time with a message about an import, several layers from the line that caused it.",
|
|
504
|
+
"reach": "config"
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
"audience": "consumer",
|
|
508
|
+
"channel": "static",
|
|
509
|
+
"sentence": "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 an error; a `*.specification.ts(x)` is out of reach by role.",
|
|
510
|
+
"specification": "model",
|
|
511
|
+
"family": "setup",
|
|
512
|
+
"fix": "Use `vi.stubEnv('X', 'y')`, which restores itself; a CLI default belongs to `specification.cli({ defaults })`.",
|
|
513
|
+
"name": "no-raw-env-write",
|
|
514
|
+
"rationale": "The assignment outlasts the test: the next file in the same worker inherits it, and the failure lands somewhere else.",
|
|
515
|
+
"reach": "tests"
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
"audience": "consumer",
|
|
519
|
+
"channel": "static",
|
|
520
|
+
"sentence": "A production module never reaches test material. A production module is every source module that is no test material itself (`src/`, `lint/`, `bin/`, `scripts/` and a project's root modules alike) but a tool's config (`*.config.*`) and a module under a `__tests__/` folder or a separate test root (a root `test/` or `tests/`, `test-in-its-tests-folder`'s); test material is a test package (`@jterrazz/test`, `vitest`, `@vitest/*`, `vitest-mock-extended`, `vitest-browser-*`, `msw`, `@testing-library/*`, `playwright`, `playwright-core`, `@playwright/test`), a `*.test.*`, `*.test-d.*`, `*.spec.*`, `*.specification.*`, `*.fixtures.*` or `*.render.*` module, anything under a `__fixtures__/`, `__expected__/`, `__requests__/` or `__seeds__/` folder or under `specs/`, and a stub: a `*.stubs.ts` facade (an entry at the source root, `src/index.stubs.ts`, is none) and the `stubs/` folder that holds one. Every form reaches: an import, `import type`, a re-export (`export *` included), `import()`, `require()`, `require.resolve()`, `import.meta.resolve()`, a type's `import('...')`; a specifier resolving there through a tsconfig `paths` alias (its `extends` chain followed) or a `#...` entry of the package's `imports`; a path the module builds from where it sits (`new URL('...', import.meta.url)`, `join(import.meta.dirname, ...)`, a template opening on `__dirname`) or reads (`readFileSync('./...')`); and a module that is not production reaching it in turn, in any of these forms, read from its parsed program: a production module on the way is judged for its own references. The four ground names mean ground wherever a test stands on a file, in a module's `__tests__/` as much as under `specs/`. Two exceptions: `@jterrazz/test/oxlint`, the zero-runtime lint entry; and a test package the unit's `jterrazz.config.ts` names under `test.production.allow`, each with the reason its product runs it (`{ playwright: 'the gate drives a browser in production' }`); an entry with no reason leaves the list unread.",
|
|
521
|
+
"specification": "model",
|
|
522
|
+
"family": "imports",
|
|
523
|
+
"fix": "Move the code that needs the reference into a test, move what production needs out of the test material, or inject it as a port.",
|
|
524
|
+
"name": "no-test-code-in-production",
|
|
525
|
+
"rationale": "Keeping test artefacts out of production protects the consumer's application bundle.",
|
|
526
|
+
"reach": "all"
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
"audience": "consumer",
|
|
530
|
+
"channel": "static",
|
|
531
|
+
"sentence": "A service or a stub is imported from its subpath, never from the root: `postgres`, `redis`, `sqlite`, `process`, `ProcessHandle` and the types of a service, a database or a container from `@jterrazz/test/services`; `anthropic`, `openai`, `http`, `defineStub`, `defineStubs`, `intercept` and the types of a stub, a filter or an interception from `@jterrazz/test/stubs`. An import or a re-export from `@jterrazz/test` naming one is an error at the name, and so is a member of a namespace import of the root (`t.postgres`). The fix moves each name to its subpath, a type-only import staying type-only; a namespace import is left to its reader.",
|
|
532
|
+
"specification": "model",
|
|
533
|
+
"family": "imports",
|
|
534
|
+
"fix": "Import the name from its subpath, `@jterrazz/test/services` or `@jterrazz/test/stubs`; `typescript fix` moves a named import, and a namespace import is rewritten by hand.",
|
|
535
|
+
"name": "service-or-stub-from-its-subpath",
|
|
536
|
+
"rationale": "The root is what writes a spec. Filed at their subpaths, the services a spec starts and the stubs it serves are read off its imports, and a test that imports the root for a `file()` loads no service constructor and no stub builder.",
|
|
537
|
+
"reach": "all"
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
"audience": "consumer",
|
|
541
|
+
"channel": "static",
|
|
542
|
+
"sentence": "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.",
|
|
543
|
+
"specification": "component",
|
|
544
|
+
"family": "setup",
|
|
545
|
+
"fix": "A rendered thing is a `.test.tsx` in the `__tests__/` of its component's folder, collected by `component()`.",
|
|
546
|
+
"name": "no-dom-in-module-test",
|
|
547
|
+
"rationale": "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.",
|
|
548
|
+
"reach": "module"
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
"audience": "consumer",
|
|
552
|
+
"channel": "static",
|
|
553
|
+
"sentence": "The test of `<dir>/<file>.ts` is `<dir>/__tests__/<file>.test.ts`: the conventional folder of its module's folder, holding its tests, the typed data they read (`<file>.fixtures.ts`) and the ground they stand on. A `*.fixtures.ts` outside `specs/` is test material and sits in a `__tests__/` too: its module's, or (when the module tests of several folders share it) the one of the lowest folder they share, as a `__setup__/` does under `specs/`; anywhere else it is an error. A module test outside a `__tests__/` is an error, the fix naming the folder it moves to; one in a `__tests__/` naming no module of the folder above it, and a package's `tests/` or `test/` root, are errors. A RENDERED unit answers to the same law: a `.test.tsx` names the `.tsx` it renders, or the `.ts` of the hook or DOM function whose Host it carries. A specs tree holds no `__tests__/`: a rendered unit under `specs/` keeps its component beside it, and a `module`-role file there is a repository suite: it covers a tree, so it names no module.",
|
|
554
|
+
"specification": "model",
|
|
555
|
+
"family": "layout",
|
|
556
|
+
"fix": "Move the test into the `__tests__/` of the folder of the unit it covers, under the same basename, and each relative path it imports one folder up; a `*.fixtures.ts` into the `__tests__/` of the lowest folder its readers share.",
|
|
557
|
+
"name": "test-in-its-tests-folder",
|
|
558
|
+
"rationale": "Neighbouring tests (parity with Go's `foo_test.go`) keep a test and its code together and discoverable.",
|
|
559
|
+
"reach": "tests"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
"audience": "consumer",
|
|
563
|
+
"channel": "static",
|
|
564
|
+
"sentence": "A test doubles a PORT, never a module: `vi.mock`/`vi.doMock` of a specifier outside the `modules` allow-list (each entry `{ module, reason }`, and an entry with no reason clears nothing), a `__mocks__/` directory, and, in a `module`-role test, importing a data asset (`.json`, `.sql`, `.yaml`, ...) are all errors, a JSON file imported `with { type: 'json' }` excepted: it is data the product reads as it does, not a mock. 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.",
|
|
565
|
+
"specification": "model",
|
|
566
|
+
"family": "mocks",
|
|
567
|
+
"fix": "Double the port with `mockOf<Port>()`, keep a module test's payload in a `__tests__/<file>.fixtures.ts` beside it and a spec's in `__fixtures__/`, and allow-list a native module as `{ module, reason }` (Module tests § Doubles).",
|
|
568
|
+
"name": "no-module-mock",
|
|
569
|
+
"rationale": "A module test that reaches for a real file or a module mock is describing an assembled product, and that has a specification of its own.",
|
|
570
|
+
"reach": "tests"
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
"audience": "consumer",
|
|
574
|
+
"channel": "static",
|
|
575
|
+
"sentence": "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 is out of reach: the double is staging the world, not the test waiting on it. A double is a factory call (`vi.fn(...)`, `mockImplementation(...)`, a `mockOf` handler) or a function-valued PROPERTY of an object literal, which is the plainest double there is: `const git: GitGateway = { cloneRepository: async () => ... }`, where the object itself is the double.",
|
|
576
|
+
"specification": "model",
|
|
577
|
+
"family": "clock",
|
|
578
|
+
"fix": "Wait for the condition, not for a duration.",
|
|
579
|
+
"name": "no-sleep",
|
|
580
|
+
"rationale": "A fixed sleep makes tests slow and flaky; waiting on a condition is deterministic.",
|
|
581
|
+
"reach": "tests"
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
"audience": "consumer",
|
|
585
|
+
"channel": "static",
|
|
586
|
+
"sentence": "A `beforeEach`/`beforeAll` in a test file is an error: the Given is built in the test. An `afterEach`/`afterAll` gives back what the tests took (a temp directory removed, a server stopped, a mock restored) and stands.",
|
|
587
|
+
"specification": "model",
|
|
588
|
+
"family": "chain",
|
|
589
|
+
"fix": "Call a function from each test instead of a hook; `clock.at()` and `intercept()` restore themselves.",
|
|
590
|
+
"name": "given-in-the-test",
|
|
591
|
+
"rationale": "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.",
|
|
592
|
+
"reach": "tests"
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
"audience": "consumer",
|
|
596
|
+
"channel": "static",
|
|
597
|
+
"sentence": "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()` stands in the callback; assertions live in the Then, on the returned result.",
|
|
598
|
+
"specification": "model",
|
|
599
|
+
"family": "visitor",
|
|
600
|
+
"fix": "Move the assertion out of the callback, onto the result the terminal action resolves to.",
|
|
601
|
+
"name": "no-expect-in-scenario",
|
|
602
|
+
"rationale": "Separating interaction from assertion keeps the grammar of setup, action and result intact and the scenarios replayable.",
|
|
603
|
+
"reach": "tests"
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
"audience": "consumer",
|
|
607
|
+
"channel": "static",
|
|
608
|
+
"sentence": "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.",
|
|
609
|
+
"specification": "model",
|
|
610
|
+
"family": "visitor",
|
|
611
|
+
"fix": "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()`.",
|
|
612
|
+
"name": "test-id-states-what-is-missing",
|
|
613
|
+
"rationale": "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.",
|
|
614
|
+
"reach": "tests"
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
"audience": "consumer",
|
|
618
|
+
"channel": "static",
|
|
619
|
+
"sentence": "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.",
|
|
620
|
+
"specification": "model",
|
|
621
|
+
"family": "visitor",
|
|
622
|
+
"fix": "End on `visitor.see(<what the action produced>)` or `visitor.gone(<what it removed>)`.",
|
|
623
|
+
"name": "scenario-ends-settled",
|
|
624
|
+
"rationale": "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.",
|
|
625
|
+
"reach": "specs"
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
toolchain: [
|
|
629
|
+
{
|
|
630
|
+
"audience": "consumer",
|
|
631
|
+
"channel": "toolchain",
|
|
632
|
+
"sentence": "A test file never imports another test file.",
|
|
633
|
+
"specification": "model",
|
|
634
|
+
"family": "imports",
|
|
635
|
+
"fix": "Under `specs/`, move what the two read into the `__setup__/` of the lowest folder they share; for a module test under `src/`, into a `__tests__/<file>.fixtures.ts` for typed input data.",
|
|
636
|
+
"name": "no-test-to-test-import",
|
|
637
|
+
"rationale": "Sharing between tests goes through a module that prepares them (a `__setup__/` module under `specs/`, a `__tests__/<file>.fixtures.ts` under `src/`), not through test-to-test imports that couple the files.",
|
|
638
|
+
"reach": "tests"
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
"audience": "consumer",
|
|
642
|
+
"channel": "toolchain",
|
|
643
|
+
"sentence": "A `*.fixtures.ts` is importable only from a test file.",
|
|
644
|
+
"specification": "model",
|
|
645
|
+
"family": "imports",
|
|
646
|
+
"fix": "Move the value into production code if production needs it; otherwise import it from a test.",
|
|
647
|
+
"name": "fixtures-imported-by-tests-only",
|
|
648
|
+
"rationale": "Confining fixtures to tests stops test data leaking into production code.",
|
|
649
|
+
"reach": "all"
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
"audience": "consumer",
|
|
653
|
+
"channel": "toolchain",
|
|
654
|
+
"sentence": "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, one layer per seam (`no-second-test-runtime-<seam>`), 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.",
|
|
655
|
+
"specification": "model",
|
|
656
|
+
"family": "imports",
|
|
657
|
+
"fix": "Go through what replaces the seam: a specification, the visitor's vocabulary, `intercept()`, `clock` or `mockOf`.",
|
|
658
|
+
"name": "no-second-test-runtime",
|
|
659
|
+
"rationale": "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.",
|
|
660
|
+
"reach": "tests"
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
"audience": "consumer",
|
|
664
|
+
"channel": "toolchain",
|
|
665
|
+
"sentence": "An import of `@jterrazz/test/<subpath>` is an error unless the package's `exports` map publishes the subpath: `./services` and `./stubs` for a spec, beside the root, and `./vitest`, `./config`, `./oxlint` and `./schema` for the tools. The list is READ from the manifest, never copied into the rule, so a published entry is exempt the day it ships; an unpublished subpath is the import the cruise cannot resolve through the `exports` map.",
|
|
666
|
+
"specification": "model",
|
|
667
|
+
"family": "imports",
|
|
668
|
+
"fix": "Import the name from the entry that publishes it: `@jterrazz/test`, `@jterrazz/test/services` or `@jterrazz/test/stubs`.",
|
|
669
|
+
"name": "no-unpublished-subpath",
|
|
670
|
+
"rationale": "A closed list of entries keeps the public API explicit and the internal modules invisible.",
|
|
671
|
+
"reach": "all"
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
"audience": "consumer",
|
|
675
|
+
"channel": "toolchain",
|
|
676
|
+
"sentence": "The specs root holds its specification folders and the special folders of what they share alone (a repository suite is a first-level folder too): code every specification shares is `<specs>/__setup__/`, data the ground at the specs root. A file directly under it that is no test, no spec, no document and no runner is an error, save the four files of a unit (`package.json`, `jterrazz.config.ts`, `tsconfig.json` and `vitest.config.ts`), which a workspace's own suite, a member at `specs/`, keeps. A spec or a runner at the root is `specs-at-declared-depth`'s, and a path into a sibling's ground `no-reach-into-sibling-ground`'s.",
|
|
677
|
+
"specification": "model",
|
|
678
|
+
"family": "layout",
|
|
679
|
+
"fix": "Move code into `<specs>/__setup__/` (or the `__setup__/` of the one specification that reads it) and data into the ground folder of its role.",
|
|
680
|
+
"name": "specs-root-holds-specifications-and-shared",
|
|
681
|
+
"rationale": "The first listing a reader opens names the specifications: a helper or a data file beside them is a second kind of thing at the one level where every entry is a constructor.",
|
|
682
|
+
"reach": "specs"
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
"audience": "consumer",
|
|
686
|
+
"channel": "toolchain",
|
|
687
|
+
"sentence": "A folder under a specs tree whose name starts with `_` is one of the closed list of special folders (`__setup__/`, `__expected__/`, `__fixtures__/`, `__requests__/`, `__seeds__/`) and any other `_` name is an error at the folder, the closed list in its fix: an unknown `__<word>__/` (`__mocks__/`, `__snapshots__/`, `__helpers__/`, `__tests__/`), a subject that starts with `_`, and an earlier spelling (`_setup/`, `_fixtures/`, `_expected/`, `_requests/`, `_seeds/`, whose fix names its special folder, and `_shared/`, whose folders sit at the specs root now). What sits inside a `__setup__/` is `reserved-folder-in-closed-list`'s, and ground is never entered.",
|
|
688
|
+
"specification": "model",
|
|
689
|
+
"family": "layout",
|
|
690
|
+
"fix": "Rename the folder to the special folder that says what it holds (an earlier `_<name>/` to its `__<name>__/`) and the paths that name it; move what a `_shared/` held into the special folders at the specs root; drop the `_` from a subject.",
|
|
691
|
+
"name": "special-folder-in-closed-list",
|
|
692
|
+
"rationale": "A route, a command or a job names its subject folder, so a subject can take any word: only a marker keeps the folders that are no subject from colliding with one, and only a closed list keeps the marker meaning something, and the marker is the `__<word>__/` the ecosystem already reads as no route (Jest's `__tests__/`, `__mocks__/`).",
|
|
693
|
+
"reach": "specs"
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
"audience": "consumer",
|
|
697
|
+
"channel": "toolchain",
|
|
698
|
+
"sentence": "A `.spec.ts` (the ASSEMBLED product) sits under a `specs/` tree; one in any other folder is an error at the file. One at a project's own root is `file-suffix-matches-place`'s.",
|
|
699
|
+
"specification": "model",
|
|
700
|
+
"family": "layout",
|
|
701
|
+
"fix": "Move it under `specs/<specification>/`, or rename it `.test.ts` in the `__tests__/` of the folder of the module it covers.",
|
|
702
|
+
"name": "spec-file-in-a-specs-tree",
|
|
703
|
+
"rationale": "The suffix says the kind: a `.spec.ts` outside the specs tree is a spec nobody running the specifications finds, or a unit test wearing the wrong name.",
|
|
704
|
+
"reach": "all"
|
|
705
|
+
}
|
|
706
|
+
],
|
|
707
|
+
checker: [
|
|
708
|
+
{
|
|
709
|
+
"audience": "consumer",
|
|
710
|
+
"channel": "checker",
|
|
711
|
+
"sentence": "A folder under a specs tree whose name starts with `_` is never a subject, and the special folders form a closed list: `__setup__/` (the TypeScript that prepares specs), `__seeds__/`, `__requests__/`, `__expected__/`, `__fixtures__/` (the ground); at the specs root, they hold what the specifications share, as a folder's hold what its leaves share. Any other `_` name is `special-folder-in-closed-list`'s. A folder with a special name but no marks (`setup/`, `seeds/`, `requests/`, `expected/`, `fixtures/`, and `doubles/` for test adapters) is an error wherever it holds no spec, the fix naming its special folder (`__setup__/` for `doubles/`), and so is a `shared/`, whose fix is the special folders of the folder that encloses it; one holding a spec is a subject (`GET /requests` files under `api/requests/list/`) and passes, except directly under the specs root, which holds no subject. Inside a `__setup__/` the names are the code's own and no bare reserved name is judged, but `__setup__/` holds code alone: a ground folder in it is an error, and so is any file that is no module (`.ts`, `.tsx`, `.js`, `.mjs`, a `.d.ts` and their kin), the fix naming the ground folder of its role beside that `__setup__/`: `__seeds__/` for a `.sql`, `__requests__/` for a `.http`, `__expected__/` for what a test compares against, `__fixtures__/` for any other input. Ground is never entered: what a fixture holds is its own.",
|
|
712
|
+
"specification": "model",
|
|
713
|
+
"family": "layout",
|
|
714
|
+
"fix": "Rename the folder to the special folder that says what it holds, and the paths that name it; move what a `shared/` holds into the special folders of the folder that encloses it.",
|
|
715
|
+
"name": "reserved-folder-in-closed-list",
|
|
716
|
+
"rationale": "A route, a command or a job names its subject folder, so a subject can take any word: only a marker keeps the folders that are no subject from colliding with one, and only a closed list keeps the marker meaning something.",
|
|
717
|
+
"reach": "specs"
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
"audience": "consumer",
|
|
721
|
+
"channel": "checker",
|
|
722
|
+
"sentence": "Under a specs tree, a module (`.ts`, `.tsx`, `.js`, `.mjs` and their kin) that is no spec (`*.spec.ts`), no test (`*.test.ts`, `*.test.tsx`) and no runner (`*.specification.ts`) sits under a `__setup__/`: a test adapter, a shared value, a helper, a stub facade and its kind folders (`__setup__/stubs/`). Ground is out of reach, and so is a rendered unit (the `.tsx`, or the `.ts` of a hook) beside the `*.test.tsx` of the same name that renders it (`test-in-its-tests-folder`): it is that test's subject, not code that prepares it. A `*.fixtures.ts` is an error anywhere under the tree, `__setup__/` included: a fixture is input data (a file in `__fixtures__/`), so a module that prepares specs is named for what it holds. A file directly at the specs root is `specs-root-holds-specifications-and-shared`'s.",
|
|
723
|
+
"specification": "model",
|
|
724
|
+
"family": "layout",
|
|
725
|
+
"fix": "Move the module into the `__setup__/` of the lowest folder its readers share, named for what it holds, and the paths that import it.",
|
|
726
|
+
"name": "spec-code-in-setup-folder",
|
|
727
|
+
"rationale": "A spec folder holds specs, and a reader listing it expects questions asked of the subject: code beside them is a second kind of file mixed in, and a module called `fixtures` promises data it does not hold.",
|
|
728
|
+
"reach": "specs"
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
"audience": "consumer",
|
|
732
|
+
"channel": "checker",
|
|
733
|
+
"sentence": "A `__setup__/` module sits in the `__setup__/` of the LOWEST folder every module importing it shares, the law ground follows (`ground-shared-or-inline`) read on imports. A reader is any module of the tree importing it by a relative path or a `package.json` `imports` alias, and a reader inside a `__setup__/` counts as the folder that `__setup__/` belongs to. Readers meeting only at the specs root share `<specs>/__setup__/`. A module one spec or test file alone imports is an error at the module: it belongs in that file, written in place, unless the two together would pass the 400 lines `spec-file-within-line-limit` holds a spec to (it then stays a file in its reader's `__setup__/`); a module under `stubs/` is excepted, since a stub is imported through its facade. A module no module of the tree imports is out of this check's reach and knip's to report, as an unused file: a config may name it (a vitest `setupFiles` entry), which no import shows. Fixed by `typescript fix`: a module off its readers' lowest folder moves to that folder's `__setup__/` (a `git mv` in a work tree), with its own relative literals, the `'..'` pieces it climbs from its own folder by (`resolve(import.meta.dirname, '..', '..')`), and every importer's path to it rewritten; one a reader reaches through an `imports` alias is left where it is, since the alias is the package's to rewrite; one a single spec reads is written into that spec by its author.",
|
|
734
|
+
"specification": "model",
|
|
735
|
+
"family": "layout",
|
|
736
|
+
"fix": "Run `typescript fix`, which moves it to the `__setup__/` the finding names and rewrites the paths that import it, or write it in its one reader.",
|
|
737
|
+
"name": "setup-module-at-lowest-common-folder",
|
|
738
|
+
"rationale": "Where shared code sits says who shares it: a helper parked above its readers reads as everyone's and one parked beside one of them reads as that leaf's, so the reader cannot trust the folder to tell them what a change reaches.",
|
|
739
|
+
"reach": "specs"
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
"audience": "consumer",
|
|
743
|
+
"channel": "checker",
|
|
744
|
+
"sentence": "A `__setup__/` module whose class `implements` a type it imports from outside the specs tree (the application's port) is a test adapter, and its file is named for that port: `<port>.adapter.ts`, the port's name kebab-cased with a trailing `Port` dropped (`ticker-source.adapter.ts` for `TickerSourcePort`); the class keeps whatever name it has. Where a module's classes implement several ports, any one of their names passes. A double built otherwise (an object literal, a class extending the adapter it replaces) is out of reach, and review names it.",
|
|
745
|
+
"specification": "model",
|
|
746
|
+
"family": "layout",
|
|
747
|
+
"fix": "Rename the file `<port>.adapter.ts` for the port its class implements, and the paths that import it.",
|
|
748
|
+
"name": "adapter-named-for-its-port",
|
|
749
|
+
"rationale": "A test adapter replaces one port of the application, and the port is the name the application gave the seam: named for it, the adapter is found from the port and the port from the adapter, whatever the class is called.",
|
|
750
|
+
"reach": "specs"
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
"audience": "consumer",
|
|
754
|
+
"channel": "checker",
|
|
755
|
+
"sentence": "A `<case>.spec.yaml` is a SPEC, not ground: a document under `__expected__/`, `__requests__/`, `__seeds__/` or `__fixtures__/` is an error.",
|
|
756
|
+
"specification": "cli",
|
|
757
|
+
"family": "layout",
|
|
758
|
+
"fix": "Move the document beside the test that runs it.",
|
|
759
|
+
"name": "no-document-in-ground",
|
|
760
|
+
"rationale": "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.",
|
|
761
|
+
"reach": "document"
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
"audience": "consumer",
|
|
765
|
+
"channel": "checker",
|
|
766
|
+
"sentence": "A test under `specs/<specification>/`, where the specification root holds a `*.specification.ts(x)`, imports that module or constructs a runner itself. Ground is out of reach: nothing under a `__fixtures__/`, `__expected__/`, `__requests__/` or `__seeds__/` 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 specification: a specification with no module to sit beside and no runner to reach lives in a repository suite there.",
|
|
767
|
+
"specification": "model",
|
|
768
|
+
"family": "layout",
|
|
769
|
+
"fix": "Import `{ <specification> }` from `<specification>.specification.js`, move a module test into the `__tests__/` of its module's folder, or move a repository suite to a first-level folder of its own under `specs/`. A module of the specs tree itself has no module test (its `__tests__/` would be a special folder the ground refuses), so it moves out of `specs/` into a code root (`src/`, or one `typescript.roots` declares) with its test.",
|
|
770
|
+
"name": "specification-test-uses-its-runner",
|
|
771
|
+
"rationale": "A test reaching no runner under a specification is a module test paying the specification's budget and services to prove something that belongs in its module's `__tests__/`.",
|
|
772
|
+
"reach": "specs"
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
"audience": "consumer",
|
|
776
|
+
"channel": "checker",
|
|
777
|
+
"sentence": "A first-level folder named for one of the seven specifications holds a `*.specification.ts(x)` calling `specification.<specification>(`. Positive-only: any other first-level name is a repository suite, judged by `specs-at-declared-depth`'s declared depth alone.",
|
|
778
|
+
"specification": "model",
|
|
779
|
+
"family": "layout",
|
|
780
|
+
"fix": "Create the specification the folder promises, or rename the folder.",
|
|
781
|
+
"name": "specification-folder-has-its-runner",
|
|
782
|
+
"rationale": "Folder = constructor is the law the fork rests on; a specification folder with no runner tells the reader something false before they open a file.",
|
|
783
|
+
"reach": "specs"
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
"audience": "consumer",
|
|
787
|
+
"channel": "checker",
|
|
788
|
+
"sentence": "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.",
|
|
789
|
+
"specification": "model",
|
|
790
|
+
"family": "services",
|
|
791
|
+
"fix": "Write the instant the case is about, and pin the run's clock with `.clock()` on the chain.",
|
|
792
|
+
"name": "seeds-state-absolute-time",
|
|
793
|
+
"rationale": "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 estate dry run (the whole catalogue over every repository of the workbench) counts 36, every one of them signews-api's.",
|
|
794
|
+
"reach": "ground"
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
"audience": "consumer",
|
|
798
|
+
"channel": "checker",
|
|
799
|
+
"sentence": "Ground in a leaf holding ≥ 2 node tests (`.test.ts`, `.spec.ts`) that a single one of them reads is an error. A `.test.tsx` tree is out of reach, as it is for `specs-at-declared-depth`, and so are the api, cli and job specifications: there a leaf is its subject's standard name (`spec-in-subject-folder`), its aspect files share it by design, and a folder of one spec's own is one `spec-in-subject-folder` moves the spec back out of.",
|
|
800
|
+
"specification": "model",
|
|
801
|
+
"family": "layout",
|
|
802
|
+
"fix": "Give that spec its own domain folder, with the ground beside it.",
|
|
803
|
+
"name": "ground-beside-its-only-reader",
|
|
804
|
+
"rationale": "Ground one spec reads, parked where several sit, reads as shared: everyone assumes someone else depends on it and nobody dares touch it.",
|
|
805
|
+
"reach": "ground"
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
"audience": "consumer",
|
|
809
|
+
"channel": "checker",
|
|
810
|
+
"sentence": "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.",
|
|
811
|
+
"specification": "model",
|
|
812
|
+
"family": "layout",
|
|
813
|
+
"fix": "Write `checker-disable-next-line <name> -- <reason>`.",
|
|
814
|
+
"name": "checker-suppression-states-a-reason",
|
|
815
|
+
"rationale": "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.",
|
|
816
|
+
"reach": "specs"
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
"audience": "consumer",
|
|
820
|
+
"channel": "checker",
|
|
821
|
+
"sentence": "Every id a `checker-disable-next-line` / `checker-disable-line` directive names is a pass of the checker, or `*`. An id no pass has (misspelled, or retired by a major) is an error at the directive.",
|
|
822
|
+
"specification": "model",
|
|
823
|
+
"family": "layout",
|
|
824
|
+
"fix": "Name the pass the line is the exception to, or delete the directive.",
|
|
825
|
+
"name": "checker-suppression-names-a-pass",
|
|
826
|
+
"rationale": "A directive naming an id no pass has silences nothing, and reads as if it did: a major renames its passes with no bridge, so without this pass every directive naming a retired id would stay in the tree forever, stating an exception nobody holds.",
|
|
827
|
+
"reach": "specs"
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
"audience": "consumer",
|
|
831
|
+
"channel": "checker",
|
|
832
|
+
"sentence": "`environment: 'happy-dom'|'jsdom'` in a test project of `jterrazz.config.ts` is an error. The MEMBER pass is the one channel that holds it: a settings file 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.",
|
|
833
|
+
"specification": "component",
|
|
834
|
+
"family": "setup",
|
|
835
|
+
"fix": "Remove `environment` and collect the rendered tests with `component()`.",
|
|
836
|
+
"name": "no-simulated-dom-in-config",
|
|
837
|
+
"rationale": "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.",
|
|
838
|
+
"reach": "member"
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
"audience": "consumer",
|
|
842
|
+
"channel": "checker",
|
|
843
|
+
"sentence": "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` and the like) are declared by the consumer, by design.",
|
|
844
|
+
"specification": "model",
|
|
845
|
+
"family": "imports",
|
|
846
|
+
"fix": "Remove the declaration: a transitive is already resolved by the package; a retired seam has a specification that replaces it (`component()`, `website()`, `clock`, `intercept()`). The diagnostic carries a sentence for each case.",
|
|
847
|
+
"name": "no-redundant-test-dependency",
|
|
848
|
+
"rationale": "A duplicate declaration lets two versions of one seam coexist, and a retired seam keeps a vocabulary the framework has replaced.",
|
|
849
|
+
"reach": "member"
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
"audience": "consumer",
|
|
853
|
+
"channel": "checker",
|
|
854
|
+
"sentence": "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.",
|
|
855
|
+
"specification": "model",
|
|
856
|
+
"family": "expectations",
|
|
857
|
+
"fix": "Write a token the grammar knows; the message prints the full list.",
|
|
858
|
+
"name": "no-unknown-token",
|
|
859
|
+
"rationale": "A closed grammar shared with the runtime matcher stops the two channels drifting.",
|
|
860
|
+
"reach": "ground"
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
"audience": "consumer",
|
|
864
|
+
"channel": "checker",
|
|
865
|
+
"sentence": "A malformed ref of a known kind (`{{iso8601#}}`, `{{uuid #id}}`) in a text file under `__expected__/` is an error.",
|
|
866
|
+
"specification": "model",
|
|
867
|
+
"family": "expectations",
|
|
868
|
+
"fix": "Write `{{kind#name}}` with no space, and a name for the ref.",
|
|
869
|
+
"name": "no-malformed-ref",
|
|
870
|
+
"rationale": "A malformed capture would fail silently: naming it makes it visible early.",
|
|
871
|
+
"reach": "ground"
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
"audience": "consumer",
|
|
875
|
+
"channel": "checker",
|
|
876
|
+
"sentence": "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__/`.",
|
|
877
|
+
"specification": "api",
|
|
878
|
+
"family": "expectations",
|
|
879
|
+
"fix": "Write the opening line the folder calls for, or move the file to the other one.",
|
|
880
|
+
"name": "http-first-line-follows-its-grammar",
|
|
881
|
+
"rationale": "The opening line is what tells a request from a response: constraining it catches misplaced files.",
|
|
882
|
+
"reach": "ground"
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
"audience": "consumer",
|
|
886
|
+
"channel": "checker",
|
|
887
|
+
"sentence": "A `<case>.spec.yaml` document follows its grammar: closed keys (`description`, `fixture`, `env`, `serve`, `runs`) and closed keys inside a run; `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; every `fixture:` path relative, reaching `__fixtures__/`.",
|
|
888
|
+
"specification": "cli",
|
|
889
|
+
"family": "expectations",
|
|
890
|
+
"fix": "Write the key the grammar names; the message prints the accepted set.",
|
|
891
|
+
"name": "document-follows-its-grammar",
|
|
892
|
+
"rationale": "The grammar is read by the runner's own parser: the document lint accepts is exactly the one the runner runs.",
|
|
893
|
+
"reach": "document"
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
"audience": "consumer",
|
|
897
|
+
"channel": "checker",
|
|
898
|
+
"sentence": "The suffix says the kind, and the tree agrees with it. A `.spec.ts` (the ASSEMBLED product) lives under `specs/<specification>/`: one at a project's own root is an error here, one in any folder `spec-file-in-a-specs-tree`'s. A `.test.ts` (the UNIT) lives in the `__tests__/` of its module's folder; under a specification folder (<specifications>) it is an error, fixable by `typescript fix` (a `git mv`). A `.test.ts` that reaches no runner is `specification-test-uses-its-runner`'s, whose fix is the opposite move, and this rule's fix leaves it alone. A first level that is NOT a specification is a repository suite: it covers a tree, keeps `.test.ts`, and `specs-at-declared-depth`'s declared depth is what judges it, unless the tree carries its `*.specification.ts` at its own ROOT, which makes the whole tree one specification'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.",
|
|
899
|
+
"specification": "model",
|
|
900
|
+
"family": "layout",
|
|
901
|
+
"fix": "Move the `.spec.ts` under `specs/<specification>/` (or rename it `.test.ts` in the `__tests__/` of its module's folder); rename the specification `.test.ts` to `.spec.ts`: `typescript fix` does it with `git mv`, and names every `include`/`exclude` entry of the member the rename broke.",
|
|
902
|
+
"name": "file-suffix-matches-place",
|
|
903
|
+
"rationale": "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.",
|
|
904
|
+
"reach": "specs"
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
"audience": "consumer",
|
|
908
|
+
"channel": "checker",
|
|
909
|
+
"sentence": "A document's `description:` is a title: one line, lowercase initial (an all-caps identifier exempted), no trailing period, under 100 characters.",
|
|
910
|
+
"specification": "cli",
|
|
911
|
+
"family": "layout",
|
|
912
|
+
"fix": "Write it as a title, not as a sentence.",
|
|
913
|
+
"name": "document-description-reads-as-a-title",
|
|
914
|
+
"rationale": "The description IS the vitest title: a title's rules apply to it, not a paragraph's.",
|
|
915
|
+
"reach": "document"
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
"audience": "consumer",
|
|
919
|
+
"channel": "checker",
|
|
920
|
+
"sentence": "Two `<case>.spec.yaml` documents in one directory do not share a `description:`.",
|
|
921
|
+
"specification": "cli",
|
|
922
|
+
"family": "layout",
|
|
923
|
+
"fix": "Name what each case proves; two titles that read alike leave two files to open.",
|
|
924
|
+
"name": "document-description-unique",
|
|
925
|
+
"rationale": "The reporter names the title: two identical titles leave two files to open.",
|
|
926
|
+
"reach": "document"
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
"audience": "consumer",
|
|
930
|
+
"channel": "checker",
|
|
931
|
+
"sentence": "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.",
|
|
932
|
+
"specification": "cli",
|
|
933
|
+
"family": "expectations",
|
|
934
|
+
"fix": "Write the token the message names (`{{workdir}}`, `{{url}}`) in place of the literal.",
|
|
935
|
+
"name": "no-volatile-literal",
|
|
936
|
+
"rationale": "This is exactly the shape a `TEST_UPDATE` run would have tokenised: finding one means a token was overwritten by hand.",
|
|
937
|
+
"reach": "ground"
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
"audience": "consumer",
|
|
941
|
+
"channel": "checker",
|
|
942
|
+
"sentence": "A literal ISO-8601 timestamp or uuid in an expected stream of a `<case>.spec.yaml` is an error, 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 `no-unseeded-value-in-golden`, whose criterion the Conventions page carries. A value a document cannot show where it is pinned (a `serve:` stub's clock, the product's own wording) is declared above its stream, `# checker-disable-next-line pinned-value-from-the-input -- <reason>`: no comment can stand inside a block scalar, so the directive on the stream's key silences every line of it.",
|
|
943
|
+
"specification": "cli",
|
|
944
|
+
"family": "expectations",
|
|
945
|
+
"fix": "Token it, or seed it: a value the document pins is a value it may assert.",
|
|
946
|
+
"name": "pinned-value-from-the-input",
|
|
947
|
+
"rationale": "The defect is not the literal: it is the literal nothing pinned.",
|
|
948
|
+
"reach": "document"
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
"audience": "consumer",
|
|
952
|
+
"channel": "checker",
|
|
953
|
+
"sentence": "An expected stream whose whole content is `{{any}}` is an error: the run executes and proves nothing.",
|
|
954
|
+
"specification": "cli",
|
|
955
|
+
"family": "expectations",
|
|
956
|
+
"fix": "Regenerate with `TEST_UPDATE=1` and keep tokens only for the parts that move.",
|
|
957
|
+
"name": "no-empty-expected-stream",
|
|
958
|
+
"rationale": "The `vitest/expect-expect` mirror for documents: an assertion that accepts anything is an absent assertion.",
|
|
959
|
+
"reach": "document"
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
"audience": "consumer",
|
|
963
|
+
"channel": "checker",
|
|
964
|
+
"sentence": "A run whose `exit:` is non-zero with neither `stdout:` nor `stderr:` is an error: the document does not say why the command refused.",
|
|
965
|
+
"specification": "cli",
|
|
966
|
+
"family": "expectations",
|
|
967
|
+
"fix": "Write the refusal the command prints: that is what the case proves.",
|
|
968
|
+
"name": "no-silent-refusal",
|
|
969
|
+
"rationale": "A silent refusal is either a defect in the product, or a golden that forgot the words went to stderr.",
|
|
970
|
+
"reach": "document"
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
"audience": "consumer",
|
|
974
|
+
"channel": "checker",
|
|
975
|
+
"sentence": "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).",
|
|
976
|
+
"specification": "cli",
|
|
977
|
+
"family": "setup",
|
|
978
|
+
"fix": "Register the name in the specification, or fix the spelling.",
|
|
979
|
+
"name": "document-env-and-serve-registered",
|
|
980
|
+
"rationale": "A typo (`serve: dashbord`) fails at run time today, a test file later; here it is one lint line.",
|
|
981
|
+
"reach": "document"
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
"audience": "consumer",
|
|
985
|
+
"channel": "checker",
|
|
986
|
+
"sentence": "A known token in a file under `__requests__/` is an error: requests are inputs, never matched.",
|
|
987
|
+
"specification": "api",
|
|
988
|
+
"family": "expectations",
|
|
989
|
+
"fix": "Write the literal value the request actually sends.",
|
|
990
|
+
"name": "no-token-in-request",
|
|
991
|
+
"rationale": "A token in an input is neither validated nor substituted: it is almost always a mistake.",
|
|
992
|
+
"reach": "ground"
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
"audience": "consumer",
|
|
996
|
+
"channel": "checker",
|
|
997
|
+
"sentence": "No dead fixtures: every file under `__seeds__/`/`__requests__/`/`__fixtures__/` and every top-level entry of `__expected__/` must be referenced: a folder's by the tests at or below it, the specs root's shared four by any test of the tree; a folder holding ground with no test file and no `*.spec.yaml` at or below it is an orphan. The ground inside a module test's `__tests__/` is held the same way: an entry no test of that folder names is dead. A reference is a path RESOLVED to the entry, never a name: a document's keys and a `file()`/`directory()` argument from the file that writes them, the words of any other literal from that file and from the project root, a template literal's table from the folders written before its hole, so `./__seeds__/x.sql` in a leaf vouches for that leaf's `__seeds__/x.sql`, and not for `<specs>/__seeds__/x.sql`. A `<case>.spec.yaml` references the files every path key of it names.",
|
|
998
|
+
"specification": "model",
|
|
999
|
+
"family": "layout",
|
|
1000
|
+
"fix": "Reference it from the spec it stands under, or delete it.",
|
|
1001
|
+
"name": "no-unused-ground",
|
|
1002
|
+
"rationale": "The `named-path-exists` mirror: a fixture nothing references is dead weight that misleads the reader.",
|
|
1003
|
+
"reach": "ground"
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
"audience": "consumer",
|
|
1007
|
+
"channel": "checker",
|
|
1008
|
+
"sentence": "A ground file exists only if it is shared, and a value lives in a test only while it stays short. The checker counts the TESTS that read each file of the specs trees, and of the ground a module test stands on inside its `__tests__/` (`src/render/__tests__/__expected__/`), a `test()`/`it()` whose body names it with a literal `file()`/`directory()`, a document, and judges its level. Read by ONE test, named by no module test of the member, and read by no document's `fixture:` line, no import and no path joined to `import.meta.dirname`, a file of `__expected__/` or `__requests__/` is an error where the test names it: it belongs in the test, written in place (a golden as the value `toMatch` compares, a request as its text or its body value) unless it is a directory tree, a binary file, a text no literal survives in place (one opening on a line break whose lines share an indentation, which the comparison's dedent strips from the template and the quoted form alike), or longer than its role's cap: 30 lines for `__requests__/`, 150 for `__expected__/` (a JSON file counted as the value it becomes). A golden compared `{ frozen: true }` stays a file, as the bytes are then the contract; one that is a byte copy of a file the repository keeps is reported to be compared with that source, and the fixer leaves it. The caps hold the other way too: a request or a body written in the test past 30 lines, an expectation past 150, is an error where it is written, and belongs in a file beside the test. `__seeds__/` has no form in place: SQL is never written in a test, and SQL tagged in one is an error where the tag stands. A seed is a `.sql` file, beside its test in `./__seeds__/` when one test reads it, never an error for being unshared. `__fixtures__/` is file state a program reads, never a value, and stays a file. Read by several tests, a file lives in the ground of the LOWEST folder its readers share, the law `setup-module-at-lowest-common-folder` holds code to: beside them when they are one leaf's, in `events/__seeds__/` when `events/list/` and `events/get/` read it, in a route's own when the route and the routes under it read it (`map/pois/` and `map/pois/detail/`), and in the specs root's shared ground (`<specs>/__seeds__/`, ...) only when they meet at the root, across specifications. An entry kept above that folder (a pool entry one specification alone reaches among them) is an error where it sits, and belongs in that folder's ground of the same role, and every reader counts where it stands: a test at its folder, a runner at its specification root, a `__setup__/` module at the folder that owns that `__setup__/`. Only a reader outside the specs tree keeps an entry where it is whatever the specs say: a module test of the member, by a relative literal, the literal pieces `named-path-exists` reads or a whole literal spelled from the member root (`'specs/...'`), and the member's Makefile or vitest config, by a path spelled from that root. `no-reach-into-sibling-ground` refuses the path that leaves a leaf for a sibling's, and `no-duplicate-ground` finds copies. A reference outside every test (a `const` of the file, a specification, a helper) counts as shared, and zero references is `no-unused-ground`'s. Fixed by `typescript fix`: SQL a test holds is written to `./__seeds__/<name>.sql` beside it (named by its first `--` comment, kebab-cased, else by the first four meaningful words of its test's title) and named with `file()`; a value past its cap is written to `./__requests__/` or `./__expected__/` beside its test, named by the test's title, and named with `file()`, never one a plain value is compared to (`expect(message).toMatch(...)`, a name or a string literal as the subject): a string has no golden, and the finding names `text()` to wrap it with; a single-use file whose one reference is a literal `file()` handed to `.request(`, `.post(`/`.put(` or `toMatch(` is written in place and deleted (a `file` import no longer used dropped, an emptied ground folder removed); an entry kept above the lowest folder its readers share is moved into that folder's ground (a `git mv` in a work tree) and the literals rewritten from each referrer.",
|
|
1009
|
+
"specification": "model",
|
|
1010
|
+
"family": "layout",
|
|
1011
|
+
"fix": "Run `typescript fix`: it writes SQL and a value past its cap to a file beside the test, writes a single-use file into its one test and deletes it, and moves an entry kept above its readers down to the lowest folder they share. A test whose subject IS the file form keeps it, with a reasoned `checker-disable-next-line ground-shared-or-inline`.",
|
|
1012
|
+
"name": "ground-shared-or-inline",
|
|
1013
|
+
"rationale": "A file a test reads is read at a distance: the reader opens a second file to know what the test stands on or expects, and a file one test reads pays that price for nothing, until the value is too long to read in the test, where the file is the clearer page. SQL is the exception at any length: a seed is database state a reader opens as a file, and SQL in a TypeScript string is checked by nothing. A single scenario's file parked in the pool is worse still: it reads as shared, so nobody dares change it.",
|
|
1014
|
+
"reach": "ground"
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
"audience": "consumer",
|
|
1018
|
+
"channel": "checker",
|
|
1019
|
+
"sentence": "Every chain of a spec file reaches ONE subject, read statically: an api chain its method and route template (`GET /events/:id`, the request line of a `.http` a `.request(file())` sends), a job chain the job it triggers, a cli chain its command words, a website chain the page it visits, a mobile chain the journey it opens. A mobile test that opens the app a second time (the relaunch that proves what its first open left behind) reaches the journey of its first open alone. A file reaching several is an error at the first chain that reaches a second. The finding prints the plan: the tests each subject takes, and the folder it files under, the subject's standard folder under the specification as `spec-in-subject-folder` names it. The split is its author's, who names each file: `typescript fix` writes none, since naming a file is not a rewrite a tool can take. A test whose own chains reach two subjects is reported and left. A spec of a SPECIMEN is out of reach, one whose runner drives an app under a `__fixtures__/` (its specification's or the pool's) that its specification module, or the one that module imports a runner from, names in a literal: its routes, jobs and commands are no subject of the project.",
|
|
1020
|
+
"specification": "model",
|
|
1021
|
+
"family": "layout",
|
|
1022
|
+
"fix": "Split it by hand along the plan the finding prints, one file per subject, each named by you. A test reaching two subjects states what it stands on with a seed instead of a second action.",
|
|
1023
|
+
"name": "one-subject-per-spec",
|
|
1024
|
+
"rationale": "A file about one subject is found by its subject: the reader of `GET /events/:id` opens one folder and reads every question asked of it, and a change to that route touches one file.",
|
|
1025
|
+
"reach": "spec"
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
"audience": "consumer",
|
|
1029
|
+
"channel": "checker",
|
|
1030
|
+
"sentence": "A spec file about one subject sits in the subject's standard folder, derived and never listed: `GET /events` files under `events/list/`, `GET /events/:id` under `events/get/`, `POST /events` under `events/create/`, `PUT|PATCH /events/:id` under `events/update/`, `DELETE /events/:id` under `events/delete/`, a route under an item (`GET /events/:id/timeline`) its literal segments (`events/timeline/`), a singleton (`GET /health`) `health/get/`, a method-less route, one `jterrazz.config.ts` names so (`test: { methodless: { '/mcp': 'the MCP transport answers GET and POST' } }`), reached by whatever verb a test sends, and never split by it (`one-subject-per-spec`), its segments (`mcp/`); a job its name (`specs/job/<job>/`), a cli command its first word (`specs/cli/<word>/`), or every leading word the `commands` list of the runner its chain's handle came from holds (`specification.cli(bin, { commands: ['db', 'migrate'] })` files `db migrate` under `db/migrate/`; the list written in the call or bound to a `const` of the runner's file). A route of an app mounted under a base path `jterrazz.config.ts` declares (`test: { basePath: '/api' }`) is filed by its path past that base: `GET /api/posts` files under `posts/list/`, never `api/posts/list/`. A GET on one segment lists a plural noun, or a collection `jterrazz.config.ts` names (`test: { collections: ['data'] }`), and gets anything else; a segment is a parameter when the chain writes the route's template (a `:name` segment, or `{ params }` beside the path, `route-written-as-template`), or when it is a template hole or anything but lowercase words. A filled path whose word sits right after a collection with more path after it (`btc` in `/indicators/btc/chart`) reads two ways: it is reported at its chain, naming the template to write (or `{ params: {} }` when the word is the route's own) and never moved; a `.http` request line is read as the route. A page, a journey and the root route have no standard name, and are not judged, and neither is a spec of a specimen (`one-subject-per-spec`). An error at the file's first line; fixed by `typescript fix`, which moves the file whole, rebasing the paths of its `file()`, `directory()`, imports, `vi.mock()` and `new URL(..., import.meta.url)`, and leaving the file to its author when a relative path no rebase carries would break, and removes the folders the move leaves empty.",
|
|
1031
|
+
"specification": "model",
|
|
1032
|
+
"family": "layout",
|
|
1033
|
+
"fix": "Run `typescript fix`: it moves the file to its subject's folder, and the ground it stands on with it.",
|
|
1034
|
+
"name": "spec-in-subject-folder",
|
|
1035
|
+
"rationale": "A name derived from the subject is the same in every repository: nobody lists the folders, nobody argues them, and a reader who knows the route knows the path.",
|
|
1036
|
+
"reach": "spec"
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
"audience": "consumer",
|
|
1040
|
+
"channel": "checker",
|
|
1041
|
+
"sentence": "Ground several leaves share lives once: an entry of a leaf's ground (a file, or a tree compared file by file) that another leaf holds byte for byte under the same role, whatever either is called, is an error on every copy; it belongs once, in the ground of the lowest folder the leaves share (`events/__seeds__/<name>` for `events/list/` and `events/get/`, `<specs>/__seeds__/<name>` across specifications), named by the path that climbs to it. Fixed by `typescript fix` when the copies share one name: one copy is moved there (a `git mv` in a work tree), the others are deleted, and every whole relative literal that named a copy is rewritten (a literal of the specs tree's sources, a key of its documents, a literal of the member's module tests); a path built from pieces, or spelled into a command, is the author's to rewrite, `named-path-exists` names one built from literal pieces once the copy has moved. Copies of several names, and a ground already holding the name with other content, are left to the author.",
|
|
1042
|
+
"specification": "model",
|
|
1043
|
+
"family": "layout",
|
|
1044
|
+
"fix": "Run `typescript fix`, or keep the copies apart on purpose and record them in the ratchet.",
|
|
1045
|
+
"name": "no-duplicate-ground",
|
|
1046
|
+
"rationale": "Copies drift: one leaf's seed gains a column, the other's does not, and the reader of either cannot tell a difference meant from one forgotten.",
|
|
1047
|
+
"reach": "ground"
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
"audience": "consumer",
|
|
1051
|
+
"channel": "checker",
|
|
1052
|
+
"sentence": "A path leaves its leaf only by climbing: into the ground of a folder that encloses it, the specs root's included, whose `<specs>/<role>/` holds what the specifications share. A path a `file()`, a `directory()` or any path key of a document names, whose target is neither under the spec's own directory, nor under one of `<specs>/__fixtures__/`, `<specs>/__seeds__/`, `<specs>/__requests__/`, `<specs>/__expected__/` (the four roles the specs root keeps beside its specifications) nor under the four of a folder above it (`map/pois/detail/` reads `../__seeds__/` of `map/pois/`, and `events/list/` and `events/get/` read `../__seeds__/` of `events/`), is an error; ground shared by several leaves goes to the lowest folder they share, in the folder of its role (`verb-reads-its-ground-folder` judges which, `ground-shared-or-inline` which folder). A leaf's ground reached from a sibling leaf is one such path, so this row holds the sibling case too.",
|
|
1053
|
+
"specification": "model",
|
|
1054
|
+
"family": "layout",
|
|
1055
|
+
"fix": "Copy it under this leaf's own ground, or move it to the ground of the lowest folder the leaves share: `<specs>/<role>/` when they span specifications.",
|
|
1056
|
+
"name": "no-reach-into-sibling-ground",
|
|
1057
|
+
"rationale": "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.",
|
|
1058
|
+
"reach": "ground"
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
"audience": "consumer",
|
|
1062
|
+
"channel": "checker",
|
|
1063
|
+
"sentence": "Every `file()` and `directory()` a test names with a literal path (a spec under `specs/` and a module test in its `__tests__/` alike), and every path a document's `fixture:` names, names an entry that exists inside a git work tree, one git does not ignore, of the kind the wrapper says, resolved from the test file's directory: a missing or ignored path, a `file()` naming a directory or a `directory()` naming a file is an error, reported where the path is written. So does a path a test resolves from its own directory with literal pieces alone (`resolve(import.meta.dirname, ...)`, `join(...)` alike, `__dirname` alike, `new URL('...', import.meta.url)`), of either kind: several pieces are read joined (`resolve(import.meta.dirname, '..', '__fixtures__', 'gone')`), one piece only when it is spelled `./` or `../`, as a `file()` is. A lone bare piece (`resolve(import.meta.dirname, '__expected__')`) is an output folder a test makes as often as a path it reads, and is not read. A path under the unit's build output (`dist/`, `.artifacts/<tool>/`) is accepted missing, in a spec and in its specification alike: the kind's test builds before it runs. The tree pass reads the `specs/` trees, the member pass every other test of the member. A computed piece leaves its whole path out of reach, as a computed path is, a path spelled into a command a process runs is the author's, and the runtime refuses it with the path it resolved to; a call written inside a string is text, and is not read.",
|
|
1064
|
+
"specification": "model",
|
|
1065
|
+
"family": "layout",
|
|
1066
|
+
"fix": "Create the entry (out of `.gitignore`), or fix the path: the message names what it resolved to.",
|
|
1067
|
+
"name": "named-path-exists",
|
|
1068
|
+
"rationale": "A typo in a path fails a run, a test later, far from the line that made it; statically it is one report line at the path.",
|
|
1069
|
+
"reach": "ground"
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
"audience": "consumer",
|
|
1073
|
+
"channel": "checker",
|
|
1074
|
+
"sentence": "One word per meaning: a folder named `snapshots/`, `__snapshots__/`, `golden/` or `goldens/` is an error anywhere in a member when a test would stand on it; any such folder under `specs/`, and elsewhere one that holds no module of code (a `goldens/` folder of matcher source is named for what its code does). What it would hold is what a test asserts, and that is `__expected__/`. Ground is not entered: a fixture project may carry the word on purpose.",
|
|
1075
|
+
"specification": "model",
|
|
1076
|
+
"family": "layout",
|
|
1077
|
+
"fix": "Rename the folder `__expected__/`, and the paths that name it.",
|
|
1078
|
+
"name": "no-snapshot-folder",
|
|
1079
|
+
"rationale": "Four reserved names are a vocabulary only while nobody adds a fifth: a second word for the goldens is a second place a reader has to look, and the rules that read `__expected__/` never see it.",
|
|
1080
|
+
"reach": "all"
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
"audience": "consumer",
|
|
1084
|
+
"channel": "checker",
|
|
1085
|
+
"sentence": "Docker-aware runners are inferred from the `docker:` option of the imported specification file, and every `.exec()` result bound without `await using` is reported.",
|
|
1086
|
+
"specification": "model",
|
|
1087
|
+
"family": "setup",
|
|
1088
|
+
"fix": "Bind the result with `await using`, so the containers the run spawned are disposed with the scope.",
|
|
1089
|
+
"name": "docker-exec-bound-with-await-using",
|
|
1090
|
+
"rationale": "Inferring the runners removes the hand-maintained list an oxlint rule would need.",
|
|
1091
|
+
"reach": "tests"
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
"audience": "consumer",
|
|
1095
|
+
"channel": "checker",
|
|
1096
|
+
"sentence": "With ≥ 2 databases, `database:` is required on every `.seed()`/`.table()`; with exactly one it is forbidden, which is checked by crossing the `services:` record with the calls in the tests.",
|
|
1097
|
+
"specification": "model",
|
|
1098
|
+
"family": "setup",
|
|
1099
|
+
"fix": "State `database:` everywhere or nowhere, whichever the record calls for.",
|
|
1100
|
+
"name": "database-named-only-when-several",
|
|
1101
|
+
"rationale": "The number of databases fixes the call API; the cross-file analysis catches the omission before run time.",
|
|
1102
|
+
"reach": "tests"
|
|
1103
|
+
}
|
|
1104
|
+
],
|
|
1105
|
+
type: [
|
|
1106
|
+
{
|
|
1107
|
+
"audience": "consumer",
|
|
1108
|
+
"channel": "type",
|
|
1109
|
+
"sentence": "A request to a route with parameters is written as the route's template, its values beside it, `api.get('/indicators/:symbol/chart', { params: { symbol: 'btc' } })`, and the template types them: every `:name` it holds takes a value, and no other name does. `get` and `delete` take the options second, `post` and `put` third, after the body; a path with no parameter takes none, or `{ params: {} }` to state that it is the route itself.",
|
|
1110
|
+
"specification": "api",
|
|
1111
|
+
"family": "chain",
|
|
1112
|
+
"fix": "Write the route as the app declares it and hand its values: `.get('/events/:id', { params: { id } })`.",
|
|
1113
|
+
"name": "route-written-as-template",
|
|
1114
|
+
"rationale": "A filled path hides which of its segments are values: the reader, the subject reader and route coverage all have to guess that `btc` in `/indicators/btc/chart` is a symbol. The template says it, and the compiler holds the values to it.",
|
|
1115
|
+
"reach": "tests"
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
"audience": "consumer",
|
|
1119
|
+
"channel": "type",
|
|
1120
|
+
"sentence": "A string is a value and the disk is `file()` or `directory()`: `seed('./__seeds__/x.sql')` (a bare string where a file is meant), `seed(file('./__seeds__/x.json'))` (the wrong extension), `file('x.sql')` (not a relative path), `directory('./__fixtures__/shop')` (a directory without its `/`), `get(file(...))` (a file where a value is meant) and `toMatch(directory(...))` on a stream do not compile.",
|
|
1121
|
+
"specification": "model",
|
|
1122
|
+
"family": "chain",
|
|
1123
|
+
"fix": "Name the disk from the spec file: `file('./__seeds__/users.sql')`, `file('./__expected__/help.txt')`, `directory('../../__fixtures__/shop/')`.",
|
|
1124
|
+
"name": "disk-named-by-relative-path",
|
|
1125
|
+
"rationale": "A bare name resolved against a hidden folder is a convention the reader has to know; a relative path is read where it is written, and the compiler refuses the other form at the call.",
|
|
1126
|
+
"reach": "tests"
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
"audience": "consumer",
|
|
1130
|
+
"channel": "type",
|
|
1131
|
+
"sentence": "The `services` record types the factory argument: `server: (services) => ...` sees exactly the keys the record declares.",
|
|
1132
|
+
"specification": "model",
|
|
1133
|
+
"family": "setup",
|
|
1134
|
+
"fix": "Declare the service in the record, or read the key that is there.",
|
|
1135
|
+
"name": "server-sees-declared-services",
|
|
1136
|
+
"rationale": "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.",
|
|
1137
|
+
"reach": "specification"
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
"audience": "consumer",
|
|
1141
|
+
"channel": "type",
|
|
1142
|
+
"sentence": "`server` and `url` are mutually exclusive on `specification.website()`: the pair is inexpressible, not runtime-checked.",
|
|
1143
|
+
"specification": "model",
|
|
1144
|
+
"family": "setup",
|
|
1145
|
+
"fix": "State `server` for a site this run starts, `url` for one already running.",
|
|
1146
|
+
"name": "no-server-with-url",
|
|
1147
|
+
"rationale": "They name two different subjects, and a specification that states both leaves the reader to guess which site the spec is about.",
|
|
1148
|
+
"reach": "specification"
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
"audience": "consumer",
|
|
1152
|
+
"channel": "type",
|
|
1153
|
+
"sentence": "Exactly ONE terminal action closes a chain: what it answers is a result, and a result carries no verbs to continue with.",
|
|
1154
|
+
"specification": "model",
|
|
1155
|
+
"family": "chain",
|
|
1156
|
+
"fix": "Open a second chain for a second action.",
|
|
1157
|
+
"name": "one-terminal-action-per-chain",
|
|
1158
|
+
"rationale": "Two actions in one chain make a test with two Whens, and the reader cannot tell which one the Then is about.",
|
|
1159
|
+
"reach": "tests"
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
"audience": "consumer",
|
|
1163
|
+
"channel": "type",
|
|
1164
|
+
"sentence": "A chain carries no label: the test name is the sentence.",
|
|
1165
|
+
"specification": "model",
|
|
1166
|
+
"family": "chain",
|
|
1167
|
+
"fix": "Name the test; the chain states what it does, not what it is called.",
|
|
1168
|
+
"name": "no-chain-label",
|
|
1169
|
+
"rationale": "A second place to read the intent is a second place for it to be stale.",
|
|
1170
|
+
"reach": "tests"
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
"audience": "consumer",
|
|
1174
|
+
"channel": "type",
|
|
1175
|
+
"sentence": "A result accessor is read-only.",
|
|
1176
|
+
"specification": "model",
|
|
1177
|
+
"family": "expectations",
|
|
1178
|
+
"fix": "Assert on what the run produced; change the Given if the answer is wrong.",
|
|
1179
|
+
"name": "result-accessor-read-only",
|
|
1180
|
+
"rationale": "The answer a run produced is evidence, and a test that can rewrite its own evidence proves nothing.",
|
|
1181
|
+
"reach": "tests"
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
"audience": "consumer",
|
|
1185
|
+
"channel": "type",
|
|
1186
|
+
"sentence": "`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.",
|
|
1187
|
+
"specification": "model",
|
|
1188
|
+
"family": "mocks",
|
|
1189
|
+
"fix": "State the port: `mockOf<PaymentGateway>()`.",
|
|
1190
|
+
"name": "mock-of-needs-a-port",
|
|
1191
|
+
"rationale": "The type argument IS the contract the double stands for; without it the double agrees with anything.",
|
|
1192
|
+
"reach": "tests"
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
"audience": "consumer",
|
|
1196
|
+
"channel": "type",
|
|
1197
|
+
"sentence": "The verb set is the boundary between the specifications: `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.",
|
|
1198
|
+
"specification": "model",
|
|
1199
|
+
"family": "visitor",
|
|
1200
|
+
"fix": "Reach for the verb the specification offers; a missing one is a decision, not an omission.",
|
|
1201
|
+
"name": "verb-belongs-to-its-specification",
|
|
1202
|
+
"rationale": "One vocabulary across three specifications is memorable only while each specification refuses the words it cannot mean.",
|
|
1203
|
+
"reach": "tests"
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
"audience": "consumer",
|
|
1207
|
+
"channel": "type",
|
|
1208
|
+
"sentence": "`jterrazz.config.ts` takes a `test` section, typed by `@jterrazz/test`: `projects`, how a vitest project departs from the one the tree derives, keyed by its specification with its `reason`; `routes`, a path to an OpenAPI document; `collections`, a list of route segments; `basePath`, the path every api route is mounted under, written from the root with no slash at its end (`'/api'`): any other string is refused when the section is read; `methodless`, the paths of routes taken on any method, each with its reason; `production.allow`. A key the package does not read is refused, and so is a list written as one word, a specification the package does not ship, and a project stating no reason.",
|
|
1209
|
+
"specification": "model",
|
|
1210
|
+
"family": "setup",
|
|
1211
|
+
"fix": "Write only the keys `@jterrazz/test` reads, lists as lists: `test: { collections: ['data'] }`.",
|
|
1212
|
+
"name": "test-settings-section",
|
|
1213
|
+
"rationale": "A key nothing reads is a setting that silently does nothing, and the compiler is the one reader that sees the whole file before a run does.",
|
|
1214
|
+
"reach": "config"
|
|
1215
|
+
}
|
|
1216
|
+
],
|
|
1217
|
+
upstream: [
|
|
1218
|
+
{
|
|
1219
|
+
"audience": "consumer",
|
|
1220
|
+
"channel": "upstream",
|
|
1221
|
+
"sentence": "No vitest snapshot matcher: `toMatchSnapshot`, `toMatchInlineSnapshot`, `toMatchFileSnapshot`, `toMatchAriaSnapshot`, `toMatchScreenshot`, `toThrowErrorMatchingSnapshot` and `toThrowErrorMatchingInlineSnapshot` are refused by `vitest/no-restricted-matchers`, whose option the `test()` preset sets (ADR-005).",
|
|
1222
|
+
"specification": "model",
|
|
1223
|
+
"family": "expectations",
|
|
1224
|
+
"fix": "Write the golden: `expect(subject).toMatch(file('./__expected__/<name>.<ext>'))`; under `src/` compare to a literal with `toStrictEqual`.",
|
|
1225
|
+
"name": "no-vitest-snapshot",
|
|
1226
|
+
"rationale": "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.",
|
|
1227
|
+
"reach": "tests",
|
|
1228
|
+
"upstream": "vitest/no-restricted-matchers"
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
"audience": "consumer",
|
|
1232
|
+
"channel": "upstream",
|
|
1233
|
+
"sentence": "At most one level of `describe`: `vitest/max-nested-describe` with `{ max: 1 }`, set by the `test()` preset.",
|
|
1234
|
+
"specification": "model",
|
|
1235
|
+
"family": "layout",
|
|
1236
|
+
"fix": "Flatten the nesting: the test name is the sentence, and one level groups the file.",
|
|
1237
|
+
"name": "one-describe-level",
|
|
1238
|
+
"rationale": "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.",
|
|
1239
|
+
"reach": "tests",
|
|
1240
|
+
"upstream": "vitest/max-nested-describe"
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
"audience": "consumer",
|
|
1244
|
+
"channel": "upstream",
|
|
1245
|
+
"sentence": "`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(stubs, { origin })`. `vi.stubEnv` is sanctioned; `vi.mock`/`vi.doMock` stay on `no-module-mock`, whose allow-list the option cannot express.",
|
|
1246
|
+
"specification": "model",
|
|
1247
|
+
"family": "clock",
|
|
1248
|
+
"fix": "Reach for `intercept()` and `clock`: both give back what they took at the end of the scope.",
|
|
1249
|
+
"name": "no-vitest-time-or-global-stub",
|
|
1250
|
+
"rationale": "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.",
|
|
1251
|
+
"reach": "tests",
|
|
1252
|
+
"upstream": "vitest/no-restricted-vi-methods"
|
|
1253
|
+
}
|
|
1254
|
+
],
|
|
1255
|
+
process: [{
|
|
1256
|
+
"audience": "consumer",
|
|
1257
|
+
"channel": "process",
|
|
1258
|
+
"sentence": "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.",
|
|
1259
|
+
"specification": "model",
|
|
1260
|
+
"family": "layout",
|
|
1261
|
+
"fix": "Read what the assets say: a test that stands on its own ground earns a folder.",
|
|
1262
|
+
"name": "folder-follows-assets",
|
|
1263
|
+
"rationale": "It is the assets that settle the grouping: a criterion no channel can decide alone.",
|
|
1264
|
+
"reach": "specs"
|
|
1265
|
+
}, {
|
|
1266
|
+
"audience": "consumer",
|
|
1267
|
+
"channel": "process",
|
|
1268
|
+
"sentence": "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.",
|
|
1269
|
+
"specification": "model",
|
|
1270
|
+
"family": "expectations",
|
|
1271
|
+
"fix": "Golden the stream, and keep `.grep()` for the one line a case is genuinely about.",
|
|
1272
|
+
"name": "output-asserted-as-a-golden",
|
|
1273
|
+
"rationale": "A review judgement: the static channel cannot tell a legitimate grep from a lazy one.",
|
|
1274
|
+
"reach": "tests"
|
|
1275
|
+
}]
|
|
1276
|
+
};
|
|
1277
|
+
//#endregion
|
|
1278
|
+
//#region lint/consumer/catalogue/manifest.ts
|
|
1279
|
+
const SPECIFICATION_LIST = [...SPECIFICATIONS].map((specification) => `\`${specification}\``).join(", ");
|
|
1280
|
+
const TEXT_FIELDS = [
|
|
1281
|
+
"audience",
|
|
1282
|
+
"channel",
|
|
1283
|
+
"specification",
|
|
1284
|
+
"fix",
|
|
1285
|
+
"name",
|
|
1286
|
+
"rationale",
|
|
1287
|
+
"reach",
|
|
1288
|
+
"sentence"
|
|
1289
|
+
];
|
|
1290
|
+
const isFamily = (value) => typeof value === "string" && Object.keys(FAMILIES).includes(value);
|
|
1291
|
+
function isRow(value) {
|
|
1292
|
+
return typeof value === "object" && value !== null && TEXT_FIELDS.every((field) => typeof Reflect.get(value, field) === "string") && isFamily(Reflect.get(value, "family"));
|
|
1293
|
+
}
|
|
1294
|
+
function rowsOf(channel) {
|
|
1295
|
+
return manifest_default[channel].map((entry) => {
|
|
1296
|
+
const row = Object.assign(structuredClone(entry), { sentence: entry.sentence.replaceAll("<specifications>", SPECIFICATION_LIST) });
|
|
1297
|
+
if (!isRow(row)) throw new Error(`lint/consumer/catalogue/manifest.json holds a ${channel} row of no known shape: ${entry.name}`);
|
|
1298
|
+
return row;
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
/** Each rule file attaches its entry as `meta.docs`. A convention oxlint's own `vitest` plugin enforces belongs there, not here. */
|
|
1302
|
+
const STATIC_RULES = rowsOf("static");
|
|
1303
|
+
const TOOLCHAIN_RULES = rowsOf("toolchain");
|
|
1304
|
+
const CHECKER_PASSES = rowsOf("checker");
|
|
1305
|
+
const TYPE_ROWS = rowsOf("type");
|
|
1306
|
+
const UPSTREAM_RULES = rowsOf("upstream");
|
|
1307
|
+
/** Review-borne; listed so the catalogue is complete, the constitution keeps their rationale. */
|
|
1308
|
+
const PROCESS_RULES = rowsOf("process");
|
|
1309
|
+
const consumerRows = byName([
|
|
1310
|
+
...STATIC_RULES,
|
|
1311
|
+
...UPSTREAM_RULES,
|
|
1312
|
+
...TOOLCHAIN_RULES,
|
|
1313
|
+
...TYPE_ROWS,
|
|
1314
|
+
...CHECKER_PASSES,
|
|
1315
|
+
...PROCESS_RULES
|
|
1316
|
+
]);
|
|
1317
|
+
const recommendedRules = Object.fromEntries(consumerRows.filter(({ channel }) => channel === "static").map(({ name }) => [`jterrazz/${name}`, "error"]));
|
|
1318
|
+
//#endregion
|
|
1319
|
+
export { recommendedRules as n, consumerRows as t };
|
|
1320
|
+
|
|
1321
|
+
//# sourceMappingURL=shared-manifest.js.map
|