@systemfsoftware/oxlint-plugin-test-discipline 3.5.1

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/dist/index.mjs ADDED
@@ -0,0 +1,2776 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { Array as Array$1, Effect, Option, Schema } from "effect";
3
+ //#region src/rules/path.config.ts
4
+ const TEST_BASENAME = /\.(?:test|spec)\.[cm]?tsx?$/;
5
+ const SANCTIONED_TEST_DIRS = /* @__PURE__ */ new Set(["tests"]);
6
+ /**
7
+ * The only test location sanctioned under `src/`. A workflow property test
8
+ * earns colocation with the workflow it covers, but not adjacency: it lives in
9
+ * a `__tests__` directory beside that workflow, never as a sibling file.
10
+ */
11
+ const NESTED_TEST_DIR = "__tests__";
12
+ const TEST_TREE_DIRS = /* @__PURE__ */ new Set([...SANCTIONED_TEST_DIRS, NESTED_TEST_DIR]);
13
+ const PROPERTY_SUFFIX = ".property.test.ts";
14
+ const INTEGRATION_SUFFIX = ".integration.test.ts";
15
+ /**
16
+ * Forbidden outright. A schema's laws are generated, so a hand-written
17
+ * `*.schema.test.ts` only ever restates coverage that already exists.
18
+ */
19
+ const SCHEMA_SUFFIX = ".schema.test.ts";
20
+ /**
21
+ * The one property-test basename sanctioned under `src/`: a single-segment
22
+ * stem, then `.workflow.property.test.ts`, beside the `<stem>.workflow.ts` it
23
+ * covers. Every other test file under `src/` is banned; a kernel, policy, or
24
+ * schema suite has no file home and becomes an in-source `import.meta.vitest`
25
+ * block in the module it covers.
26
+ */
27
+ const WORKFLOW_TEST_BASENAME = /^[^.]+\.workflow\.property\.test\.ts$/;
28
+ const GHERKIN_PACKAGE = "@systemfsoftware/effect-gherkin-spec";
29
+ const FOREIGN_RUNNERS = /* @__PURE__ */ new Set(["vitest", "@effect/vitest"]);
30
+ const RUNNER_NAMES = /* @__PURE__ */ new Set([
31
+ "it",
32
+ "test",
33
+ "describe"
34
+ ]);
35
+ const MESSAGE$7 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
36
+ const ABSENCE_MESSAGE = "{{name}} is untested. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
37
+ //#endregion
38
+ //#region src/rules/behaviour-exercises-use-case.config.ts
39
+ const NO_SUBJECT_IMPORT_NAME = "a *.integration.test.ts that reaches no package code";
40
+ const NO_SUBJECT_IMPORT_EXPECTED = "an import of the package code under test";
41
+ const NO_SUBJECT_IMPORT_ACTUAL = "a behaviour file whose every runtime import is vitest, @effect/vitest, the gherkin spec package, effect, a Node builtin, or the file itself";
42
+ const NO_SUBJECT_IMPORT_FIX = "a behaviour test exercises a use case, so it has to reach the package. A file that imports nothing but its runner and effect is asserting over values it built in the same file. Ask whether the assertion tests anything at all: if it restates a literal, delete the scenario; if it states an invariant that holds over generated inputs, move it to a *.property.test.ts beside the cell that decides it. Type-only imports never count - they are erased before anything runs - while a side-effect import (import \"../src/x.js\", import {} from \"../src/x.js\") does count, because it executes that module.";
43
+ const meta$22 = {
44
+ type: "problem",
45
+ docs: { description: "A *.integration.test.ts must import the package under test, not only its runner and effect, so the scenario exercises code that ships rather than values the test built itself." },
46
+ schema: [],
47
+ messages: { noSubjectImport: MESSAGE$7 }
48
+ };
49
+ //#endregion
50
+ //#region src/rules/path.ts
51
+ const PathSegments = Schema.NonEmptyArray(Schema.String);
52
+ const segmentsOf = (filename) => Schema.decodeUnknownSync(PathSegments)(filename.split("/"));
53
+ const basenameOf = (filename) => Array$1.lastNonEmpty(segmentsOf(filename));
54
+ /** Directory segments only — the basename never counts as a directory. */
55
+ const directoriesOf = (filename) => Array$1.initNonEmpty(segmentsOf(filename));
56
+ const isUnderSrc = (filename) => directoriesOf(filename).includes("src");
57
+ const isInSanctionedTestDir = (filename) => directoriesOf(filename).some((segment) => SANCTIONED_TEST_DIRS.has(segment));
58
+ const isInTestsImportScope = (filename) => {
59
+ const segments = segmentsOf(filename);
60
+ const directories = Array$1.initNonEmpty(segments);
61
+ if (directories.some((segment) => segment === "src")) return false;
62
+ return isTestFile(Array$1.lastNonEmpty(segments)) || directories.some((segment) => TEST_TREE_DIRS.has(segment));
63
+ };
64
+ const isInConfiguredTestDir = (filename, dirs) => directoriesOf(filename).some((segment) => dirs.includes(segment));
65
+ const isTestFile = (basename) => TEST_BASENAME.test(basename);
66
+ const CELL_SUFFIX = /\.([^.]+)\.[cm]?tsx?$/;
67
+ const cellOf = (basename) => CELL_SUFFIX.exec(basename)?.[1];
68
+ //#endregion
69
+ //#region src/rules/behaviour-exercises-use-case.ts
70
+ const EFFECT_PACKAGE = "effect";
71
+ const FOUNDATION_PACKAGES = /* @__PURE__ */ new Set([
72
+ ...FOREIGN_RUNNERS,
73
+ GHERKIN_PACKAGE,
74
+ EFFECT_PACKAGE
75
+ ]);
76
+ /**
77
+ * The runner, the spec DSL and effect itself - the scaffolding every behaviour
78
+ * file imports. `effect/testing` and every other subpath counts too: a subpath
79
+ * is still the same dependency, and admitting it would let a file satisfy the
80
+ * rule by importing an arbitrary. A `node:` builtin is scaffolding as well: it
81
+ * is part of the environment, not of the package, and a file whose only
82
+ * non-runner import is `node:assert` still never touches the package under
83
+ * test. Whether `node:child_process` legitimately reaches a CLI's behaviour is
84
+ * a decision for the file's other imports - the builtin itself never does.
85
+ *
86
+ * The gherkin spec package is scaffolding for every other package's tests. A
87
+ * behaviour file that lives inside that package and imports the package name
88
+ * is exercising the package under test, not importing a runner.
89
+ */
90
+ const isGherkinPackageTree = (filename) => filename.includes("/gherkin/effect/") || filename.includes("/effect-gherkin-spec/");
91
+ const isFoundationImport = (source, filename) => {
92
+ if (source === "@systemfsoftware/effect-gherkin-spec" && isGherkinPackageTree(filename)) return false;
93
+ return FOUNDATION_PACKAGES.has(source) || source.startsWith(`${EFFECT_PACKAGE}/`) || source.startsWith("node:");
94
+ };
95
+ const isBehaviourTest$3 = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
96
+ /** The stem of a path with its final extension stripped, for identity comparisons. */
97
+ const stemOf = (file) => file.replace(/\.[^/]+$/, "");
98
+ /**
99
+ * A relative module specifier resolved against the linted file's directory,
100
+ * with `..` segments collapsed. Existence is never checked: one file cannot
101
+ * know what else is on disk (OX-TS2), so only the lexical identity is decided.
102
+ */
103
+ const sourceResolvesToItself = (source, filename) => {
104
+ if (!source.startsWith(".")) return false;
105
+ if (source.startsWith("/")) return false;
106
+ const stack = filename.slice(0, Math.max(0, filename.lastIndexOf("/"))).split("/");
107
+ for (const segment of source.split("/")) {
108
+ if (segment === "" || segment === ".") continue;
109
+ if (segment === "..") {
110
+ if (stack.length > 0) stack.pop();
111
+ } else stack.push(segment);
112
+ }
113
+ return stemOf(stack.join("/")) === stemOf(filename);
114
+ };
115
+ /**
116
+ * `import type` is erased before anything executes; `import { type X, Y }` still
117
+ * binds the value `Y`. Zero specifiers is a side-effect import - it executes the
118
+ * module it names, so it reaches whatever that module is.
119
+ */
120
+ const hasRuntimeSpecifier = (statement) => {
121
+ if (statement.specifiers.length === 0) return true;
122
+ return statement.specifiers.some((spec) => !(spec.type === "ImportSpecifier" && spec.importKind === "type"));
123
+ };
124
+ /**
125
+ * A path segment naming the package's build output. Keyed on the emitted directory
126
+ * rather than a filename, so a renamed entry is followed automatically.
127
+ */
128
+ const DIST_SEGMENT = /(?:^|\/)dist\//;
129
+ /**
130
+ * Reports a behaviour file that reaches no package code at all.
131
+ *
132
+ * This deliberately stops short of the convention it serves. The convention is
133
+ * that a behaviour test drives a real use case through the I/O sandwich, and the
134
+ * earlier form of this rule claimed to enforce it by requiring an import whose
135
+ * basename ended in `Executor`, `Handler`, `Adapter`, `Store` or `Middleware`.
136
+ * A probe settled that: a pure kernel named `ZzPureAdapter.ts`, imported by a
137
+ * behaviour test that touched nothing else, was admitted in silence. The gate
138
+ * read a filename its own author chose, so renaming a kernel bought a pass and
139
+ * nothing recomputed whether the module did any I/O. Which side of the sandwich
140
+ * an imported module sits on is not decidable from the importing file's syntax -
141
+ * one file holds no cross-file or type information - so the rule states the part
142
+ * that is: whether the file reaches the package under test. That the reached
143
+ * module is a shell is a review matter, and the role word in a shell module's
144
+ * name documents it for the reader without pretending to be evidence.
145
+ *
146
+ * What counts as reaching: a runtime import from anything that is not the
147
+ * scaffolding - vitest, @effect/vitest, the gherkin spec package, effect or a
148
+ * subpath of it, or a Node builtin - and not the test file itself, or a dynamic
149
+ * `import(...)` of such a source. Type-only specifiers are erased and never
150
+ * count; a side-effect import (`import "./x.js"`, `import {} from "./x.js"`)
151
+ * executes its module and counts for whatever that module is. Whether a named
152
+ * module actually exists is not observable from one file, so an import whose
153
+ * path names nothing satisfies the rule the same way a real one does.
154
+ */
155
+ const behaviourExercisesUseCase = defineRule({
156
+ meta: meta$22,
157
+ create(context) {
158
+ let reached = false;
159
+ return {
160
+ ImportExpression(node) {
161
+ if (node.source.type !== "Literal" || typeof node.source.value !== "string") {
162
+ reached = true;
163
+ return;
164
+ }
165
+ if (!isFoundationImport(node.source.value, context.filename)) reached = true;
166
+ },
167
+ Literal(node) {
168
+ if (typeof node.value === "string" && DIST_SEGMENT.test(node.value)) reached = true;
169
+ },
170
+ "Program:exit"(node) {
171
+ if (!isBehaviourTest$3(basenameOf(context.filename))) return;
172
+ if (reached) return;
173
+ for (const statement of node.body) {
174
+ if (statement.type !== "ImportDeclaration") continue;
175
+ if (statement.importKind === "type") continue;
176
+ const source = statement.source.value;
177
+ if (isFoundationImport(source, context.filename)) continue;
178
+ if (sourceResolvesToItself(source, context.filename)) continue;
179
+ if (!hasRuntimeSpecifier(statement)) continue;
180
+ return;
181
+ }
182
+ context.report({
183
+ node: node.body[0] ?? node,
184
+ messageId: "noSubjectImport",
185
+ data: {
186
+ name: NO_SUBJECT_IMPORT_NAME,
187
+ expected: NO_SUBJECT_IMPORT_EXPECTED,
188
+ actual: NO_SUBJECT_IMPORT_ACTUAL,
189
+ fix: NO_SUBJECT_IMPORT_FIX
190
+ }
191
+ });
192
+ }
193
+ };
194
+ }
195
+ });
196
+ //#endregion
197
+ //#region src/rules/behaviour-one-feature-per-file.config.ts
198
+ const TOO_FEW_FEATURES_NAME = "a *.integration.test.ts that constructs no Feature";
199
+ const TOO_FEW_FEATURES_EXPECTED = "exactly one Feature(...) — the capability the file proves";
200
+ const TOO_FEW_FEATURES_ACTUAL = "a behaviour file with zero Feature(...) calls";
201
+ const TOO_FEW_FEATURES_FIX = "one file, one capability. A Feature is what the file is for; if there is none, the file is not a behaviour test — delete it or add the missing Feature call.";
202
+ const TOO_MANY_FEATURES_NAME = "a *.integration.test.ts accumulating multiple Feature(...) calls";
203
+ const TOO_MANY_FEATURES_EXPECTED = "exactly one Feature(...) — every additional one signals a junk drawer";
204
+ const TOO_MANY_FEATURES_ACTUAL = "a behaviour file with two or more Feature(...) calls";
205
+ const TOO_MANY_FEATURES_FIX = "splitting a junk drawer into several smaller junk drawers is not an improvement. When separating scenarios surfaces assertions that restate a pure function return value — change detectors against a lookup table or constant — those get deleted, not rehoused. Each surviving capability keeps its own file with exactly one Feature.";
206
+ const meta$21 = {
207
+ type: "problem",
208
+ docs: { description: "A *.integration.test.ts must contain exactly one Feature(...) call. Zero or two-or-more is the junk-drawer signal that produced 41 scenarios of pure-function assertions in a single file." },
209
+ schema: [],
210
+ messages: {
211
+ tooFewFeatures: MESSAGE$7,
212
+ tooManyFeatures: MESSAGE$7
213
+ }
214
+ };
215
+ //#endregion
216
+ //#region src/rules/behaviour-one-feature-per-file.ts
217
+ const rootCalleeName = (callee) => {
218
+ const descend = (current) => {
219
+ if (current.type === "CallExpression") return descend(current.callee);
220
+ if (current.type === "MemberExpression") return descend(current.object);
221
+ return current;
222
+ };
223
+ const root = descend(callee);
224
+ return root.type === "Identifier" ? root.name : null;
225
+ };
226
+ const isFeatureCallStatement = (statement) => {
227
+ if (statement.type !== "ExpressionStatement") return false;
228
+ if (statement.expression.type !== "CallExpression") return false;
229
+ return rootCalleeName(statement.expression.callee) === "Feature";
230
+ };
231
+ const isBehaviourTest$2 = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
232
+ const findSecondFeatureCall = (program) => {
233
+ let seen = 0;
234
+ for (const statement of program.body) {
235
+ if (!isFeatureCallStatement(statement)) continue;
236
+ seen += 1;
237
+ if (seen === 2) return statement;
238
+ }
239
+ return null;
240
+ };
241
+ const countFeatureCalls = (program) => {
242
+ let count = 0;
243
+ for (const statement of program.body) if (isFeatureCallStatement(statement)) count += 1;
244
+ return count;
245
+ };
246
+ const hasAnyFeatureCall = (program) => {
247
+ for (const statement of program.body) if (isFeatureCallStatement(statement)) return true;
248
+ return false;
249
+ };
250
+ const behaviourOneFeaturePerFile = defineRule({
251
+ meta: meta$21,
252
+ create(context) {
253
+ return { "Program:exit"(node) {
254
+ if (!isBehaviourTest$2(basenameOf(context.filename))) return;
255
+ const excess = findSecondFeatureCall(node);
256
+ if (excess !== null) {
257
+ const total = countFeatureCalls(node);
258
+ context.report({
259
+ node: excess,
260
+ messageId: "tooManyFeatures",
261
+ data: {
262
+ name: TOO_MANY_FEATURES_NAME,
263
+ expected: TOO_MANY_FEATURES_EXPECTED,
264
+ actual: `${TOO_MANY_FEATURES_ACTUAL} (${total} found)`,
265
+ fix: TOO_MANY_FEATURES_FIX
266
+ }
267
+ });
268
+ return;
269
+ }
270
+ if (!hasAnyFeatureCall(node)) context.report({
271
+ node: node.body[0] ?? node,
272
+ messageId: "tooFewFeatures",
273
+ data: {
274
+ name: TOO_FEW_FEATURES_NAME,
275
+ expected: TOO_FEW_FEATURES_EXPECTED,
276
+ actual: TOO_FEW_FEATURES_ACTUAL,
277
+ fix: TOO_FEW_FEATURES_FIX
278
+ }
279
+ });
280
+ } };
281
+ }
282
+ });
283
+ //#endregion
284
+ //#region src/rules/behaviour-test-requires-gherkin.config.ts
285
+ const FOREIGN_RUNNER_EXPECTED = "it and layer imported from @systemfsoftware/effect-gherkin-spec";
286
+ const FOREIGN_RUNNER_ACTUAL = "a test runner imported directly from vitest or @effect/vitest in a behaviour file";
287
+ const FOREIGN_RUNNER_FIX = "import { it, layer } from @systemfsoftware/effect-gherkin-spec and build the suite with makeFeature({ it, layer })";
288
+ const MISSING_MAKE_FEATURE_NAME = "a *.integration.test.ts without makeFeature";
289
+ const MISSING_MAKE_FEATURE_EXPECTED = "makeFeature imported from @systemfsoftware/effect-gherkin-spec";
290
+ const MISSING_MAKE_FEATURE_ACTUAL = "a behaviour file that never constructs a Gherkin feature";
291
+ const MISSING_MAKE_FEATURE_FIX = "import { makeFeature } from @systemfsoftware/effect-gherkin-spec and declare `const Feature = makeFeature({ it, layer })`";
292
+ const meta$20 = {
293
+ type: "problem",
294
+ docs: { description: "A *.integration.test.ts must drive its suite through makeFeature from @systemfsoftware/effect-gherkin-spec and must not import test runners from vitest or @effect/vitest." },
295
+ schema: [],
296
+ messages: {
297
+ foreignRunner: MESSAGE$7,
298
+ missingMakeFeature: MESSAGE$7
299
+ }
300
+ };
301
+ //#endregion
302
+ //#region src/rules/behaviour-test-requires-gherkin.ts
303
+ const ImportedIdentifier = Schema.Struct({ name: Schema.String });
304
+ const isMakeFeatureSpecifier = (specifier) => {
305
+ const imported = specifier.imported;
306
+ return imported.type === "Identifier" && imported.name === "makeFeature";
307
+ };
308
+ /**
309
+ * `null` for any specifier that cannot name a runner. The decode is unreachable
310
+ * for a string-literal import name because the narrowing above rejects it first;
311
+ * it exists so removing that narrowing fails loudly instead of silently.
312
+ */
313
+ const foreignRunnerNameOf = (specifier) => {
314
+ if (specifier.imported.type !== "Identifier") return null;
315
+ const { name } = Schema.decodeSync(ImportedIdentifier)(specifier.imported);
316
+ return RUNNER_NAMES.has(name) ? name : null;
317
+ };
318
+ const isBehaviourTest$1 = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
319
+ const behaviourTestRequiresGherkin = defineRule({
320
+ meta: meta$20,
321
+ create(context) {
322
+ const basename = basenameOf(context.filename);
323
+ return { Program(node) {
324
+ if (!isBehaviourTest$1(basename)) return;
325
+ let hasMakeFeature = false;
326
+ for (const statement of node.body) {
327
+ if (statement.type !== "ImportDeclaration") continue;
328
+ const sourceValue = statement.source.value;
329
+ if (sourceValue === "@systemfsoftware/effect-gherkin-spec") for (const specifier of statement.specifiers) {
330
+ if (specifier.type !== "ImportSpecifier") continue;
331
+ if (isMakeFeatureSpecifier(specifier)) hasMakeFeature = true;
332
+ }
333
+ if (!FOREIGN_RUNNERS.has(sourceValue)) continue;
334
+ for (const specifier of statement.specifiers) {
335
+ if (specifier.type !== "ImportSpecifier") continue;
336
+ const runnerName = foreignRunnerNameOf(specifier);
337
+ if (runnerName === null) continue;
338
+ context.report({
339
+ node: specifier,
340
+ messageId: "foreignRunner",
341
+ data: {
342
+ name: runnerName,
343
+ expected: FOREIGN_RUNNER_EXPECTED,
344
+ actual: FOREIGN_RUNNER_ACTUAL,
345
+ fix: FOREIGN_RUNNER_FIX
346
+ }
347
+ });
348
+ }
349
+ }
350
+ if (!hasMakeFeature) context.report({
351
+ node,
352
+ messageId: "missingMakeFeature",
353
+ data: {
354
+ name: MISSING_MAKE_FEATURE_NAME,
355
+ expected: MISSING_MAKE_FEATURE_EXPECTED,
356
+ actual: MISSING_MAKE_FEATURE_ACTUAL,
357
+ fix: MISSING_MAKE_FEATURE_FIX
358
+ }
359
+ });
360
+ } };
361
+ }
362
+ });
363
+ //#endregion
364
+ //#region src/rules/damp-test-naming.config.ts
365
+ const PASCAL_CASE$1 = /^[A-Z][a-z][a-zA-Z0-9]*$/;
366
+ const RECOGNIZED_TEST_METHODS = /* @__PURE__ */ new Set(["only", "effect"]);
367
+ const TEST_PREFIX_FORBIDDEN_EXPECTED = "DAMP format starting with Should_";
368
+ const MISSING_SHOULD_PREFIX_EXPECTED = "Test name starting with Should_";
369
+ const MISSING_WHEN_SEPARATOR_EXPECTED = "Should_[Behavior]_When_[Condition] format";
370
+ const EMPTY_BEHAVIOR_EXPECTED = "Non-empty behavior in PascalCase (e.g., ThrowError)";
371
+ const EMPTY_CONDITION_EXPECTED = "Non-empty condition in PascalCase (e.g., PasswordInvalid)";
372
+ const INVALID_BEHAVIOR_CASE_EXPECTED = "PascalCase (e.g., ThrowError)";
373
+ const INVALID_CONDITION_CASE_EXPECTED = "PascalCase (e.g., PasswordInvalid)";
374
+ const TEST_PREFIX_FORBIDDEN_FIX = "Remove \"test\" prefix and use DAMP format: Should_[Behavior]_When_[Condition]";
375
+ const MISSING_SHOULD_PREFIX_FIX = "Add \"Should_\" prefix to test name";
376
+ const MISSING_WHEN_SEPARATOR_FIX = "Insert \"_When_\" separator between behavior and condition";
377
+ const EMPTY_BEHAVIOR_FIX = "Add descriptive behavior between Should_ and _When_ (e.g., Should_ThrowError_When_Called)";
378
+ const EMPTY_CONDITION_FIX = "Add descriptive condition after _When_ (e.g., Should_ThrowError_When_PasswordInvalid)";
379
+ const INVALID_BEHAVIOR_CASE_FIX = "Convert behavior to PascalCase (e.g., throwError → ThrowError)";
380
+ const INVALID_CONDITION_CASE_FIX = "Convert condition to PascalCase (e.g., passwordInvalid → PasswordInvalid)";
381
+ const EMPTY_BEHAVIOR_ACTUAL = "Empty string between Should_ and _When_";
382
+ const EMPTY_CONDITION_ACTUAL = "Empty string after _When_";
383
+ const meta$19 = {
384
+ type: "suggestion",
385
+ docs: { description: "Enforce DAMP (Descriptive and Meaningful Phrases) test naming format: Should_[ExpectedBehavior]_When_[Condition]" },
386
+ schema: [],
387
+ messages: {
388
+ testPrefixForbidden: "{{expected}}. Actual: {{actual}}. Fix: {{fix}}.",
389
+ missingShouldPrefix: "{{expected}}. Actual: {{actual}}. Fix: {{fix}}.",
390
+ missingWhenSeparator: "{{expected}}. Actual: {{actual}}. Fix: {{fix}}.",
391
+ emptyBehavior: "{{expected}}. Actual: {{actual}}. Fix: {{fix}}.",
392
+ emptyCondition: "{{expected}}. Actual: {{actual}}. Fix: {{fix}}.",
393
+ invalidBehaviorCase: "{{expected}}. Actual: {{actual}}. Fix: {{fix}}.",
394
+ invalidConditionCase: "{{expected}}. Actual: {{actual}}. Fix: {{fix}}."
395
+ }
396
+ };
397
+ //#endregion
398
+ //#region src/rules/damp-test-naming.ts
399
+ const getExpected = (errorCode) => {
400
+ switch (errorCode) {
401
+ case "testPrefixForbidden": return TEST_PREFIX_FORBIDDEN_EXPECTED;
402
+ case "missingShouldPrefix": return MISSING_SHOULD_PREFIX_EXPECTED;
403
+ case "missingWhenSeparator": return MISSING_WHEN_SEPARATOR_EXPECTED;
404
+ case "emptyBehavior": return EMPTY_BEHAVIOR_EXPECTED;
405
+ case "emptyCondition": return EMPTY_CONDITION_EXPECTED;
406
+ case "invalidBehaviorCase": return INVALID_BEHAVIOR_CASE_EXPECTED;
407
+ case "invalidConditionCase": return INVALID_CONDITION_CASE_EXPECTED;
408
+ }
409
+ };
410
+ const getActual = (testName, errorCode) => {
411
+ switch (errorCode) {
412
+ case "testPrefixForbidden": return `Test starts with "${testName.startsWith("Test") ? "Test" : "test"}" prefix`;
413
+ case "missingShouldPrefix": return `Test name "${testName}" missing Should_ prefix`;
414
+ case "missingWhenSeparator": return `Test name "${testName}" missing _When_ separator`;
415
+ case "emptyBehavior": return EMPTY_BEHAVIOR_ACTUAL;
416
+ case "emptyCondition": return EMPTY_CONDITION_ACTUAL;
417
+ case "invalidBehaviorCase": return `Behavior "${testName.slice(7, testName.indexOf("_When_"))}" is not PascalCase`;
418
+ case "invalidConditionCase": return `Condition "${testName.slice(testName.indexOf("_When_") + 6)}" is not PascalCase`;
419
+ }
420
+ };
421
+ const getFix = (errorCode) => {
422
+ switch (errorCode) {
423
+ case "testPrefixForbidden": return TEST_PREFIX_FORBIDDEN_FIX;
424
+ case "missingShouldPrefix": return MISSING_SHOULD_PREFIX_FIX;
425
+ case "missingWhenSeparator": return MISSING_WHEN_SEPARATOR_FIX;
426
+ case "emptyBehavior": return EMPTY_BEHAVIOR_FIX;
427
+ case "emptyCondition": return EMPTY_CONDITION_FIX;
428
+ case "invalidBehaviorCase": return INVALID_BEHAVIOR_CASE_FIX;
429
+ case "invalidConditionCase": return INVALID_CONDITION_CASE_FIX;
430
+ }
431
+ };
432
+ const validateDampFormat = (name) => {
433
+ if (name.toLowerCase().startsWith("test")) return "testPrefixForbidden";
434
+ if (!name.startsWith("Should_")) return "missingShouldPrefix";
435
+ const whenIndex = name.indexOf("_When_");
436
+ if (whenIndex === -1) return "missingWhenSeparator";
437
+ const behavior = name.slice(7, whenIndex);
438
+ const condition = name.slice(whenIndex + 6);
439
+ if (behavior.length === 0) return "emptyBehavior";
440
+ if (condition.length === 0) return "emptyCondition";
441
+ if (!PASCAL_CASE$1.test(behavior)) return "invalidBehaviorCase";
442
+ if (!PASCAL_CASE$1.test(condition)) return "invalidConditionCase";
443
+ return null;
444
+ };
445
+ const extractTestName$1 = (node) => {
446
+ const firstArg = node.arguments[0];
447
+ if (!firstArg) return;
448
+ if (firstArg.type === "Literal") return String(firstArg.value);
449
+ if (firstArg.type === "TemplateLiteral" && firstArg.quasis.length === 1) return firstArg.quasis[0]?.value.cooked ?? void 0;
450
+ };
451
+ const isTestFunctionCall = (node) => {
452
+ if (node.callee.type === "Identifier") return node.callee.name === "it" || node.callee.name === "test";
453
+ if (node.callee.type === "MemberExpression") {
454
+ if (node.callee.property.type !== "Identifier") return false;
455
+ if (!RECOGNIZED_TEST_METHODS.has(node.callee.property.name)) return false;
456
+ let current = node.callee.object;
457
+ for (; current.type === "MemberExpression"; current = current.object);
458
+ return current.type === "Identifier" && (current.name === "it" || current.name === "test");
459
+ }
460
+ return false;
461
+ };
462
+ const dampTestNaming = defineRule({
463
+ meta: meta$19,
464
+ create(context) {
465
+ return { CallExpression(node) {
466
+ if (!isTestFunctionCall(node)) return;
467
+ const firstArg = node.arguments[0];
468
+ if (!firstArg) return;
469
+ const testName = extractTestName$1(node);
470
+ if (!testName) return;
471
+ const errorCode = validateDampFormat(testName);
472
+ if (errorCode) context.report({
473
+ node: firstArg,
474
+ messageId: errorCode,
475
+ data: {
476
+ expected: getExpected(errorCode),
477
+ actual: getActual(testName, errorCode),
478
+ fix: getFix(errorCode)
479
+ }
480
+ });
481
+ } };
482
+ }
483
+ });
484
+ //#endregion
485
+ //#region src/rules/in-source-test-prop-only.config.ts
486
+ const NON_PROP_CALL_NAME = "a non-property test call inside an `import.meta.vitest` block";
487
+ const NON_PROP_CALL_EXPECTED = "only `it.prop` or `it.effect.prop` member-chain calls (standard modifiers included) with boolean predicates";
488
+ const NON_PROP_CALL_ACTUAL = "a bare or member-chain test call other than `it.prop`/`it.effect.prop`";
489
+ const NON_PROP_CALL_FIX = "delete the block — non-property in-source tests belong nowhere in `src/`; re-home a meaningful example through the cell public export as `*.integration.test.ts`, or rewrite a real invariant as `it.prop` over a schema-derived arbitrary";
490
+ const meta$18 = {
491
+ type: "problem",
492
+ docs: { description: "In-source `if (import.meta.vitest)` blocks under src/ must contain only `it.prop` or `it.effect.prop` calls; every other test call fails — delete the block or rewrite the invariant as a property." },
493
+ schema: [],
494
+ messages: { nonPropCall: MESSAGE$7 }
495
+ };
496
+ //#endregion
497
+ //#region src/rules/vitest-guard.ts
498
+ /** True when `node` is the `import.meta.vitest` member expression itself. */
499
+ const isMetaVitest = (node) => node.type === "MemberExpression" && node.property.type === "Identifier" && node.property.name === "vitest" && node.object.type === "MetaProperty";
500
+ /**
501
+ * True when `test` is the condition of an in-source test block — `import.meta.vitest` bare,
502
+ * or compared against a sentinel on either side.
503
+ *
504
+ * Shared by the rules that must recognise an in-source block without judging its contents.
505
+ */
506
+ const isVitestGuard = (test) => {
507
+ if (isMetaVitest(test)) return true;
508
+ if (test.type !== "BinaryExpression") return false;
509
+ return isMetaVitest(test.left) || isMetaVitest(test.right);
510
+ };
511
+ const isInsideConsequent = (node, consequent) => {
512
+ const walk = (current) => {
513
+ if (current === null) return false;
514
+ if (current === consequent) return true;
515
+ return walk(current.parent);
516
+ };
517
+ return walk(node.parent);
518
+ };
519
+ //#endregion
520
+ //#region src/rules/in-source-test-prop-only.ts
521
+ const PROP_MODIFIERS$1 = /* @__PURE__ */ new Set([
522
+ "only",
523
+ "skip",
524
+ "todo"
525
+ ]);
526
+ const isPropCallee$1 = (callee) => {
527
+ if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return false;
528
+ if (callee.property.name === "prop") {
529
+ const object = callee.object;
530
+ if (object.type === "Identifier") return object.name === "it";
531
+ return object.type === "MemberExpression" && object.property.type === "Identifier" && object.property.name === "effect" && object.object.type === "Identifier" && object.object.name === "it";
532
+ }
533
+ return PROP_MODIFIERS$1.has(callee.property.name) && isPropCallee$1(callee.object);
534
+ };
535
+ const BANNED_TEST_ROOTS = /* @__PURE__ */ new Set([
536
+ "it",
537
+ "test",
538
+ "describe",
539
+ "suite",
540
+ "expect",
541
+ "assert",
542
+ "vi",
543
+ "beforeEach",
544
+ "afterEach",
545
+ "beforeAll",
546
+ "afterAll"
547
+ ]);
548
+ const rootNameOf = (node) => {
549
+ if (node.type === "Identifier") return node.name;
550
+ if (node.type === "MemberExpression") return rootNameOf(node.object);
551
+ if (node.type === "CallExpression") return rootNameOf(node.callee);
552
+ };
553
+ const inSourceTestPropOnly = defineRule({
554
+ meta: meta$18,
555
+ create(context) {
556
+ const filename = context.filename;
557
+ const basename = basenameOf(filename);
558
+ if (!isUnderSrc(filename) || isTestFile(basename)) return {};
559
+ const guards = [];
560
+ const offending = [];
561
+ return {
562
+ IfStatement(node) {
563
+ if (!isVitestGuard(node.test)) return;
564
+ guards.push(node);
565
+ },
566
+ CallExpression(node) {
567
+ if (guards.length === 0) return;
568
+ if (isPropCallee$1(node.callee)) return;
569
+ const root = rootNameOf(node.callee);
570
+ if (root === void 0 || !BANNED_TEST_ROOTS.has(root)) return;
571
+ if (guards.find((g) => isInsideConsequent(node, g.consequent)) === void 0) return;
572
+ offending.push(node);
573
+ },
574
+ "Program:exit"() {
575
+ for (const node of offending) context.report({
576
+ node,
577
+ messageId: "nonPropCall",
578
+ data: {
579
+ name: NON_PROP_CALL_NAME,
580
+ expected: NON_PROP_CALL_EXPECTED,
581
+ actual: NON_PROP_CALL_ACTUAL,
582
+ fix: NON_PROP_CALL_FIX
583
+ }
584
+ });
585
+ }
586
+ };
587
+ }
588
+ });
589
+ //#endregion
590
+ //#region src/rules/in-source-test-targets-private.config.ts
591
+ const NOT_MODULE_LEVEL_NAME = "a nested `import.meta.vitest` block";
592
+ const NOT_MODULE_LEVEL_EXPECTED = "the in-source test block as a direct statement of the module body";
593
+ const NOT_MODULE_LEVEL_ACTUAL = "an `import.meta.vitest` block nested inside another statement";
594
+ const NOT_MODULE_LEVEL_FIX = "move the block to module level — a nested block does not run under vitest includeSource";
595
+ const NO_PRIVATE_TARGET_NAME = "an `import.meta.vitest` block touching no private binding";
596
+ const NO_PRIVATE_TARGET_EXPECTED = "an in-source test exercising a non-exported module-level binding";
597
+ const NO_PRIVATE_TARGET_ACTUAL = "an in-source block referencing only exported or imported names";
598
+ const NO_PRIVATE_TARGET_FIX = "test the public surface from tests/ as *.integration.test.ts; in-source blocks exist for private helpers only — if the public behaviour you meant to cover is a pure function, delete the assertion: the type system already proves it";
599
+ const meta$17 = {
600
+ type: "problem",
601
+ docs: { description: "In-source `if (import.meta.vitest)` blocks under src/ must be at module level and exercise at least one non-exported binding; other tests belong in tests/." },
602
+ schema: [],
603
+ messages: {
604
+ notModuleLevel: MESSAGE$7,
605
+ noPrivateTarget: MESSAGE$7
606
+ }
607
+ };
608
+ //#endregion
609
+ //#region src/rules/in-source-test-targets-private.ts
610
+ const isCollectable = (node) => node.type === "VariableDeclaration" || node.type === "FunctionDeclaration" || node.type === "ClassDeclaration" || node.type === "TSEnumDeclaration";
611
+ const addPrivateName = (id, out) => {
612
+ if (id.type === "Identifier") out.add(id.name);
613
+ };
614
+ const LocalBinding = Schema.Struct({ name: Schema.String });
615
+ const collectDeclaration = (node, out) => {
616
+ if (node.type === "VariableDeclaration") {
617
+ for (const declarator of node.declarations) addPrivateName(declarator.id, out);
618
+ return;
619
+ }
620
+ if (node.id !== null) addPrivateName(node.id, out);
621
+ };
622
+ const collectPrivateNames = (body, out) => {
623
+ for (const node of body) {
624
+ if (node.type === "ExportNamedDeclaration") {
625
+ if (node.source !== null) continue;
626
+ for (const specifier of node.specifiers) out.delete(Schema.decodeUnknownSync(LocalBinding)(specifier.local).name);
627
+ continue;
628
+ }
629
+ if (isCollectable(node)) collectDeclaration(node, out);
630
+ }
631
+ };
632
+ const inSourceTestTargetsPrivate = defineRule({
633
+ meta: meta$17,
634
+ create(context) {
635
+ const filename = context.filename;
636
+ const basename = basenameOf(filename);
637
+ const underSrc = isUnderSrc(filename);
638
+ const inTestFile = isTestFile(basename);
639
+ if (!underSrc || inTestFile) return {};
640
+ const privateNames = /* @__PURE__ */ new Set();
641
+ const guards = [];
642
+ return {
643
+ Program(node) {
644
+ collectPrivateNames(node.body, privateNames);
645
+ },
646
+ IfStatement(node) {
647
+ if (!isVitestGuard(node.test)) return;
648
+ if (node.parent.type !== "Program") {
649
+ context.report({
650
+ node: node.test,
651
+ messageId: "notModuleLevel",
652
+ data: {
653
+ name: NOT_MODULE_LEVEL_NAME,
654
+ expected: NOT_MODULE_LEVEL_EXPECTED,
655
+ actual: NOT_MODULE_LEVEL_ACTUAL,
656
+ fix: NOT_MODULE_LEVEL_FIX
657
+ }
658
+ });
659
+ return;
660
+ }
661
+ guards.push({
662
+ node,
663
+ hit: false
664
+ });
665
+ },
666
+ Identifier(node) {
667
+ if (!privateNames.has(node.name)) return;
668
+ const guard = guards.find((g) => isInsideConsequent(node, g.node.consequent));
669
+ if (guard !== void 0) guard.hit = true;
670
+ },
671
+ "Program:exit"() {
672
+ for (const guard of guards) {
673
+ if (guard.hit) continue;
674
+ context.report({
675
+ node: guard.node.test,
676
+ messageId: "noPrivateTarget",
677
+ data: {
678
+ name: NO_PRIVATE_TARGET_NAME,
679
+ expected: NO_PRIVATE_TARGET_EXPECTED,
680
+ actual: NO_PRIVATE_TARGET_ACTUAL,
681
+ fix: NO_PRIVATE_TARGET_FIX
682
+ }
683
+ });
684
+ }
685
+ }
686
+ };
687
+ }
688
+ });
689
+ //#endregion
690
+ //#region src/rules/no-assert-in-property.config.ts
691
+ const MESSAGE$6 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
692
+ const meta$16 = {
693
+ type: "problem",
694
+ docs: { description: "Property predicates (it.prop / it.effect.prop) must never call expect(...), assert*(...), or raw fc.assert/fc.check. The boolean return IS the verdict — assertions fork the failure channel. assert* remains correct in normal (non-property) tests." },
695
+ schema: [],
696
+ messages: {
697
+ expectCall: MESSAGE$6,
698
+ assertCall: MESSAGE$6,
699
+ rawFcRun: MESSAGE$6
700
+ }
701
+ };
702
+ //#endregion
703
+ //#region src/rules/prop-call.ts
704
+ const PROP_MODIFIERS = /* @__PURE__ */ new Set([
705
+ "only",
706
+ "skip",
707
+ "todo"
708
+ ]);
709
+ const isPropCallee = (callee) => {
710
+ if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return false;
711
+ if (callee.property.name === "prop") {
712
+ const object = callee.object;
713
+ if (object.type === "Identifier") return object.name === "it";
714
+ return object.type === "MemberExpression" && object.property.type === "Identifier" && object.property.name === "effect" && object.object.type === "Identifier" && object.object.name === "it";
715
+ }
716
+ return PROP_MODIFIERS.has(callee.property.name) && isPropCallee(callee.object);
717
+ };
718
+ const getPredicate = (node) => Array$1.findLast(node.arguments, (arg) => arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression");
719
+ //#endregion
720
+ //#region src/rules/no-assert-in-property.ts
721
+ const ASSERT_IDENTIFIER = /^assert/;
722
+ const FC_RUN_METHODS = /* @__PURE__ */ new Set(["assert", "check"]);
723
+ const isInsidePropPredicate = (node) => {
724
+ const parent = node.parent;
725
+ if (parent === null) return false;
726
+ if ((parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") && Option.fromNullishOr(parent.parent).pipe(Option.exists((grandparent) => grandparent.type === "CallExpression" && isPropCallee(grandparent.callee)))) return true;
727
+ return isInsidePropPredicate(parent);
728
+ };
729
+ const noAssertInProperty = defineRule({
730
+ meta: meta$16,
731
+ create(context) {
732
+ return { CallExpression(node) {
733
+ const callee = node.callee;
734
+ let finding = null;
735
+ if (callee.type === "Identifier") {
736
+ if (callee.name === "expect") finding = {
737
+ messageId: "expectCall",
738
+ name: "expect(...)"
739
+ };
740
+ else if (ASSERT_IDENTIFIER.test(callee.name)) finding = {
741
+ messageId: "assertCall",
742
+ name: `${callee.name}(...)`
743
+ };
744
+ } else if (callee.type === "MemberExpression" && callee.property.type === "Identifier") {
745
+ if (callee.object.type === "Identifier" && callee.object.name === "assert") finding = {
746
+ messageId: "assertCall",
747
+ name: `assert.${callee.property.name}(...)`
748
+ };
749
+ else if (callee.object.type === "Identifier" && callee.object.name === "fc" && FC_RUN_METHODS.has(callee.property.name)) finding = {
750
+ messageId: "rawFcRun",
751
+ name: `fc.${callee.property.name}(...)`
752
+ };
753
+ }
754
+ if (finding === null || !isInsidePropPredicate(node)) return;
755
+ context.report({
756
+ node,
757
+ messageId: finding.messageId,
758
+ data: {
759
+ name: `${finding.name} inside a property predicate`,
760
+ expected: "return <boolean> — the boolean return IS the verdict in it.prop / it.effect.prop",
761
+ actual: `${finding.name} forks the failure channel (throw vs false)`,
762
+ fix: "compute the value, then return a single boolean expression; assert* stays correct in normal (non-property) tests"
763
+ }
764
+ });
765
+ } };
766
+ }
767
+ });
768
+ //#endregion
769
+ //#region src/rules/no-behaviourless-assertion.config.ts
770
+ const TEST_FILE = /\.(test|spec)\.[cm]?tsx?$/;
771
+ const BEHAVIOUR_NODES = {
772
+ AwaitExpression: true,
773
+ CallExpression: true,
774
+ NewExpression: true,
775
+ TaggedTemplateExpression: true
776
+ };
777
+ const SKIP_WALK_KEYS = /* @__PURE__ */ new Set([
778
+ "type",
779
+ "loc",
780
+ "range",
781
+ "parent"
782
+ ]);
783
+ const meta$15 = {
784
+ type: "problem",
785
+ docs: { description: "Flag an assertion whose subject and expectation are both built only from imported declarations and literals. Such an assertion invokes nothing, so no change to the behaviour under test can make it fail." },
786
+ schema: [],
787
+ messages: {
788
+ behaviourlessAssertion: "Expected: an assertion over a value the code under test produced. Actual: both sides are built only from imported declarations and literals, so this calls nothing and cannot fail on any behaviour change. Fix: assert the output of the function under test, or delete this — mutation score is computed over mutants, not tests, so a worthless test leaves it untouched and nothing else will catch this.",
789
+ gherkinEmptyCallback: "Expected: a Then/And/But step callback that asserts on the scope or invokes behaviour. Actual: the step callback is empty or contains no assertions/effects, performing no verification."
790
+ }
791
+ };
792
+ //#endregion
793
+ //#region src/rules/no-behaviourless-assertion.ts
794
+ const GHERKIN_TAP_KEYWORDS = {
795
+ Then: true,
796
+ And: true,
797
+ But: true
798
+ };
799
+ const isGherkinTapCall = (node) => {
800
+ if (node.callee.type !== "CallExpression") return false;
801
+ const outerCallee = node.callee.callee;
802
+ return outerCallee.type === "Identifier" && GHERKIN_TAP_KEYWORDS[outerCallee.name] === true;
803
+ };
804
+ const isEmptyCallback = (node) => {
805
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") {
806
+ if (node.body !== null && node.body.type === "BlockStatement") return node.body.body.length === 0;
807
+ }
808
+ return false;
809
+ };
810
+ const isNode$4 = (value) => typeof value === "object" && value !== null && "type" in value;
811
+ /**
812
+ * An identifier that is not an import binding is treated as behaviour, because it was bound
813
+ * locally and most often holds a call result (`const verdict = interpret(cmd)`). That
814
+ * direction is deliberate; only import bindings are known to be declarations and only
815
+ * literals are known to be inert, so anything else is given the benefit of the doubt and the
816
+ * rule stays silent. It under-reports rather than accusing a real assertion.
817
+ */
818
+ const dependsOnBehaviour = (node, imported) => {
819
+ if (Array.isArray(node)) {
820
+ for (const child of node) if (dependsOnBehaviour(child, imported)) return true;
821
+ return false;
822
+ }
823
+ if (!isNode$4(node)) return false;
824
+ if (BEHAVIOUR_NODES[node.type] === true) return true;
825
+ if (node.type === "Identifier") return !imported.has(node.name);
826
+ for (const [key, value] of Object.entries(node)) {
827
+ if (SKIP_WALK_KEYS.has(key)) continue;
828
+ if (node.type === "MemberExpression" && key === "property" && node.computed !== true) continue;
829
+ if (dependsOnBehaviour(value, imported)) return true;
830
+ }
831
+ return false;
832
+ };
833
+ const expectCallOf = (node) => {
834
+ if (node.callee.type !== "MemberExpression") return void 0;
835
+ let target = node.callee.object;
836
+ while (target.type === "MemberExpression") target = target.object;
837
+ if (target.type !== "CallExpression") return void 0;
838
+ const callee = target.callee;
839
+ if (!("name" in callee && callee.name === "expect")) return void 0;
840
+ return target;
841
+ };
842
+ const importedNames = (program) => {
843
+ const names = /* @__PURE__ */ new Set();
844
+ for (const statement of program.body) {
845
+ if (statement.type !== "ImportDeclaration") continue;
846
+ for (const specifier of statement.specifiers) names.add(specifier.local.name);
847
+ }
848
+ return names;
849
+ };
850
+ const noBehaviourlessAssertion = defineRule({
851
+ meta: meta$15,
852
+ create(context) {
853
+ if (!TEST_FILE.test(context.filename)) return {};
854
+ const imported = importedNames(context.sourceCode.ast);
855
+ return { CallExpression(node) {
856
+ if (isGherkinTapCall(node)) {
857
+ const firstArg = node.arguments[0];
858
+ if (firstArg !== void 0 && isEmptyCallback(firstArg)) {
859
+ context.report({
860
+ node,
861
+ messageId: "gherkinEmptyCallback"
862
+ });
863
+ return;
864
+ }
865
+ }
866
+ const expectCall = expectCallOf(node);
867
+ if (expectCall === void 0) return;
868
+ const subject = expectCall.arguments[0];
869
+ if (subject === void 0) return;
870
+ if (dependsOnBehaviour(subject, imported)) return;
871
+ if (dependsOnBehaviour(node.arguments, imported)) return;
872
+ context.report({
873
+ node,
874
+ messageId: "behaviourlessAssertion"
875
+ });
876
+ } };
877
+ }
878
+ });
879
+ //#endregion
880
+ //#region src/rules/no-io-module-in-source-test.config.ts
881
+ /**
882
+ * The specifiers whose modules perform filesystem, process-spawning or network
883
+ * work. The predicate keys on a *call* against a non-type import from one of
884
+ * these — the import alone never decides anything, because a type-only import
885
+ * is erased at runtime and performs nothing.
886
+ *
887
+ * `path` is deliberately absent: it is pure string math and performs no I/O.
888
+ *
889
+ * The effect-group entries carry both spellings an adopter can be on, because
890
+ * platform moved: v4 merges it into the main package (`effect/FileSystem`,
891
+ * verified in-tree across `effect-memfs`, `arethetypeswrong/cli` and
892
+ * `omp-claude-compat`, including a generated api report), while v3 ships it as
893
+ * `@effect/platform/...`. Both are live for someone installing this rule.
894
+ */
895
+ const IO_SPECIFIERS = {
896
+ fs: true,
897
+ "node:fs": true,
898
+ "node:fs/promises": true,
899
+ child_process: true,
900
+ "node:child_process": true,
901
+ net: true,
902
+ http: true,
903
+ https: true,
904
+ dns: true,
905
+ "node:net": true,
906
+ "node:http": true,
907
+ "node:https": true,
908
+ "node:dns": true,
909
+ "effect/FileSystem": true,
910
+ "effect/unstable/process/ChildProcessSpawner": true,
911
+ "@effect/platform/FileSystem": true,
912
+ "@effect/platform/CommandExecutor": true,
913
+ "@effect/platform-node": true,
914
+ "@effect/platform-node/NodeFileSystem": true,
915
+ "@effect/platform-node-shared/NodeFileSystem": true
916
+ };
917
+ const IO_SOURCE_TEST_NAME = "An in-source `import.meta.vitest` test block";
918
+ const IO_SOURCE_TEST_EXPECTED = "the tests of a module whose own source calls an I/O binding to live outside it — a separate test file, or a composition test with a double at the port";
919
+ const IO_SOURCE_TEST_ACTUAL = "this module calls a binding imported from a filesystem, process or network module and guards tests in-source with `import.meta.vitest`";
920
+ const IO_SOURCE_TEST_FIX = "test the module from outside its own source — a separate test file or a composition test doubling the boundary — or, when an assertion merely restates a literal the module already computes, it is a change detector: delete it. The verdict here is the file's own imports and calls, never its name";
921
+ const meta$14 = {
922
+ type: "problem",
923
+ docs: { description: "Reports an in-source `import.meta.vitest` test block in a module whose own syntax shows a called, non-type import from a filesystem, process or network module. Judges only the in-source-test idiom — a module whose tests live in separate files is a no-op for this rule, whatever it imports." },
924
+ schema: [],
925
+ messages: { ioSourceTest: "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}." }
926
+ };
927
+ //#endregion
928
+ //#region src/rules/no-io-module-in-source-test.ts
929
+ const isIoSpecifier = (source) => IO_SPECIFIERS[source] === true;
930
+ /**
931
+ * The binding a call is made against: the callee identifier itself, or the
932
+ * base identifier of a member chain (`fs`, `fs.promises`) when the call is
933
+ * `fs.readFileSync(...)` / `fs.promises.readFile(...)`. Anything else — a
934
+ * computed call, a `super` edge, an erased type construct — performs nothing a
935
+ * type-only import could not.
936
+ */
937
+ const bindingBase = (callee) => {
938
+ if (callee.type === "MemberExpression") return bindingBase(callee.object);
939
+ if (callee.type === "Identifier") return callee.name;
940
+ };
941
+ const noIoModuleInSourceTest = defineRule({
942
+ meta: meta$14,
943
+ create(context) {
944
+ /** Local binding name -> specifier it was (non-type) imported from. */
945
+ const ioBindings = {};
946
+ let ioCallSeen = false;
947
+ return {
948
+ ImportDeclaration(node) {
949
+ if (node.importKind === "type") return;
950
+ const specifier = node.source.value;
951
+ if (!isIoSpecifier(specifier)) return;
952
+ for (const spec of node.specifiers) {
953
+ if (spec.type === "ImportSpecifier" && spec.importKind === "type") continue;
954
+ ioBindings[spec.local.name] = specifier;
955
+ }
956
+ },
957
+ CallExpression(node) {
958
+ if (ioCallSeen) return;
959
+ const binding = bindingBase(node.callee);
960
+ if (binding === void 0) return;
961
+ if (ioBindings[binding] !== void 0) ioCallSeen = true;
962
+ },
963
+ "Program:exit"(node) {
964
+ if (!ioCallSeen) return;
965
+ for (const statement of node.body) {
966
+ if (statement.type !== "IfStatement") continue;
967
+ if (isVitestGuard(statement.test)) context.report({
968
+ node: statement.test,
969
+ messageId: "ioSourceTest",
970
+ data: {
971
+ name: IO_SOURCE_TEST_NAME,
972
+ expected: IO_SOURCE_TEST_EXPECTED,
973
+ actual: IO_SOURCE_TEST_ACTUAL,
974
+ fix: IO_SOURCE_TEST_FIX
975
+ }
976
+ });
977
+ }
978
+ }
979
+ };
980
+ }
981
+ });
982
+ //#endregion
983
+ //#region src/rules/no-nested-quantification.config.ts
984
+ const MESSAGE$5 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
985
+ const Options$2 = Schema.Struct({ exempt: Schema.Array(Schema.String).pipe(Schema.annotate({ description: "File basenames this rule stays silent on. An owner who has measured the cost and accepted it names the file here; the property still runs." }), Schema.withDecodingDefaultType(Effect.succeed([]))) });
986
+ const ITERATOR_METHODS = /* @__PURE__ */ new Set([
987
+ "every",
988
+ "some",
989
+ "map",
990
+ "flatMap",
991
+ "filter",
992
+ "forEach",
993
+ "reduce",
994
+ "reduceRight",
995
+ "find",
996
+ "findIndex",
997
+ "findLast",
998
+ "findLastIndex",
999
+ "sort"
1000
+ ]);
1001
+ const CONSTANT_POOL_ARBITRARIES = /* @__PURE__ */ new Set(["constant", "constantFrom"]);
1002
+ const FASTCHECK_NAMESPACES = /* @__PURE__ */ new Set(["fc"]);
1003
+ const VIOLATION_NAME$2 = "quantification nested inside a property predicate";
1004
+ const EXPECTED$2 = "per-case cost bounded by the draw, not by the draw times a second traversal — inspect a generated value with a fold whose body calls nothing, or move the inner quantifier into the generator so the shrinker can see it";
1005
+ const ACTUAL$2 = "the predicate iterates a value derived from a generated parameter and calls a free function inside that loop, so cost scales with the drawn size times whatever that call costs";
1006
+ const FIX$2 = "hoist the inner call out of the loop when its result does not vary per element; otherwise assert one drawn element per case and let numRuns supply the quantifier, or generate the pair and compare directly. If the cost is understood and accepted, add this file's basename to the rule's exempt option";
1007
+ const meta$13 = {
1008
+ type: "problem",
1009
+ docs: { description: "A property predicate must not quantify over its own generated value and call out again inside that loop. Per-case cost then scales with the drawn size rather than with the draw count, which is the shape Hypothesis reports as nested_given: the suite slows superlinearly as the generator widens, and a CI budget tuned on small draws times out on large ones. Iteration over a bound the generator does not control, and a fold whose body calls nothing, are both fine." },
1010
+ schema: [Schema.toJsonSchemaDocument(Options$2).schema],
1011
+ messages: { nestedQuantification: MESSAGE$5 }
1012
+ };
1013
+ //#endregion
1014
+ //#region src/rules/no-nested-quantification.ts
1015
+ const isNode$3 = (value) => value !== null && typeof value === "object" && "type" in value;
1016
+ const elementsOf = (value) => Array.isArray(value) ? value : null;
1017
+ const entriesOf = (node) => Object.entries(node);
1018
+ const visit = (value, onNode) => {
1019
+ const elements = elementsOf(value);
1020
+ if (elements !== null) {
1021
+ for (const item of elements) visit(item, onNode);
1022
+ return;
1023
+ }
1024
+ if (!isNode$3(value)) return;
1025
+ onNode(value);
1026
+ for (const [key, child] of entriesOf(value)) {
1027
+ if (key === "parent") continue;
1028
+ visit(child, onNode);
1029
+ }
1030
+ };
1031
+ const identifiersIn = (value) => {
1032
+ const names = /* @__PURE__ */ new Set();
1033
+ visit(value, (node) => {
1034
+ if (node.type === "Identifier") names.add(node.name);
1035
+ });
1036
+ return names;
1037
+ };
1038
+ const touches = (value, names) => {
1039
+ for (const name of identifiersIn(value)) if (names.has(name)) return true;
1040
+ return false;
1041
+ };
1042
+ const declaredNamesIn = (predicate) => {
1043
+ const names = /* @__PURE__ */ new Set();
1044
+ const addPattern = (pattern) => {
1045
+ for (const name of identifiersIn(pattern)) names.add(name);
1046
+ };
1047
+ visit(predicate, (node) => {
1048
+ if (node.type === "VariableDeclarator") addPattern(node.id);
1049
+ if (node.type === "FunctionDeclaration" && node.id !== null) names.add(node.id.name);
1050
+ if ("params" in node) addPattern(node.params);
1051
+ });
1052
+ return names;
1053
+ };
1054
+ const isConstantPool = (element) => {
1055
+ if (!isNode$3(element)) return false;
1056
+ if (element.type !== "CallExpression") return false;
1057
+ const callee = element.callee;
1058
+ if (callee.type !== "MemberExpression") return false;
1059
+ if (callee.object.type !== "Identifier") return false;
1060
+ if (!FASTCHECK_NAMESPACES.has(callee.object.name)) return false;
1061
+ if (callee.property.type !== "Identifier") return false;
1062
+ return CONSTANT_POOL_ARBITRARIES.has(callee.property.name);
1063
+ };
1064
+ const boundedPoolIndices = (call) => {
1065
+ const bounded = /* @__PURE__ */ new Set();
1066
+ const generators = call.arguments.find((argument) => argument.type === "ArrayExpression");
1067
+ if (generators === void 0) return bounded;
1068
+ generators.elements.forEach((element, index) => {
1069
+ if (isConstantPool(element)) bounded.add(index);
1070
+ });
1071
+ return bounded;
1072
+ };
1073
+ const drawnNamesIn = (predicate, bounded) => {
1074
+ const drawn = /* @__PURE__ */ new Set();
1075
+ const addFrom = (value) => {
1076
+ for (const name of identifiersIn(value)) drawn.add(name);
1077
+ };
1078
+ const [first, ...rest] = predicate.params;
1079
+ if (isNode$3(first) && first.type === "ArrayPattern") first.elements.forEach((element, index) => {
1080
+ if (bounded.has(index)) return;
1081
+ addFrom(element);
1082
+ });
1083
+ else addFrom(first);
1084
+ addFrom(rest);
1085
+ visit(predicate.body, (node) => {
1086
+ if (node.type !== "VariableDeclarator") return;
1087
+ if (!touches(node.init, drawn)) return;
1088
+ addFrom(node.id);
1089
+ });
1090
+ return drawn;
1091
+ };
1092
+ const iterationOf = (node) => {
1093
+ switch (node.type) {
1094
+ case "ForOfStatement":
1095
+ case "ForInStatement": return {
1096
+ node,
1097
+ iterable: node.right,
1098
+ body: node.body
1099
+ };
1100
+ case "ForStatement":
1101
+ case "WhileStatement":
1102
+ case "DoWhileStatement": return {
1103
+ node,
1104
+ iterable: node.test,
1105
+ body: node.body
1106
+ };
1107
+ case "CallExpression": {
1108
+ const callee = node.callee;
1109
+ if (callee.type !== "MemberExpression") return null;
1110
+ if (callee.property.type !== "Identifier") return null;
1111
+ if (!ITERATOR_METHODS.has(callee.property.name)) return null;
1112
+ return {
1113
+ node,
1114
+ iterable: callee.object,
1115
+ body: node.arguments[0]
1116
+ };
1117
+ }
1118
+ default: return null;
1119
+ }
1120
+ };
1121
+ const hasFreeCall = (body, declared) => {
1122
+ let free = false;
1123
+ visit(body, (node) => {
1124
+ if (node.type !== "CallExpression" && node.type !== "NewExpression") return;
1125
+ if (node.callee.type !== "Identifier") return;
1126
+ if (declared.has(node.callee.name)) return;
1127
+ free = true;
1128
+ });
1129
+ return free;
1130
+ };
1131
+ const check = (context, call, predicate) => {
1132
+ const declared = declaredNamesIn(predicate);
1133
+ const drawn = drawnNamesIn(predicate, boundedPoolIndices(call));
1134
+ visit(predicate.body, (node) => {
1135
+ const iteration = iterationOf(node);
1136
+ if (iteration === null) return;
1137
+ if (!touches(iteration.iterable, drawn)) return;
1138
+ if (!hasFreeCall(iteration.body, declared)) return;
1139
+ context.report({
1140
+ node: iteration.node,
1141
+ messageId: "nestedQuantification",
1142
+ data: {
1143
+ name: VIOLATION_NAME$2,
1144
+ expected: EXPECTED$2,
1145
+ actual: ACTUAL$2,
1146
+ fix: FIX$2
1147
+ }
1148
+ });
1149
+ });
1150
+ };
1151
+ /**
1152
+ * Admission gate (measured 2026-08-06): across the whole repo (packages/ +
1153
+ * omp/, all .ts) this rule reports 0 times — 0% false positives, under the 5%
1154
+ * band that licenses `error` severity. A draw from a constant pool is bounded
1155
+ * and excluded; DISCHARGED_BY in the suite pins the unbounded recipe shape.
1156
+ */
1157
+ const noNestedQuantification = defineRule({
1158
+ meta: meta$13,
1159
+ create(context) {
1160
+ const options = Schema.decodeUnknownSync(Options$2)(context.options[0] ?? {});
1161
+ const exempt = new Set(options.exempt);
1162
+ const basename = context.filename.slice(context.filename.lastIndexOf("/") + 1);
1163
+ return { CallExpression(node) {
1164
+ if (exempt.has(basename)) return;
1165
+ if (!isPropCallee(node.callee)) return;
1166
+ Option.match(getPredicate(node), {
1167
+ onNone: () => {},
1168
+ onSome: (predicate) => check(context, node, predicate)
1169
+ });
1170
+ } };
1171
+ }
1172
+ });
1173
+ //#endregion
1174
+ //#region src/rules/no-pseudo-gherkin-unit-tests.config.ts
1175
+ const NO_LAYER_IN_FEATURE_NAME = "a *.integration.test.ts feature with no environment double";
1176
+ const NO_LAYER_IN_FEATURE_EXPECTED = "a feature builder chained with .withLayer(...) or .withScenarioLayer(...)";
1177
+ const NO_LAYER_IN_FEATURE_ACTUAL = "a Feature(...) call with no .withLayer or .withScenarioLayer builder method";
1178
+ const NO_LAYER_IN_FEATURE_FIX = "an integration test under WGI-CLS1 exercises real collaborator seams using Layer doubles (withLayer or withScenarioLayer). A test with no Layers is a pure in-memory calculation: move its laws into deterministic-universal scenarios, or provide the external boundary Layer it exercises.";
1179
+ const meta$12 = {
1180
+ type: "problem",
1181
+ docs: { description: "A *.integration.test.ts Feature builder must configure at least one environment Layer (.withLayer or .withScenarioLayer) to enforce real integration boundaries under WGI-CLS1." },
1182
+ schema: [],
1183
+ messages: { noLayerInFeature: MESSAGE$7 }
1184
+ };
1185
+ //#endregion
1186
+ //#region src/rules/no-pseudo-gherkin-unit-tests.ts
1187
+ const LAYER_METHODS = {
1188
+ withLayer: true,
1189
+ withScenarioLayer: true
1190
+ };
1191
+ const isBehaviourTest = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
1192
+ const hasLayerInChain = (callNode) => {
1193
+ let current = callNode;
1194
+ while (current.type === "CallExpression") {
1195
+ const calleeNode = current.callee;
1196
+ if (calleeNode.type === "MemberExpression") {
1197
+ if (calleeNode.property.type === "Identifier" && LAYER_METHODS[calleeNode.property.name] === true) return true;
1198
+ current = calleeNode.object;
1199
+ } else break;
1200
+ }
1201
+ return false;
1202
+ };
1203
+ const findRootFeatureCall = (callNode) => {
1204
+ let current = callNode;
1205
+ while (current.type === "CallExpression") {
1206
+ const calleeNode = current.callee;
1207
+ if (calleeNode.type === "Identifier" && calleeNode.name === "Feature") return current;
1208
+ if (calleeNode.type === "MemberExpression") current = calleeNode.object;
1209
+ else break;
1210
+ }
1211
+ return null;
1212
+ };
1213
+ const noPseudoGherkinUnitTests = defineRule({
1214
+ meta: meta$12,
1215
+ create(context) {
1216
+ if (!isBehaviourTest(basenameOf(context.filename))) return {};
1217
+ return { CallExpression(node) {
1218
+ if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier") {
1219
+ if (node.callee.property.name === "body") {
1220
+ const rootFeature = findRootFeatureCall(node);
1221
+ if (rootFeature !== null && !hasLayerInChain(node)) context.report({
1222
+ node: rootFeature,
1223
+ messageId: "noLayerInFeature",
1224
+ data: {
1225
+ name: NO_LAYER_IN_FEATURE_NAME,
1226
+ expected: NO_LAYER_IN_FEATURE_EXPECTED,
1227
+ actual: NO_LAYER_IN_FEATURE_ACTUAL,
1228
+ fix: NO_LAYER_IN_FEATURE_FIX
1229
+ }
1230
+ });
1231
+ }
1232
+ }
1233
+ } };
1234
+ }
1235
+ });
1236
+ //#endregion
1237
+ //#region src/rules/no-silent-return.config.ts
1238
+ const MESSAGE$4 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1239
+ const meta$11 = {
1240
+ type: "problem",
1241
+ docs: { description: "Property predicates (it.prop / it.effect.prop from @effect/vitest) must return a boolean verdict on every code path. fast-check counts undefined as success, so a bare return, a non-boolean return, or falling off the end of the body is a silent pass. Opaque values (identifiers, member expressions, calls) are trusted to be boolean; literals and operators are checked." },
1242
+ schema: [],
1243
+ messages: {
1244
+ bareReturn: MESSAGE$4,
1245
+ nonBooleanReturn: MESSAGE$4,
1246
+ missingReturn: MESSAGE$4,
1247
+ nonBooleanBody: MESSAGE$4
1248
+ }
1249
+ };
1250
+ //#endregion
1251
+ //#region src/rules/no-silent-return.ts
1252
+ const BOOLEAN_PRODUCING_OPERATORS = /* @__PURE__ */ new Set([
1253
+ "===",
1254
+ "!==",
1255
+ "==",
1256
+ "!=",
1257
+ "<",
1258
+ "<=",
1259
+ ">",
1260
+ ">=",
1261
+ "instanceof",
1262
+ "in",
1263
+ "!"
1264
+ ]);
1265
+ const LOOP_LIKE = /* @__PURE__ */ new Set([
1266
+ "ForStatement",
1267
+ "ForInStatement",
1268
+ "ForOfStatement",
1269
+ "WhileStatement",
1270
+ "DoWhileStatement",
1271
+ "LabeledStatement"
1272
+ ]);
1273
+ const isLoopLike = (stmt) => LOOP_LIKE.has(stmt.type);
1274
+ /**
1275
+ * Syntactically boolean-producing expressions. Identifiers, member
1276
+ * expressions, and calls are opaque — trusted to be boolean (documented in
1277
+ * the rule meta); everything else must PROVE it is boolean.
1278
+ */
1279
+ const isBooleanShaped = (expr) => {
1280
+ if (expr.type === "ChainExpression") return isBooleanShaped(expr.expression);
1281
+ if (expr.type === "AwaitExpression") return isBooleanShaped(expr.argument);
1282
+ if (expr.type === "TSAsExpression" || expr.type === "TSSatisfiesExpression" || expr.type === "TSNonNullExpression" || expr.type === "TSTypeAssertion") return isBooleanShaped(expr.expression);
1283
+ switch (expr.type) {
1284
+ case "Literal": return typeof expr.value === "boolean";
1285
+ case "UnaryExpression":
1286
+ case "BinaryExpression": return BOOLEAN_PRODUCING_OPERATORS.has(expr.operator);
1287
+ case "LogicalExpression": return expr.operator !== "??" && isBooleanShaped(expr.left) && isBooleanShaped(expr.right);
1288
+ case "ConditionalExpression": return isBooleanShaped(expr.consequent) && isBooleanShaped(expr.alternate);
1289
+ case "CallExpression":
1290
+ case "Identifier":
1291
+ case "MemberExpression": return true;
1292
+ default: return false;
1293
+ }
1294
+ };
1295
+ const report = (context, node, messageId, actual) => {
1296
+ context.report({
1297
+ node,
1298
+ messageId,
1299
+ data: {
1300
+ name: "A silent exit from a property predicate",
1301
+ expected: "return <boolean> on every code path — fast-check counts undefined as success",
1302
+ actual,
1303
+ fix: "return a boolean verdict; to skip an input dynamically, call fc.pre(condition) instead"
1304
+ }
1305
+ });
1306
+ };
1307
+ const checkReturn = (context, stmt) => {
1308
+ if (stmt.argument === null) {
1309
+ report(context, stmt, "bareReturn", "bare `return;` — the predicate exits with undefined, a silent pass");
1310
+ return;
1311
+ }
1312
+ if (!isBooleanShaped(stmt.argument)) report(context, stmt, "nonBooleanReturn", `return of a non-boolean ${stmt.argument.type}`);
1313
+ };
1314
+ const checkStatements = (context, statements) => {
1315
+ for (const stmt of statements) if (stmt.type === "ReturnStatement") checkReturn(context, stmt);
1316
+ else if (stmt.type === "IfStatement") {
1317
+ checkStatements(context, [stmt.consequent]);
1318
+ if (stmt.alternate !== null) checkStatements(context, [stmt.alternate]);
1319
+ } else if (stmt.type === "BlockStatement") checkStatements(context, stmt.body);
1320
+ else if (stmt.type === "SwitchStatement") for (const switchCase of stmt.cases) checkStatements(context, switchCase.consequent);
1321
+ else if (stmt.type === "TryStatement") {
1322
+ checkStatements(context, [stmt.block]);
1323
+ if (stmt.handler !== null) checkStatements(context, [stmt.handler.body]);
1324
+ if (stmt.finalizer !== null) checkStatements(context, [stmt.finalizer]);
1325
+ } else if (isLoopLike(stmt)) checkStatements(context, [stmt.body]);
1326
+ else if (stmt.type === "FunctionDeclaration") {
1327
+ if (stmt.generator) checkFn(context, stmt);
1328
+ }
1329
+ };
1330
+ /**
1331
+ * Does every path through this statement EXIT the function (return or
1332
+ * throw)? Returns already reported by checkReturn count as exits — the tail
1333
+ * analysis only owns paths with no exit at all.
1334
+ */
1335
+ const pathExits = (stmt) => {
1336
+ switch (stmt.type) {
1337
+ case "ReturnStatement":
1338
+ case "ThrowStatement": return true;
1339
+ case "BlockStatement": return Array$1.last(stmt.body).pipe(Option.exists(pathExits));
1340
+ case "IfStatement": return stmt.alternate !== null && pathExits(stmt.consequent) && pathExits(stmt.alternate);
1341
+ case "SwitchStatement": return Array$1.some(stmt.cases, (switchCase) => switchCase.test === null) && Array$1.every(stmt.cases, (switchCase) => Array$1.last(switchCase.consequent).pipe(Option.exists(pathExits)));
1342
+ case "TryStatement": return pathExits(stmt.block) && (stmt.handler === null || pathExits(stmt.handler.body));
1343
+ default: return false;
1344
+ }
1345
+ };
1346
+ /** Narrow an arbitrary ESTree-adjacent value to a walkable object record. */
1347
+ const isRecord = (value) => typeof value === "object" && value !== null;
1348
+ /** Narrow an object whose `type`/`generator` fields mark it as a generator function expression. */
1349
+ const isGeneratorFunction = (value) => {
1350
+ if (typeof value !== "object" || value === null) return false;
1351
+ if (!("type" in value) || value["type"] !== "FunctionExpression") return false;
1352
+ return "generator" in value && Boolean(value["generator"]);
1353
+ };
1354
+ /** Collect generator functions (Effect.gen bodies are verdict carriers). */
1355
+ const collectGenerators = (value) => {
1356
+ const out = [];
1357
+ const walk = (inner) => {
1358
+ const items = Array.isArray(inner) ? inner : [inner];
1359
+ for (const item of items) {
1360
+ if (!isRecord(item)) continue;
1361
+ if (item["type"] === "FunctionExpression") {
1362
+ if (isGeneratorFunction(item)) out.push(item);
1363
+ continue;
1364
+ }
1365
+ if (item["type"] === "ArrowFunctionExpression") continue;
1366
+ for (const key of Object.keys(item)) {
1367
+ if (key === "parent") continue;
1368
+ walk(item[key]);
1369
+ }
1370
+ }
1371
+ };
1372
+ walk(value);
1373
+ return out;
1374
+ };
1375
+ const checkFn = (context, fn) => {
1376
+ const body = Option.getOrThrow(Option.fromNullishOr(fn.body));
1377
+ if (body.type !== "BlockStatement") {
1378
+ if (!isBooleanShaped(body)) report(context, body, "nonBooleanBody", `predicate body is a non-boolean ${body.type}`);
1379
+ for (const gen of collectGenerators(body)) checkFn(context, gen);
1380
+ return;
1381
+ }
1382
+ checkStatements(context, body.body);
1383
+ for (const gen of collectGenerators(body)) checkFn(context, gen);
1384
+ if (!Array$1.last(body.body).pipe(Option.exists(pathExits))) report(context, fn, "missingReturn", "the predicate can fall off the end without returning — undefined is a silent pass");
1385
+ };
1386
+ const noSilentReturn = defineRule({
1387
+ meta: meta$11,
1388
+ create(context) {
1389
+ return { CallExpression(node) {
1390
+ if (!isPropCallee(node.callee)) return;
1391
+ Option.match(getPredicate(node), {
1392
+ onNone: () => {},
1393
+ onSome: (predicate) => checkFn(context, predicate)
1394
+ });
1395
+ } };
1396
+ }
1397
+ });
1398
+ //#endregion
1399
+ //#region src/rules/no-test-file-in-src.config.ts
1400
+ const Options$1 = Schema.Struct({ sanctionedDirs: Schema.NonEmptyArray(Schema.String).pipe(Schema.withDecodingDefaultType(Effect.succeed([NESTED_TEST_DIR]))) });
1401
+ const testFileInSrcDetail = (dir) => ({
1402
+ expected: `src/**/${dir}/<stem>.workflow.property.test.ts beside the <stem>.workflow.ts it covers, or an in-source import.meta.vitest block`,
1403
+ actual: `a test file under src/ that is neither the schema-laws entry point nor a <stem>.workflow.property.test.ts inside a ${dir} directory`,
1404
+ fix: `pick the arm matching what this file exercises. A workflow law -> rename to <stem>.workflow.property.test.ts inside src/<path>/${dir}/. A kernel/policy/schema property or characterization suite -> convert it to an in-source \`if (import.meta.vitest)\` block in the module it covers. The package public surface -> move outside src/ to tests/<name>.integration.test.ts. No arm matches -> delete this file`
1405
+ });
1406
+ const propertyTestLocationDetail = (dir) => ({
1407
+ expected: `src/**/${dir}/<stem>.workflow.property.test.ts — a property test one directory down from the workflow it covers, never beside it`,
1408
+ actual: `a property test under src/ that is not a single-segment <stem>.workflow.property.test.ts inside a ${dir} directory`,
1409
+ fix: `a workflow law -> rename to <stem>.workflow.property.test.ts inside src/<path>/${dir}/. A kernel/policy/schema property suite -> convert it to an in-source \`if (import.meta.vitest)\` block in the module it covers. Relative imports shift one level when moving into ${dir}: ./<cell>.js -> ../<cell>.js`
1410
+ });
1411
+ const SCHEMA_TEST_DETAIL = {
1412
+ expected: "no authored test under this name — the generated schema-laws.test.ts carries the ruleOfSchemas pair for every exported schema",
1413
+ actual: "an authored *.schema.test.ts restating generated coverage",
1414
+ fix: "delete it. The generated laws already state round-trip identity and encode stability. What they cannot state is rejection — every input they draw comes from the arbitrary the schema itself supplies — so a refusal belongs in an in-source if (import.meta.vitest) block in the schema file, never here"
1415
+ };
1416
+ const meta$10 = {
1417
+ type: "problem",
1418
+ docs: { description: "Under src/, the only sanctioned test file is a single-segment <stem>.workflow.property.test.ts inside a sanctioned test directory, plus the generated schema-laws.test.ts entry point. Every other test file is banned: a kernel, policy, or schema suite becomes an in-source import.meta.vitest block, and a public-surface test moves outside src/ as an integration test. The sanctioned directory list is the sanctionedDirs option, defaulting to the one directory this repo runs." },
1419
+ schema: [Schema.toJsonSchemaDocument(Options$1).schema],
1420
+ messages: {
1421
+ testFileInSrc: MESSAGE$7,
1422
+ schemaTestInSrc: MESSAGE$7,
1423
+ propertyTestOutsideTestsDir: MESSAGE$7
1424
+ }
1425
+ };
1426
+ //#endregion
1427
+ //#region src/rules/no-test-file-in-src.ts
1428
+ const violationOf = (basename, isPropertyTest, dir) => basename.endsWith(".schema.test.ts") ? ["schemaTestInSrc", SCHEMA_TEST_DETAIL] : isPropertyTest ? ["propertyTestOutsideTestsDir", propertyTestLocationDetail(dir)] : ["testFileInSrc", testFileInSrcDetail(dir)];
1429
+ const noTestFileInSrc = defineRule({
1430
+ meta: meta$10,
1431
+ create(context) {
1432
+ const { sanctionedDirs } = Schema.decodeUnknownSync(Options$1)(context.options[0] ?? {});
1433
+ const basename = basenameOf(context.filename);
1434
+ if (!isUnderSrc(context.filename)) return {};
1435
+ if (!isTestFile(basename)) return {};
1436
+ if (basename === "schema-laws.test.ts") return {};
1437
+ const isPropertyTest = basename.endsWith(PROPERTY_SUFFIX);
1438
+ const isSchemaTest = basename.endsWith(SCHEMA_SUFFIX);
1439
+ const colocated = isInConfiguredTestDir(context.filename, sanctionedDirs);
1440
+ if (!isSchemaTest && WORKFLOW_TEST_BASENAME.test(basename) && colocated) return {};
1441
+ const [messageId, detail] = violationOf(basename, isPropertyTest, sanctionedDirs[0]);
1442
+ return { Program(node) {
1443
+ context.report({
1444
+ node,
1445
+ messageId,
1446
+ data: {
1447
+ name: basename,
1448
+ ...detail
1449
+ }
1450
+ });
1451
+ } };
1452
+ }
1453
+ });
1454
+ //#endregion
1455
+ //#region src/rules/pbt-naming.config.ts
1456
+ const SCOPE_SYMBOLS = /* @__PURE__ */ new Set([
1457
+ "∀",
1458
+ "∃",
1459
+ "→",
1460
+ "¬",
1461
+ "≤",
1462
+ "≥"
1463
+ ]);
1464
+ const PREDICATE_SYMBOLS = /* @__PURE__ */ new Set([
1465
+ "≡",
1466
+ "≠",
1467
+ "=",
1468
+ "≤",
1469
+ "≥",
1470
+ "∈",
1471
+ "⊆",
1472
+ "⊇",
1473
+ "→",
1474
+ "¬",
1475
+ "∘",
1476
+ "∩",
1477
+ "∪",
1478
+ "⊥"
1479
+ ]);
1480
+ const NULLARY_PREDICATE_SYMBOLS = /* @__PURE__ */ new Set(["⊥"]);
1481
+ const PASCAL_CASE = /^[A-Z][a-z][a-zA-Z0-9]*$/;
1482
+ const DAMP_WORDS = /When|Should|Given|Then|Otherwise|After|Before/;
1483
+ const meta$9 = {
1484
+ type: "suggestion",
1485
+ docs: { description: "Enforce a complete formal-specification name for property-based tests (it.prop / it.effect.prop). Format: [ScopeSymbol][binder]_[Domain]_[PredicateSymbol][operand] (e.g., ∀x_DecodeEncode_=x, ∀l_Filter_⊆Input, →Shipped_Cancel_⊥Allowed). Both the quantifier and the predicate must carry an operand — a bare symbol specifies nothing." },
1486
+ schema: [],
1487
+ messages: {
1488
+ invalidSegments: "Expected: exactly 2 underscores in PBT name ([Scope]_[Domain]_[Predicate]). Actual: name \"{{actual}}\" has {{count}} separator(s). If this isn't a universal invariant, delete the test. Otherwise use format [ScopeSymbol][binder]_[Domain]_[PredicateSymbol][operand] (e.g., ∀x_DecodeEncode_=x).",
1489
+ invalidScopeSymbol: "Expected: a quantifier symbol (∀ ∃ → ¬ ≤ ≥) at the start of \"{{actual}}\". Actual: it starts with \"{{firstChar}}\". If this isn't a universal invariant, delete the test. Otherwise quantify the input: ∀ (for all), ∃ (there exists), → (implies), ¬, ≤, ≥.",
1490
+ incompleteScope: "Expected: a bound variable after the quantifier \"{{symbol}}\" in scope segment \"{{scope}}\" (e.g., ∀x, ∀order, ∃e, →Shipped). A property quantifies over a named input drawn from a generator; name it. A lone quantifier binds nothing and specifies no domain.",
1491
+ emptyDomain: "Expected: a non-empty PascalCase domain between the two underscores in \"{{actual}}\". If this isn't a universal invariant, delete the test. Otherwise name the thing under test, e.g., ∀x_DecodeEncode_=x.",
1492
+ domainLeaksDAMP: "Expected: an invariant domain, not scenario language. Actual: domain \"{{domain}}\" contains \"{{word}}\" — that describes one case, not a universal law. Delete this test. It is not a property. Find the actual invariant and write that instead.",
1493
+ invalidPredicateSymbol: "Expected: a relation symbol (≡ ≠ = ≤ ≥ ∈ ⊆ ⊇ → ¬ ∘ ∩ ∪ ⊥) starting the last segment of \"{{actual}}\". Actual: it ends with \"{{firstChar}}\". If this isn't a universal invariant, delete the test. Otherwise relate the output: = / ≡ (roundtrip or oracle), ⊆ / ∈ (invariant), ≠ (distinctness), ⊥ (impossibility).",
1494
+ incompletePredicate: "Expected: an operand after the relation symbol \"{{symbol}}\" in predicate \"{{predicate}}\", naming what the output is related to: =x / ≡Oracle (roundtrip or reference), ⊆Input / ∈Ignored (invariant), ≠Zero (distinctness), ⊥Cancellable (impossibility — name the outcome that cannot occur). A bare symbol relates the output to nothing and so asserts no property."
1495
+ }
1496
+ };
1497
+ //#endregion
1498
+ //#region src/rules/pbt-naming.ts
1499
+ const isPropCall = (node) => {
1500
+ let foundProp = false;
1501
+ let current = node.callee;
1502
+ for (; current.type === "MemberExpression"; current = current.object) if (current.property.type === "Identifier" && current.property.name === "prop") foundProp = true;
1503
+ return foundProp && current.type === "Identifier" && (current.name === "it" || current.name === "test");
1504
+ };
1505
+ const extractTestName = (node) => {
1506
+ const firstArg = node.arguments[0];
1507
+ if (!firstArg) return;
1508
+ if (firstArg.type === "Literal") return String(firstArg.value);
1509
+ if (firstArg.type !== "TemplateLiteral") return;
1510
+ if (firstArg.quasis.length !== 1) return;
1511
+ return firstArg.quasis[0]?.value.cooked ?? void 0;
1512
+ };
1513
+ const parseSegments = (name) => {
1514
+ const parts = name.split("_");
1515
+ if (parts.length !== 3) return null;
1516
+ const scopeSegment = parts[0];
1517
+ const domainSegment = parts[1];
1518
+ const predicateSegment = parts[2];
1519
+ if (scopeSegment === void 0 || domainSegment === void 0 || predicateSegment === void 0) return null;
1520
+ return {
1521
+ scopeSegment,
1522
+ domainSegment,
1523
+ predicateSegment
1524
+ };
1525
+ };
1526
+ const pbtNaming = defineRule({
1527
+ meta: meta$9,
1528
+ create(context) {
1529
+ return { CallExpression(node) {
1530
+ if (!isPropCall(node)) return;
1531
+ const firstArg = node.arguments[0];
1532
+ if (!firstArg) return;
1533
+ const testName = extractTestName(node);
1534
+ if (!testName) return;
1535
+ const segments = parseSegments(testName);
1536
+ if (!segments) {
1537
+ context.report({
1538
+ node: firstArg,
1539
+ messageId: "invalidSegments",
1540
+ data: {
1541
+ actual: testName,
1542
+ count: testName.split("_").length - 1
1543
+ }
1544
+ });
1545
+ return;
1546
+ }
1547
+ const { scopeSegment, domainSegment, predicateSegment } = segments;
1548
+ const scopeSymbol = scopeSegment.charAt(0);
1549
+ if (!SCOPE_SYMBOLS.has(scopeSymbol)) {
1550
+ context.report({
1551
+ node: firstArg,
1552
+ messageId: "invalidScopeSymbol",
1553
+ data: {
1554
+ actual: testName,
1555
+ firstChar: scopeSymbol
1556
+ }
1557
+ });
1558
+ return;
1559
+ }
1560
+ if (scopeSegment.slice(1).length === 0) {
1561
+ context.report({
1562
+ node: firstArg,
1563
+ messageId: "incompleteScope",
1564
+ data: {
1565
+ symbol: scopeSymbol,
1566
+ scope: scopeSegment
1567
+ }
1568
+ });
1569
+ return;
1570
+ }
1571
+ if (!PASCAL_CASE.test(domainSegment)) {
1572
+ context.report({
1573
+ node: firstArg,
1574
+ messageId: "emptyDomain",
1575
+ data: { actual: testName }
1576
+ });
1577
+ return;
1578
+ }
1579
+ const dampMatch = DAMP_WORDS.exec(domainSegment);
1580
+ if (dampMatch) {
1581
+ context.report({
1582
+ node: firstArg,
1583
+ messageId: "domainLeaksDAMP",
1584
+ data: {
1585
+ domain: domainSegment,
1586
+ word: dampMatch[0]
1587
+ }
1588
+ });
1589
+ return;
1590
+ }
1591
+ const predicateSymbol = predicateSegment.charAt(0);
1592
+ if (!PREDICATE_SYMBOLS.has(predicateSymbol)) {
1593
+ context.report({
1594
+ node: firstArg,
1595
+ messageId: "invalidPredicateSymbol",
1596
+ data: {
1597
+ actual: testName,
1598
+ firstChar: predicateSymbol
1599
+ }
1600
+ });
1601
+ return;
1602
+ }
1603
+ if (!NULLARY_PREDICATE_SYMBOLS.has(predicateSymbol) && predicateSegment.slice(1).length === 0) {
1604
+ context.report({
1605
+ node: firstArg,
1606
+ messageId: "incompletePredicate",
1607
+ data: {
1608
+ symbol: predicateSymbol,
1609
+ predicate: predicateSegment
1610
+ }
1611
+ });
1612
+ return;
1613
+ }
1614
+ } };
1615
+ }
1616
+ });
1617
+ //#endregion
1618
+ //#region src/rules/prop-arbitrary-schema-origin.config.ts
1619
+ const MESSAGE$3 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1620
+ const VIOLATION_NAME$1 = "a hand-built fast-check arbitrary with no schema underneath";
1621
+ const EXPECTED$1 = "every in-source it.prop arbitrary derives from an Effect Schema — a schema reference, a schema-attached arbitrary annotation, or a pipe/map/oneof chain rooted in one; an input with no schema grows one first";
1622
+ const ACTUAL$1 = "the arbitrary is assembled from fast-check constructors alone, so the generator can emit shapes the schema never declared and the suite stays green while exercising almost nothing";
1623
+ const FIX$1 = "delete the block, or grow the schema the input needs and derive the arbitrary from it — Arbitrary.schema(schema) is the sanctioned derivation; nothing rewrites a hand-built generator into a property";
1624
+ const STOCK_NAME = "a stock-schema derivation wearing a domain costume";
1625
+ const STOCK_EXPECTED = "the arbitrary derives from a schema this module (or a sibling) declares for its own contract — a domain schema — never from a stock Schema member or stock composition at the prop site";
1626
+ const STOCK_ACTUAL = "the Arbitrary.schema root is Schema.String, Schema.Int, or another stock member — the filter and map chains on it are the hand-built generator, relocated behind a schema-looking call";
1627
+ const STOCK_FIX = "declare the domain schema that states the shape — its filter carries the format, its arbitrary annotation carries the generator — and derive through Arbitrary.schema(ThatSchema)";
1628
+ const SCHEMA_SOURCE_PATTERN = /schema/i;
1629
+ const SCHEMA_NAMESPACE_NAMES = {
1630
+ Schema: true,
1631
+ Arbitrary: true
1632
+ };
1633
+ const FASTCHECK_NAMESPACE_NAMES = { FastCheck: true };
1634
+ const COMBINATOR_CALLEES = { pipe: true };
1635
+ const meta$8 = {
1636
+ type: "problem",
1637
+ docs: { description: "Inside an import.meta.vitest in-source block, every it.prop / it.effect.prop arbitrary must derive from an Effect Schema — a schema reference, a schema-attached arbitrary annotation, or a chain rooted in one. A hand-built fast-check construction with no schema underneath reports; statically opaque arbitraries (unresolved or foreign bindings) fail open into the runtime domain audit." },
1638
+ schema: [],
1639
+ messages: {
1640
+ handBuiltArbitrary: MESSAGE$3,
1641
+ stockDerivedArbitrary: MESSAGE$3
1642
+ }
1643
+ };
1644
+ //#endregion
1645
+ //#region src/rules/prop-arbitrary-schema-origin.ts
1646
+ const MAX_WALK_DEPTH$1 = 32;
1647
+ const isNode$2 = (value) => value !== null && typeof value === "object" && "type" in value;
1648
+ const isImportMetaVitest$1 = (node) => node.type === "MemberExpression" && node.property.type === "Identifier" && node.property.name === "vitest" && node.object.type === "MetaProperty" && node.object.meta.name === "import" && node.object.property.name === "meta";
1649
+ /**
1650
+ * Broader than test-placement's condition-only `isVitestGuard`: this walks the whole
1651
+ * test-expression subtree, so a guard like `if (runTests && import.meta.vitest)` is
1652
+ * recognised here. Deliberate — plugins do not share code (KTD8).
1653
+ */
1654
+ const mentionsImportMetaVitest$2 = (value) => {
1655
+ if (Array.isArray(value)) return value.some(mentionsImportMetaVitest$2);
1656
+ if (!isNode$2(value)) return false;
1657
+ if (isImportMetaVitest$1(value)) return true;
1658
+ for (const [key, child] of Object.entries(value)) {
1659
+ if (key === "parent") continue;
1660
+ if (mentionsImportMetaVitest$2(child)) return true;
1661
+ }
1662
+ return false;
1663
+ };
1664
+ const isScopeLike$1 = (value) => typeof value === "object" && value !== null && "set" in value && "upper" in value;
1665
+ const resolveLocal$1 = (name, node, getScope) => {
1666
+ const scope = getScope(node);
1667
+ if (!isScopeLike$1(scope)) return { kind: "none" };
1668
+ for (let current = scope; current !== null; current = current.upper) {
1669
+ const variable = current.set.get(name);
1670
+ if (variable === void 0) continue;
1671
+ for (const def of variable.defs) {
1672
+ if (def.type === "ImportBinding") return { kind: "import" };
1673
+ if (def.node.type === "FunctionDeclaration") return { kind: "function" };
1674
+ if (def.node.type !== "VariableDeclarator") continue;
1675
+ const init = Option.fromNullishOr(def.node.init);
1676
+ if (Option.isSome(init)) return {
1677
+ kind: "init",
1678
+ init: init.value,
1679
+ declarator: def.node
1680
+ };
1681
+ }
1682
+ return { kind: "none" };
1683
+ }
1684
+ return { kind: "none" };
1685
+ };
1686
+ const importSourceOf = (argument) => {
1687
+ if (argument === void 0 || argument === null) return void 0;
1688
+ if (argument.type !== "ImportExpression") return void 0;
1689
+ const { source } = argument;
1690
+ return source.type === "Literal" && typeof source.value === "string" ? source.value : void 0;
1691
+ };
1692
+ /**
1693
+ * A guard-local `const { FastCheck: fc } = await import('effect/testing')` is a
1694
+ * hand-built generator binding. Resolve it to the same import edge a static import
1695
+ * would produce, so the dynamic idiom cannot hide a hand-built arbitrary behind
1696
+ * an opaque verdict.
1697
+ */
1698
+ const dynamicEdgeOf = (declarator, init, name) => {
1699
+ if (init.type !== "AwaitExpression") return void 0;
1700
+ const source = importSourceOf(init.argument);
1701
+ if (source === void 0) return void 0;
1702
+ const { id } = declarator;
1703
+ if (id.type === "ObjectPattern") {
1704
+ for (const property of id.properties) {
1705
+ if (property.type !== "Property") continue;
1706
+ if (property.key.type !== "Identifier" || property.value.type !== "Identifier") continue;
1707
+ if (property.value.name !== name) continue;
1708
+ return {
1709
+ source,
1710
+ imported: property.key.name
1711
+ };
1712
+ }
1713
+ return;
1714
+ }
1715
+ if (id.type === "Identifier" && id.name === name) return {
1716
+ source,
1717
+ imported: null
1718
+ };
1719
+ };
1720
+ const vocabularyOf = (edge) => {
1721
+ if (SCHEMA_SOURCE_PATTERN.test(edge.source)) return "schema";
1722
+ if (edge.source.startsWith("effect/unstable/arbitrary")) return "schema";
1723
+ if (edge.source === "effect" && edge.imported !== null) {
1724
+ if (SCHEMA_NAMESPACE_NAMES[edge.imported] === true) return "schema";
1725
+ if (FASTCHECK_NAMESPACE_NAMES[edge.imported] === true) return "handBuilt";
1726
+ }
1727
+ if (edge.source === "effect/testing" && edge.imported !== null) {
1728
+ if (FASTCHECK_NAMESPACE_NAMES[edge.imported] === true) return "handBuilt";
1729
+ if (SCHEMA_NAMESPACE_NAMES[edge.imported] === true) return "schema";
1730
+ }
1731
+ if (edge.source === "fast-check" || edge.source.startsWith(`fast-check/`)) return "handBuilt";
1732
+ return "opaque";
1733
+ };
1734
+ var Provenance = class {
1735
+ getScope;
1736
+ imports = /* @__PURE__ */ new Map();
1737
+ constructor(getScope) {
1738
+ this.getScope = getScope;
1739
+ }
1740
+ isLocalFunction(name, node) {
1741
+ const resolved = resolveLocal$1(name, node, this.getScope);
1742
+ if (resolved.kind === "function") return true;
1743
+ return resolved.kind === "init" && (resolved.init.type === "ArrowFunctionExpression" || resolved.init.type === "FunctionExpression");
1744
+ }
1745
+ isSchemaNamespaceBinding(name, node) {
1746
+ if (resolveLocal$1(name, node, this.getScope).kind !== "import") return false;
1747
+ const edge = this.imports.get(name);
1748
+ return edge !== void 0 && edge.source === "effect" && edge.imported === "Schema";
1749
+ }
1750
+ isArbitraryNamespaceBinding(name, node) {
1751
+ if (resolveLocal$1(name, node, this.getScope).kind !== "import") return false;
1752
+ const edge = this.imports.get(name);
1753
+ if (edge === void 0) return false;
1754
+ if (edge.source.startsWith("effect/unstable/arbitrary")) return true;
1755
+ return edge.source === "effect" && edge.imported === "Arbitrary";
1756
+ }
1757
+ classifyCall(name, node) {
1758
+ if (this.isLocalFunction(name, node)) return "domain";
1759
+ const resolved = resolveLocal$1(name, node, this.getScope);
1760
+ if (resolved.kind === "import") {
1761
+ const edge = this.imports.get(name);
1762
+ if (edge === void 0) return "unknown";
1763
+ const verdict = vocabularyOf(edge);
1764
+ return verdict === "schema" || verdict === "handBuilt" ? "codec" : "domain";
1765
+ }
1766
+ if (resolved.kind === "init") {
1767
+ const verdict = this.verdictOf(resolved.init, 0);
1768
+ if (verdict === "schema" || verdict === "handBuilt") return "codec";
1769
+ return "unknown";
1770
+ }
1771
+ return "unknown";
1772
+ }
1773
+ isLocalBinding(name, node) {
1774
+ const resolved = resolveLocal$1(name, node, this.getScope);
1775
+ return resolved.kind === "init" || resolved.kind === "function";
1776
+ }
1777
+ verdictOf(expr, depth) {
1778
+ if (depth > MAX_WALK_DEPTH$1 || !isNode$2(expr)) return "opaque";
1779
+ switch (expr.type) {
1780
+ case "ChainExpression": return this.verdictOf(expr.expression, depth + 1);
1781
+ case "TSAsExpression":
1782
+ case "TSSatisfiesExpression":
1783
+ case "TSNonNullExpression":
1784
+ case "TSTypeAssertion": return this.verdictOf(expr.expression, depth + 1);
1785
+ case "Identifier": {
1786
+ const resolved = resolveLocal$1(expr.name, expr, this.getScope);
1787
+ if (resolved.kind === "import") {
1788
+ const edge = this.imports.get(expr.name);
1789
+ return edge === void 0 ? "opaque" : vocabularyOf(edge);
1790
+ }
1791
+ if (resolved.kind === "init") {
1792
+ const edge = dynamicEdgeOf(resolved.declarator, resolved.init, expr.name);
1793
+ return edge === void 0 ? this.verdictOf(resolved.init, depth + 1) : vocabularyOf(edge);
1794
+ }
1795
+ return "opaque";
1796
+ }
1797
+ case "MemberExpression": return this.verdictOf(expr.object, depth + 1);
1798
+ case "Literal": return "handBuilt";
1799
+ case "ObjectExpression": return this.objectVerdictOf(expr, depth);
1800
+ case "ArrayExpression": return this.reduceArgs(expr.elements, depth);
1801
+ case "CallExpression": return this.callVerdictOf(expr, depth);
1802
+ case "AwaitExpression": return this.verdictOf(expr.argument, depth + 1);
1803
+ default: return "opaque";
1804
+ }
1805
+ }
1806
+ callVerdictOf(call, depth) {
1807
+ const callee = call.callee;
1808
+ if (callee.type === "MemberExpression") {
1809
+ const receiver = this.verdictOf(callee.object, depth + 1);
1810
+ if (receiver === "schema") return "schema";
1811
+ if (receiver === "opaque") return "opaque";
1812
+ return this.reduceArgs(call.arguments, depth);
1813
+ }
1814
+ if (callee.type === "Identifier" && COMBINATOR_CALLEES[callee.name] === true) return this.reduceArgs(call.arguments, depth);
1815
+ const calleeVerdict = this.verdictOf(callee, depth + 1);
1816
+ if (calleeVerdict === "schema") return "schema";
1817
+ if (calleeVerdict === "opaque") return "opaque";
1818
+ return this.reduceArgs(call.arguments, depth);
1819
+ }
1820
+ objectVerdictOf(object, depth) {
1821
+ let sawSchema = false;
1822
+ let sawOpaque = false;
1823
+ for (const property of object.properties) {
1824
+ if (property.type !== "Property" || property.computed) {
1825
+ sawOpaque = true;
1826
+ continue;
1827
+ }
1828
+ const verdict = this.verdictOf(property.value, depth + 1);
1829
+ if (verdict === "handBuilt") return "handBuilt";
1830
+ if (verdict === "schema") sawSchema = true;
1831
+ if (verdict === "opaque") sawOpaque = true;
1832
+ }
1833
+ return sawOpaque ? "opaque" : sawSchema ? "schema" : "handBuilt";
1834
+ }
1835
+ reduceArgs(args, depth) {
1836
+ let sawSchema = false;
1837
+ let sawOpaque = false;
1838
+ for (const arg of args) {
1839
+ if (arg === null) {
1840
+ sawOpaque = true;
1841
+ continue;
1842
+ }
1843
+ if (arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression") continue;
1844
+ const verdict = this.verdictOf(arg, depth + 1);
1845
+ if (verdict === "handBuilt") return "handBuilt";
1846
+ if (verdict === "schema") sawSchema = true;
1847
+ if (verdict === "opaque") sawOpaque = true;
1848
+ }
1849
+ return sawOpaque ? "opaque" : sawSchema ? "schema" : "handBuilt";
1850
+ }
1851
+ };
1852
+ const mentionsLocalBinding = (provenance, value) => {
1853
+ if (Array.isArray(value)) return value.some((item) => mentionsLocalBinding(provenance, item));
1854
+ if (!isNode$2(value)) return false;
1855
+ if (value.type === "Identifier") {
1856
+ if (provenance.isSchemaNamespaceBinding(value.name, value)) return false;
1857
+ if (provenance.isLocalFunction(value.name, value)) return true;
1858
+ return provenance.verdictOf(value, 0) !== "opaque";
1859
+ }
1860
+ for (const [key, child] of Object.entries(value)) {
1861
+ if (key === "parent") continue;
1862
+ if (mentionsLocalBinding(provenance, child)) return true;
1863
+ }
1864
+ return false;
1865
+ };
1866
+ const stockArgVerdict = (provenance, arg) => {
1867
+ if (arg === void 0 || arg === null || !isNode$2(arg)) return false;
1868
+ if (arg.type === "MemberExpression") {
1869
+ if (arg.object.type !== "Identifier" || !provenance.isSchemaNamespaceBinding(arg.object.name, arg)) return false;
1870
+ return true;
1871
+ }
1872
+ if (arg.type === "CallExpression") {
1873
+ const callee = arg.callee;
1874
+ if (callee.type !== "MemberExpression" || callee.object.type !== "Identifier") return false;
1875
+ if (!provenance.isSchemaNamespaceBinding(callee.object.name, arg)) return false;
1876
+ return !mentionsLocalBinding(provenance, arg.arguments);
1877
+ }
1878
+ return false;
1879
+ };
1880
+ const checkArbitraryFactory = (provenance, context, call) => {
1881
+ const callee = call.callee;
1882
+ if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return;
1883
+ const isToArbitrary = callee.property.name === "toArbitrary";
1884
+ const isArbitrarySchema = callee.property.name === "schema" && callee.object.type === "Identifier" && provenance.isArbitraryNamespaceBinding(callee.object.name, call);
1885
+ if (!isToArbitrary && !isArbitrarySchema) return;
1886
+ const arg = call.arguments[0];
1887
+ if (arg === void 0) return;
1888
+ if (stockArgVerdict(provenance, arg)) context.report({
1889
+ node: call,
1890
+ messageId: "stockDerivedArbitrary",
1891
+ data: {
1892
+ name: STOCK_NAME,
1893
+ expected: STOCK_EXPECTED,
1894
+ actual: STOCK_ACTUAL,
1895
+ fix: STOCK_FIX
1896
+ }
1897
+ });
1898
+ };
1899
+ const checkPropCall$1 = (provenance, context, call) => {
1900
+ const arbitraries = call.arguments.find((argument) => argument.type === "ArrayExpression");
1901
+ if (arbitraries === void 0) return;
1902
+ for (const element of arbitraries.elements) {
1903
+ if (element === null) continue;
1904
+ if (provenance.verdictOf(element, 0) !== "handBuilt") continue;
1905
+ context.report({
1906
+ node: element,
1907
+ messageId: "handBuiltArbitrary",
1908
+ data: {
1909
+ name: VIOLATION_NAME$1,
1910
+ expected: EXPECTED$1,
1911
+ actual: ACTUAL$1,
1912
+ fix: FIX$1
1913
+ }
1914
+ });
1915
+ }
1916
+ };
1917
+ const propArbitrarySchemaOrigin = defineRule({
1918
+ meta: meta$8,
1919
+ create(context) {
1920
+ const provenance = new Provenance(context.sourceCode.getScope);
1921
+ const collect = (value, out) => {
1922
+ if (Array.isArray(value)) {
1923
+ for (const item of value) collect(item, out);
1924
+ return;
1925
+ }
1926
+ if (!isNode$2(value)) return;
1927
+ if (value.type === "IfStatement" && mentionsImportMetaVitest$2(value.test)) return;
1928
+ if (value.type === "CallExpression" && isPropCallee(value.callee)) out.push(value);
1929
+ for (const [key, child] of Object.entries(value)) {
1930
+ if (key === "parent") continue;
1931
+ collect(child, out);
1932
+ }
1933
+ };
1934
+ return {
1935
+ Program(node) {
1936
+ for (const statement of node.body) {
1937
+ if (statement.type !== "ImportDeclaration") continue;
1938
+ const source = statement.source.value;
1939
+ for (const specifier of statement.specifiers) if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier") provenance.imports.set(specifier.local.name, {
1940
+ source,
1941
+ imported: specifier.imported.name
1942
+ });
1943
+ else provenance.imports.set(specifier.local.name, {
1944
+ source,
1945
+ imported: null
1946
+ });
1947
+ }
1948
+ },
1949
+ IfStatement(node) {
1950
+ if (!mentionsImportMetaVitest$2(node.test)) return;
1951
+ const calls = [];
1952
+ collect(node.consequent, calls);
1953
+ collect(node.alternate, calls);
1954
+ const factories = [];
1955
+ const collectFactories = (value) => {
1956
+ if (Array.isArray(value)) {
1957
+ for (const item of value) collectFactories(item);
1958
+ return;
1959
+ }
1960
+ if (!isNode$2(value)) return;
1961
+ if (value.type === "CallExpression" && value.callee.type === "MemberExpression" && value.callee.property.type === "Identifier" && (value.callee.property.name === "toArbitrary" || value.callee.property.name === "schema")) factories.push(value);
1962
+ for (const [key, child] of Object.entries(value)) {
1963
+ if (key === "parent") continue;
1964
+ collectFactories(child);
1965
+ }
1966
+ };
1967
+ collectFactories(node.consequent);
1968
+ collectFactories(node.alternate);
1969
+ for (const call of calls) checkPropCall$1(provenance, context, call);
1970
+ for (const factory of factories) checkArbitraryFactory(provenance, context, factory);
1971
+ }
1972
+ };
1973
+ }
1974
+ });
1975
+ //#endregion
1976
+ //#region src/rules/prop-fixture-schema-origin.config.ts
1977
+ const MESSAGE$2 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1978
+ const VIOLATION_NAME = "a hand-rolled recursive schema fixture inside a test block";
1979
+ const EXPECTED = "a recursive fixture schema enters the block through a named local builder call, or its recursion point declares its generation with a visible toCodecArbitrary derivation or a recursionBudget ceiling";
1980
+ const ACTUAL = "the union and its recursion cycle are assembled inline, so the suite exercises a surrogate universe the shipped schema contract never declared and no budget gate grades";
1981
+ const FIX = "hoist the members into a named builder the annotated and counterfactual fixtures share, or declare the recursion point's generation in its annotation";
1982
+ const meta$7 = {
1983
+ type: "problem",
1984
+ docs: { description: "Inside an import.meta.vitest in-source block or a *.test.ts file, a recursive schema union must enter through a named local builder or an imported helper, or its recursion point must declare its generation with a visible toCodecArbitrary derivation or recursionBudget ceiling. An inline hand-rolled recursive union reports; outside test scope the rule is silent, because production schemas are the derivation-cost rule domain." },
1985
+ schema: [],
1986
+ messages: { handRolledRecursiveFixture: MESSAGE$2 }
1987
+ };
1988
+ //#endregion
1989
+ //#region src/rules/prop-fixture-schema-origin.ts
1990
+ const MAX_WALK_DEPTH = 32;
1991
+ const isNode$1 = (value) => value !== null && typeof value === "object" && "type" in value;
1992
+ const isImportMetaVitest = (node) => node.type === "MemberExpression" && node.property.type === "Identifier" && node.property.name === "vitest" && node.object.type === "MetaProperty" && node.object.meta.name === "import" && node.object.property.name === "meta";
1993
+ /**
1994
+ * Broader than test-placement's condition-only guard: this walks the whole test-expression
1995
+ * subtree, so `if (runTests && import.meta.vitest)` is recognised here. Deliberate — plugins
1996
+ * do not share code (KTD8), and a test-scope walk that misses a guard silently disarms the rule.
1997
+ */
1998
+ const mentionsImportMetaVitest$1 = (value) => {
1999
+ if (Array.isArray(value)) return value.some(mentionsImportMetaVitest$1);
2000
+ if (!isNode$1(value)) return false;
2001
+ if (isImportMetaVitest(value)) return true;
2002
+ for (const [key, child] of Object.entries(value)) {
2003
+ if (key === "parent") continue;
2004
+ if (mentionsImportMetaVitest$1(child)) return true;
2005
+ }
2006
+ return false;
2007
+ };
2008
+ const isTestScope = (node, filename) => {
2009
+ if (filename.endsWith(".test.ts")) return true;
2010
+ for (let current = node; current !== null; current = current.parent) if (current.type === "IfStatement" && mentionsImportMetaVitest$1(current.test)) return true;
2011
+ return false;
2012
+ };
2013
+ const isScopeLike = (value) => typeof value === "object" && value !== null && "set" in value && "upper" in value;
2014
+ const resolveLocal = (name, node, getScope) => {
2015
+ const scope = getScope(node);
2016
+ if (!isScopeLike(scope)) return { kind: "none" };
2017
+ for (let current = scope; current !== null; current = current.upper) {
2018
+ const variable = current.set.get(name);
2019
+ if (variable === void 0) continue;
2020
+ for (const def of variable.defs) {
2021
+ if (def.type === "ImportBinding") return { kind: "import" };
2022
+ if (def.node.type === "FunctionDeclaration") return { kind: "function" };
2023
+ if (def.node.type !== "VariableDeclarator") continue;
2024
+ if (def.node.init === null) continue;
2025
+ return {
2026
+ kind: "init",
2027
+ init: def.node.init
2028
+ };
2029
+ }
2030
+ return { kind: "none" };
2031
+ }
2032
+ return { kind: "none" };
2033
+ };
2034
+ const isNamedBuilder = (name, node, getScope) => {
2035
+ const resolved = resolveLocal(name, node, getScope);
2036
+ if (resolved.kind === "function" || resolved.kind === "import") return true;
2037
+ return resolved.kind === "init" && (resolved.init.type === "ArrowFunctionExpression" || resolved.init.type === "FunctionExpression");
2038
+ };
2039
+ /** The identifier thunk of a `Schema.suspend(...)` / `S.suspend(...)` binding, when the name resolves to one. */
2040
+ const suspendThunkOf = (name, node, getScope) => {
2041
+ const resolved = resolveLocal(name, node, getScope);
2042
+ if (resolved.kind !== "init") return void 0;
2043
+ const { init } = resolved;
2044
+ if (init.type !== "CallExpression") return void 0;
2045
+ const { callee } = init;
2046
+ if (callee.type !== "MemberExpression" || callee.computed) return void 0;
2047
+ if (callee.property.type !== "Identifier" || callee.property.name !== "suspend") return void 0;
2048
+ return init.arguments[0];
2049
+ };
2050
+ const mentionsIdentifier = (value, name, depth) => {
2051
+ if (depth > MAX_WALK_DEPTH) return false;
2052
+ if (Array.isArray(value)) return value.some((item) => mentionsIdentifier(item, name, depth + 1));
2053
+ if (!isNode$1(value)) return false;
2054
+ if (value.type === "Identifier") return value.name === name;
2055
+ for (const [key, child] of Object.entries(value)) {
2056
+ if (key === "parent") continue;
2057
+ if (mentionsIdentifier(child, name, depth + 1)) return true;
2058
+ }
2059
+ return false;
2060
+ };
2061
+ const collectIdentifierNames = (value, out, depth) => {
2062
+ if (depth > MAX_WALK_DEPTH) return;
2063
+ if (Array.isArray(value)) {
2064
+ for (const item of value) collectIdentifierNames(item, out, depth + 1);
2065
+ return;
2066
+ }
2067
+ if (!isNode$1(value)) return;
2068
+ if (value.type === "Identifier") {
2069
+ out.push(value.name);
2070
+ return;
2071
+ }
2072
+ for (const [key, child] of Object.entries(value)) {
2073
+ if (key === "parent") continue;
2074
+ collectIdentifierNames(child, out, depth + 1);
2075
+ }
2076
+ };
2077
+ /** The variable the union call is assigned to, when it has a plain-identifier binding. */
2078
+ const bindingNameOf = (union) => {
2079
+ for (let current = union; current !== null; current = current.parent) if (current.type === "VariableDeclarator") return current.id.type === "Identifier" ? current.id.name : void 0;
2080
+ };
2081
+ const unionMembersOf = (union) => {
2082
+ const first = union.arguments[0];
2083
+ if (first !== void 0 && first.type === "ArrayExpression") return first.elements.filter((element) => element !== null);
2084
+ return union.arguments;
2085
+ };
2086
+ const memberNamesOf = (union) => {
2087
+ const names = [];
2088
+ for (const member of unionMembersOf(union)) {
2089
+ if (member.type === "Identifier") {
2090
+ names.push(member.name);
2091
+ continue;
2092
+ }
2093
+ if (member.type === "SpreadElement" && member.argument.type === "Identifier") names.push(member.argument.name);
2094
+ }
2095
+ return names;
2096
+ };
2097
+ const isRecursiveUnion = (union, getScope) => {
2098
+ const binding = bindingNameOf(union);
2099
+ const reachesBinding = (name, seen) => {
2100
+ if (seen.has(name)) return false;
2101
+ seen.add(name);
2102
+ const thunk = suspendThunkOf(name, union, getScope);
2103
+ if (thunk === void 0) return false;
2104
+ if (binding !== void 0 && mentionsIdentifier(thunk, binding, 0)) return true;
2105
+ const nestedNames = [];
2106
+ collectIdentifierNames(thunk, nestedNames, 0);
2107
+ return nestedNames.some((nested) => suspendThunkOf(nested, union, getScope) !== void 0 && reachesBinding(nested, seen));
2108
+ };
2109
+ return memberNamesOf(union).some((name) => reachesBinding(name, /* @__PURE__ */ new Set()));
2110
+ };
2111
+ const annotateCallOf = (union) => {
2112
+ const member = union.parent;
2113
+ if (member === null || member.type !== "MemberExpression") return void 0;
2114
+ if (member.object !== union) return void 0;
2115
+ if (member.property.type !== "Identifier" || member.property.name !== "annotate") return void 0;
2116
+ const call = member.parent;
2117
+ return call !== null && call.type === "CallExpression" && call.callee === member ? call : void 0;
2118
+ };
2119
+ const declaresGenerationIntent = (annotate) => {
2120
+ const options = annotate.arguments[0];
2121
+ if (options === void 0 || options.type !== "ObjectExpression") return false;
2122
+ return options.properties.some((property) => property.type === "Property" && property.key.type === "Identifier" && (property.key.name === "toCodecArbitrary" || property.key.name === "recursionBudget"));
2123
+ };
2124
+ /**
2125
+ * Sanctioned origins (a) and (b): the union is the body or argument of an inline function passed to a
2126
+ * named local builder or an imported helper, optionally through a `.annotate(...)` chain. An IIFE is
2127
+ * not sanctioned — its callee is the inline function itself, not a named binding — so it reports.
2128
+ */
2129
+ const isNamedBuilderOrigin = (union, getScope) => {
2130
+ let current = union;
2131
+ for (let depth = 0; depth <= MAX_WALK_DEPTH; depth += 1) {
2132
+ const parent = current.parent;
2133
+ if (parent === null) return false;
2134
+ if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") {
2135
+ const call = parent.parent;
2136
+ if (call === null || call.type !== "CallExpression") return false;
2137
+ if (call.callee.type !== "Identifier") return false;
2138
+ return isNamedBuilder(call.callee.name, call, getScope);
2139
+ }
2140
+ if (parent.type === "CallExpression") {
2141
+ if (!parent.arguments.some((argument) => argument === current)) return false;
2142
+ if (parent.callee.type !== "Identifier") return false;
2143
+ return isNamedBuilder(parent.callee.name, parent, getScope);
2144
+ }
2145
+ if (parent.type === "MemberExpression" && parent.object === current) {
2146
+ const call = parent.parent;
2147
+ if (call === null || call.type !== "CallExpression" || call.callee !== parent) return false;
2148
+ current = call;
2149
+ continue;
2150
+ }
2151
+ if (parent.type === "VariableDeclarator" || parent.type === "ReturnStatement" || parent.type === "BlockStatement") {
2152
+ current = parent;
2153
+ continue;
2154
+ }
2155
+ return false;
2156
+ }
2157
+ return false;
2158
+ };
2159
+ const propFixtureSchemaOrigin = defineRule({
2160
+ meta: meta$7,
2161
+ create(context) {
2162
+ const getScope = context.sourceCode.getScope;
2163
+ return { CallExpression(node) {
2164
+ const { callee } = node;
2165
+ if (callee.type !== "MemberExpression" || callee.computed) return;
2166
+ if (callee.property.type !== "Identifier" || callee.property.name !== "Union") return;
2167
+ if (callee.object.type !== "Identifier") return;
2168
+ if (!isTestScope(node, context.filename)) return;
2169
+ if (!isRecursiveUnion(node, getScope)) return;
2170
+ const annotate = annotateCallOf(node);
2171
+ if (annotate !== void 0 && declaresGenerationIntent(annotate)) return;
2172
+ if (isNamedBuilderOrigin(node, getScope)) return;
2173
+ context.report({
2174
+ node,
2175
+ messageId: "handRolledRecursiveFixture",
2176
+ data: {
2177
+ name: VIOLATION_NAME,
2178
+ expected: EXPECTED,
2179
+ actual: ACTUAL,
2180
+ fix: FIX
2181
+ }
2182
+ });
2183
+ } };
2184
+ }
2185
+ });
2186
+ //#endregion
2187
+ //#region src/rules/prop-generated-law-duplicate.config.ts
2188
+ const MESSAGE$1 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2189
+ const COMPILER_NAME = "a runtime probe of a compile-time guarantee";
2190
+ const COMPILER_EXPECTED = "the type checker owns brand and nominal-identity contracts — a dropped brand is a compile error at the consumer brand gate, not a failing property";
2191
+ const COMPILER_ACTUAL = "the predicate reflects over brand symbols at runtime, so it re-asserts what tsc already enforces and passes whenever the type system passes";
2192
+ const COMPILER_FIX = "delete the prop; state the brand contract where the compiler reads it — an expectTypeOf assertion or the consuming signature — never a symbol reflection loop";
2193
+ const NO_FUNCTION_NAME = "a property that tests no function";
2194
+ const NO_FUNCTION_EXPECTED = "every in-source property exercises domain logic — at least one call to a module-local function that is not a schema codec accessor — because a predicate that only feeds values through encode or decode tests the schema declaration, not code, and decode acceptance and refusal alike are generated or declared elsewhere";
2195
+ const NO_FUNCTION_ACTUAL = "no call in the predicate reaches module code — every call is a codec accessor, a schema wrapper, or an iteration combinator — so the property cannot fail unless the declaration it restates changes meaning";
2196
+ const NO_FUNCTION_FIX = "delete the prop; test the function that owns the decision — the workflow or a private helper in this module — with its input derived from a domain schema";
2197
+ const meta$6 = {
2198
+ type: "problem",
2199
+ docs: { description: "Inside an import.meta.vitest in-source block: a predicate that reflects over brand symbols re-asserts a compile-time guarantee, and a predicate containing no module-local non-codec function call tests a schema declaration instead of code. Both report." },
2200
+ schema: [],
2201
+ messages: {
2202
+ compilerDuplicate: MESSAGE$1,
2203
+ noDomainFunction: MESSAGE$1
2204
+ }
2205
+ };
2206
+ //#endregion
2207
+ //#region src/rules/prop-generated-law-duplicate.ts
2208
+ /**
2209
+ * Codec accessors and iteration/string combinators: a call whose terminal name
2210
+ * lands in either list never counts as the function under test.
2211
+ */
2212
+ const CODEC_ACCESSORS = {
2213
+ encode: true,
2214
+ encodeSync: true,
2215
+ encodeExit: true,
2216
+ encodeUnknownSync: true,
2217
+ encodeUnknownExit: true,
2218
+ encodeOption: true,
2219
+ decode: true,
2220
+ decodeSync: true,
2221
+ decodeExit: true,
2222
+ decodeUnknownSync: true,
2223
+ decodeUnknownExit: true,
2224
+ decodeOption: true,
2225
+ decodeEither: true,
2226
+ decodePromise: true,
2227
+ decodeUnknownPromise: true,
2228
+ isSuccess: true,
2229
+ isFailure: true,
2230
+ isRight: true,
2231
+ isLeft: true,
2232
+ isSome: true,
2233
+ isNone: true,
2234
+ toEquivalence: true,
2235
+ toEncoded: true,
2236
+ toCodecArbitrary: true,
2237
+ Exit: true
2238
+ };
2239
+ const NEUTRAL_METHODS = {
2240
+ every: true,
2241
+ some: true,
2242
+ includes: true,
2243
+ map: true,
2244
+ filter: true,
2245
+ find: true,
2246
+ join: true,
2247
+ split: true,
2248
+ trim: true,
2249
+ toLowerCase: true,
2250
+ toUpperCase: true,
2251
+ replaceAll: true,
2252
+ replace: true,
2253
+ indexOf: true,
2254
+ startsWith: true,
2255
+ endsWith: true,
2256
+ concat: true,
2257
+ slice: true,
2258
+ get: true,
2259
+ set: true,
2260
+ has: true,
2261
+ keys: true,
2262
+ values: true,
2263
+ entries: true,
2264
+ forEach: true,
2265
+ reduce: true,
2266
+ test: true,
2267
+ match: true,
2268
+ gen: true
2269
+ };
2270
+ const isNode = (value) => value !== null && typeof value === "object" && "type" in value;
2271
+ const mentionsImportMetaVitest = (value) => {
2272
+ if (Array.isArray(value)) return value.some((item) => mentionsImportMetaVitest(item));
2273
+ if (!isNode(value)) return false;
2274
+ if (value.type === "MemberExpression" && value.property.type === "Identifier" && value.property.name === "vitest" && value.object.type === "MetaProperty" && value.object.meta.name === "import" && value.object.property.name === "meta") return true;
2275
+ for (const [key, child] of Object.entries(value)) {
2276
+ if (key === "parent") continue;
2277
+ if (mentionsImportMetaVitest(child)) return true;
2278
+ }
2279
+ return false;
2280
+ };
2281
+ const collectPredicateShape = (provenance, node, shape) => {
2282
+ if (Array.isArray(node)) {
2283
+ for (const item of node) collectPredicateShape(provenance, item, shape);
2284
+ return;
2285
+ }
2286
+ if (!isNode(node)) return;
2287
+ if (node.type === "MemberExpression" && node.property.type === "Identifier") {
2288
+ if (node.property.name === "getOwnPropertySymbols" || node.property.name === "getOwnPropertyNames") shape.usesCompilerProbe = true;
2289
+ }
2290
+ if (node.type === "Identifier" && node.name.endsWith("TypeId")) shape.usesCompilerProbe = true;
2291
+ if (node.type === "CallExpression") {
2292
+ let callee = node.callee;
2293
+ while (callee.type === "CallExpression") callee = callee.callee;
2294
+ if (callee.type === "MemberExpression" && callee.property.type === "Identifier") {
2295
+ const name = callee.property.name;
2296
+ if (CODEC_ACCESSORS[name] !== true && NEUTRAL_METHODS[name] !== true) {
2297
+ const receiverIsLocal = callee.object.type === "Identifier" && provenance.isLocalBinding(callee.object.name, node);
2298
+ if (provenance.classifyCall(name, node) === "domain" || receiverIsLocal) shape.callsDomainFunction = true;
2299
+ }
2300
+ } else if (callee.type === "Identifier") {
2301
+ if (CODEC_ACCESSORS[callee.name] !== true && NEUTRAL_METHODS[callee.name] !== true && provenance.classifyCall(callee.name, node) === "domain") shape.callsDomainFunction = true;
2302
+ }
2303
+ }
2304
+ for (const [key, child] of Object.entries(node)) {
2305
+ if (key === "parent") continue;
2306
+ collectPredicateShape(provenance, child, shape);
2307
+ }
2308
+ };
2309
+ const checkPropCall = (provenance, context, call) => {
2310
+ const predicate = call.arguments[call.arguments.length - 1];
2311
+ if (predicate === void 0 || predicate.type !== "ArrowFunctionExpression") return;
2312
+ const shape = {
2313
+ usesCompilerProbe: false,
2314
+ callsDomainFunction: false
2315
+ };
2316
+ collectPredicateShape(provenance, predicate.body, shape);
2317
+ if (shape.usesCompilerProbe) {
2318
+ context.report({
2319
+ node: call,
2320
+ messageId: "compilerDuplicate",
2321
+ data: {
2322
+ name: COMPILER_NAME,
2323
+ expected: COMPILER_EXPECTED,
2324
+ actual: COMPILER_ACTUAL,
2325
+ fix: COMPILER_FIX
2326
+ }
2327
+ });
2328
+ return;
2329
+ }
2330
+ if (!shape.callsDomainFunction) context.report({
2331
+ node: call,
2332
+ messageId: "noDomainFunction",
2333
+ data: {
2334
+ name: NO_FUNCTION_NAME,
2335
+ expected: NO_FUNCTION_EXPECTED,
2336
+ actual: NO_FUNCTION_ACTUAL,
2337
+ fix: NO_FUNCTION_FIX
2338
+ }
2339
+ });
2340
+ };
2341
+ const propGeneratedLawDuplicate = defineRule({
2342
+ meta: meta$6,
2343
+ create(context) {
2344
+ const provenance = new Provenance(context.sourceCode.getScope);
2345
+ const collect = (value, out) => {
2346
+ if (Array.isArray(value)) {
2347
+ for (const item of value) collect(item, out);
2348
+ return;
2349
+ }
2350
+ if (!isNode(value)) return;
2351
+ if (value.type === "CallExpression" && isPropCallee(value.callee)) out.push(value);
2352
+ for (const [key, child] of Object.entries(value)) {
2353
+ if (key === "parent") continue;
2354
+ collect(child, out);
2355
+ }
2356
+ };
2357
+ return {
2358
+ Program(node) {
2359
+ for (const statement of node.body) {
2360
+ if (statement.type !== "ImportDeclaration") continue;
2361
+ const source = statement.source.value;
2362
+ for (const specifier of statement.specifiers) if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier") provenance.imports.set(specifier.local.name, {
2363
+ source,
2364
+ imported: specifier.imported.name
2365
+ });
2366
+ else provenance.imports.set(specifier.local.name, {
2367
+ source,
2368
+ imported: null
2369
+ });
2370
+ }
2371
+ },
2372
+ IfStatement(node) {
2373
+ if (!mentionsImportMetaVitest(node.test)) return;
2374
+ const calls = [];
2375
+ collect(node.consequent, calls);
2376
+ collect(node.alternate, calls);
2377
+ for (const call of calls) checkPropCall(provenance, context, call);
2378
+ }
2379
+ };
2380
+ }
2381
+ });
2382
+ //#endregion
2383
+ //#region src/rules/property-file-purity.config.ts
2384
+ const PROPERTY_TEST_SUFFIX = ".property.test.ts";
2385
+ const MESSAGE = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2386
+ const meta$5 = {
2387
+ type: "problem",
2388
+ docs: { description: "Property tests live ONLY in *.property.test.ts files, and those files contain ONLY property tests. In a property file: no plain it()/test()/it.effect(), no raw fc.assert/fc.check/fc.property/fc.asyncProperty. In any other test file: no FastCheck import and no it.prop/it.effect.prop — move the property to a *.property.test.ts file." },
2389
+ schema: [],
2390
+ messages: {
2391
+ plainIt: MESSAGE,
2392
+ plainEffectIt: MESSAGE,
2393
+ rawFastCheck: MESSAGE,
2394
+ fastCheckImport: MESSAGE,
2395
+ propCall: MESSAGE
2396
+ }
2397
+ };
2398
+ //#endregion
2399
+ //#region src/rules/property-file-purity.ts
2400
+ const RAW_FC_METHODS = /* @__PURE__ */ new Set([
2401
+ "assert",
2402
+ "check",
2403
+ "property",
2404
+ "asyncProperty"
2405
+ ]);
2406
+ const reportPlain = (context, node, messageId, actual) => {
2407
+ context.report({
2408
+ node,
2409
+ messageId,
2410
+ data: {
2411
+ name: `scenario test (${actual}) in a ${PROPERTY_TEST_SUFFIX} file`,
2412
+ expected: "it.prop(...) or it.effect.prop(...) — property files never mix with scenario tests",
2413
+ actual: `${actual} runs a single example, not a property`,
2414
+ fix: "move the scenario test to a plain *.test.ts file, or rewrite it as a property with arbitraries and a boolean-returning predicate"
2415
+ }
2416
+ });
2417
+ };
2418
+ const createPropertyFileVisitors = (context) => ({ CallExpression(node) {
2419
+ const callee = node.callee;
2420
+ if (callee.type === "Identifier") {
2421
+ if (callee.name === "it" || callee.name === "test") reportPlain(context, node, "plainIt", `${callee.name}(...)`);
2422
+ return;
2423
+ }
2424
+ if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return;
2425
+ const object = callee.object;
2426
+ if (object.type === "Identifier" && object.name === "fc" && RAW_FC_METHODS.has(callee.property.name)) {
2427
+ context.report({
2428
+ node,
2429
+ messageId: "rawFastCheck",
2430
+ data: {
2431
+ name: `raw fc.${callee.property.name}(...) in a ${PROPERTY_TEST_SUFFIX} file`,
2432
+ expected: "it.prop(...) or it.effect.prop(...) from @effect/vitest",
2433
+ actual: `fc.${callee.property.name}(...) bypasses the vitest/Effect integration`,
2434
+ fix: "rewrite as it.prop(name, [arbitraries], predicate) returning a boolean; fc.* stays for building arbitraries (fc.pre, fc.stringMatching, ...)"
2435
+ }
2436
+ });
2437
+ return;
2438
+ }
2439
+ if (object.type === "Identifier" && object.name === "it") {
2440
+ if (callee.property.name === "effect") reportPlain(context, node, "plainEffectIt", "it.effect(...)");
2441
+ else if (PROP_MODIFIERS.has(callee.property.name)) reportPlain(context, node, "plainIt", `it.${callee.property.name}(...)`);
2442
+ return;
2443
+ }
2444
+ if (PROP_MODIFIERS.has(callee.property.name) && object.type === "MemberExpression" && object.object.type === "Identifier" && object.object.name === "it" && object.property.type === "Identifier" && object.property.name === "effect") reportPlain(context, node, "plainEffectIt", `it.effect.${callee.property.name}(...)`);
2445
+ } });
2446
+ const FAST_CHECK_IMPORT_DATA = {
2447
+ name: "FastCheck import in a scenario test file",
2448
+ expected: `property tests (and every FastCheck usage) live in ${PROPERTY_TEST_SUFFIX} files`,
2449
+ actual: `FastCheck imported by a file that is not ${PROPERTY_TEST_SUFFIX}`,
2450
+ fix: "move the property test to a *.property.test.ts file; this file keeps plain it() scenario tests only"
2451
+ };
2452
+ const reportFastCheckImport = (context, node) => {
2453
+ context.report({
2454
+ node,
2455
+ messageId: "fastCheckImport",
2456
+ data: FAST_CHECK_IMPORT_DATA
2457
+ });
2458
+ };
2459
+ const createScenarioFileVisitors = (context) => ({
2460
+ ImportDeclaration(node) {
2461
+ if (node.source.value === "fast-check" || node.source.value.startsWith("fast-check/")) {
2462
+ reportFastCheckImport(context, node);
2463
+ return;
2464
+ }
2465
+ for (const specifier of node.specifiers) if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "FastCheck") reportFastCheckImport(context, specifier);
2466
+ },
2467
+ CallExpression(node) {
2468
+ if (!isPropCallee(node.callee)) return;
2469
+ context.report({
2470
+ node,
2471
+ messageId: "propCall",
2472
+ data: {
2473
+ name: "property test in a non-property test file",
2474
+ expected: `it.prop / it.effect.prop calls live in ${PROPERTY_TEST_SUFFIX} files`,
2475
+ actual: "a property test mixed into a test file that is not a property file",
2476
+ fix: "move this test to a *.property.test.ts file — property and non-property tests never mix"
2477
+ }
2478
+ });
2479
+ }
2480
+ });
2481
+ const propertyFilePurity = defineRule({
2482
+ meta: meta$5,
2483
+ create(context) {
2484
+ if (!Array$1.last(context.filename.split("/")).pipe(Option.exists((base) => base.includes(".test.") || base.includes(".spec.")))) return {};
2485
+ if (context.filename.endsWith(".property.test.ts")) return createPropertyFileVisitors(context);
2486
+ return createScenarioFileVisitors(context);
2487
+ }
2488
+ });
2489
+ //#endregion
2490
+ //#region src/rules/src-property-test-cell.config.ts
2491
+ const Options = Schema.Struct({ cellsRequiringTest: Schema.Array(Schema.String).pipe(Schema.annotate({ description: "Cell suffixes whose source files must carry a test, named without the leading dot (e.g. [\"kernel\", \"workflow\"]). Empty by default: a consumer who declares nothing is never accused. A cell listed here is satisfied by an in-source `if (import.meta.vitest)` block, which is the only form the rule can read from the file it is given." }), Schema.withDecodingDefaultType(Effect.succeed([]))) });
2492
+ const UNSANCTIONED_CELL_EXPECTED = "a property test named <stem>.workflow.property.test.ts inside __tests__, beside the <stem>.workflow.ts it covers";
2493
+ const UNSANCTIONED_CELL_ACTUAL = "a property test under src/ whose basename is not a single-segment <stem>.workflow.property.test.ts";
2494
+ const UNSANCTIONED_CELL_FIX = "rename it <stem>.workflow.property.test.ts beside the workflow it covers; a kernel/policy/schema property suite has no file home under the new taxonomy — convert it to an in-source if (import.meta.vitest) block in the module it covers";
2495
+ const MISSING_CELL_TEST_EXPECTED = "a test for every cell suffix the consumer lists in cellsRequiringTest";
2496
+ const MISSING_CELL_TEST_ACTUAL = "a declared cell whose own module carries no `if (import.meta.vitest)` block";
2497
+ const MISSING_CELL_TEST_FIX = "add an `if (import.meta.vitest)` block to this module, or drop this cell from cellsRequiringTest and cover it with a colocated test in a sanctioned test directory — the rule reads the file it is given, so a sibling test file is invisible to it and a declared cell must satisfy the requirement from its own source";
2498
+ const meta$4 = {
2499
+ type: "problem",
2500
+ docs: { description: "A property test under src/ must be a single-segment <stem>.workflow.property.test.ts beside the <stem>.workflow.ts it covers; every other property-test basename is unsanctioned. A source file whose suffix names a cell listed in the cellsRequiringTest option must additionally carry an in-source vitest block; that list is empty by default, so the presence arm is opt-in per consumer." },
2501
+ schema: [Schema.toJsonSchemaDocument(Options).schema],
2502
+ messages: {
2503
+ unsanctionedCell: MESSAGE$7,
2504
+ missingCellTest: ABSENCE_MESSAGE
2505
+ }
2506
+ };
2507
+ //#endregion
2508
+ //#region src/rules/src-property-test-cell.ts
2509
+ const carriesInSourceBlock = (body) => body.some((statement) => statement.type === "IfStatement" && isVitestGuard(statement.test));
2510
+ const srcPropertyTestCell = defineRule({
2511
+ meta: meta$4,
2512
+ create(context) {
2513
+ const { cellsRequiringTest } = Schema.decodeUnknownSync(Options)(context.options[0] ?? {});
2514
+ const filename = context.filename;
2515
+ const basename = basenameOf(filename);
2516
+ if (!isUnderSrc(filename)) return {};
2517
+ if (basename.endsWith(".property.test.ts")) {
2518
+ if (WORKFLOW_TEST_BASENAME.test(basename)) return {};
2519
+ return { Program(node) {
2520
+ context.report({
2521
+ node,
2522
+ messageId: "unsanctionedCell",
2523
+ data: {
2524
+ name: basename,
2525
+ expected: UNSANCTIONED_CELL_EXPECTED,
2526
+ actual: UNSANCTIONED_CELL_ACTUAL,
2527
+ fix: UNSANCTIONED_CELL_FIX
2528
+ }
2529
+ });
2530
+ } };
2531
+ }
2532
+ if (isTestFile(basename)) return {};
2533
+ const cell = cellOf(basename);
2534
+ if (cell === void 0) return {};
2535
+ if (!cellsRequiringTest.includes(cell)) return {};
2536
+ return { Program(node) {
2537
+ if (carriesInSourceBlock(node.body)) return;
2538
+ context.report({
2539
+ node,
2540
+ messageId: "missingCellTest",
2541
+ data: {
2542
+ name: basename,
2543
+ expected: MISSING_CELL_TEST_EXPECTED,
2544
+ actual: MISSING_CELL_TEST_ACTUAL,
2545
+ fix: MISSING_CELL_TEST_FIX
2546
+ }
2547
+ });
2548
+ } };
2549
+ }
2550
+ });
2551
+ //#endregion
2552
+ //#region src/rules/test-file-outside-tests-dir.config.ts
2553
+ const SANCTIONED_DIRS = [...SANCTIONED_TEST_DIRS].map((dir) => `${dir}/`).join(" or ");
2554
+ const STRAY_TEST_FILE_EXPECTED = `test files outside src/ under a ${SANCTIONED_DIRS} directory`;
2555
+ const STRAY_TEST_FILE_ACTUAL = "a test file outside src/ and outside any tests directory";
2556
+ const STRAY_TEST_FILE_FIX = `move it into the package ${SANCTIONED_DIRS} directory`;
2557
+ //#endregion
2558
+ //#region src/rules/test-file-outside-tests-dir.ts
2559
+ const testFileOutsideTestsDir = defineRule({
2560
+ meta: {
2561
+ type: "problem",
2562
+ docs: { description: "Test files outside src/ must live under a tests/ directory; free-standing test files have no sanctioned home." },
2563
+ schema: [],
2564
+ messages: { strayTestFile: MESSAGE$7 }
2565
+ },
2566
+ create(context) {
2567
+ if (isUnderSrc(context.filename)) return {};
2568
+ if (!isTestFile(basenameOf(context.filename))) return {};
2569
+ if (isInSanctionedTestDir(context.filename)) return {};
2570
+ return { Program(node) {
2571
+ context.report({
2572
+ node,
2573
+ messageId: "strayTestFile",
2574
+ data: {
2575
+ name: basenameOf(context.filename),
2576
+ expected: STRAY_TEST_FILE_EXPECTED,
2577
+ actual: STRAY_TEST_FILE_ACTUAL,
2578
+ fix: STRAY_TEST_FILE_FIX
2579
+ }
2580
+ });
2581
+ } };
2582
+ }
2583
+ });
2584
+ //#endregion
2585
+ //#region src/rules/test-suffix-outside-src.config.ts
2586
+ const UNSANCTIONED_SUFFIX_EXPECTED = "exactly *.integration.test.ts outside src/";
2587
+ const UNSANCTIONED_SUFFIX_ACTUAL = "an unsanctioned test suffix outside src/";
2588
+ const UNSANCTIONED_SUFFIX_FIX = "name what this file exercises. Every scenario restates a literal from a pure cell (a lookup-table entry, a constant, a mapping) -> it is a change detector, not a test: delete it. It drives the package through its public surface -> rename it *.integration.test.ts, the one behaviour suffix, whether or not a layer doubles at a port. It is a property over a pure cell -> it does not belong outside src/: convert it to an in-source if (import.meta.vitest) block in the module it covers";
2589
+ //#endregion
2590
+ //#region src/rules/test-suffix-outside-src.ts
2591
+ const testSuffixOutsideSrc = defineRule({
2592
+ meta: {
2593
+ type: "problem",
2594
+ docs: { description: "Outside src/, a test file must end .integration.test.ts — the one behaviour suffix. Whether the layer doubles at a port is a judgement the suffix no longer encodes." },
2595
+ schema: [],
2596
+ messages: { unsanctionedSuffix: MESSAGE$7 }
2597
+ },
2598
+ create(context) {
2599
+ const filename = context.filename;
2600
+ const basename = basenameOf(filename);
2601
+ const suffixIsAllowed = basename.endsWith(INTEGRATION_SUFFIX);
2602
+ return { Program(node) {
2603
+ if (isUnderSrc(filename)) return;
2604
+ if (!isTestFile(basename)) return;
2605
+ if (suffixIsAllowed) return;
2606
+ context.report({
2607
+ node,
2608
+ messageId: "unsanctionedSuffix",
2609
+ data: {
2610
+ name: basename,
2611
+ expected: UNSANCTIONED_SUFFIX_EXPECTED,
2612
+ actual: UNSANCTIONED_SUFFIX_ACTUAL,
2613
+ fix: UNSANCTIONED_SUFFIX_FIX
2614
+ }
2615
+ });
2616
+ } };
2617
+ }
2618
+ });
2619
+ //#endregion
2620
+ //#region src/rules/tests-dir-helpers-in-fixtures.config.ts
2621
+ const HELPER_EXPECTED = "non-test helper and fixture modules under tests/ to live inside tests/__fixtures__/";
2622
+ const HELPER_ACTUAL = "a non-test module in tests/ outside __fixtures__/";
2623
+ const HELPER_FIX = "move it under tests/__fixtures__/ — *.schema.ts if it declares schemas, <stem>.workflow.ts if it constructs a workflow";
2624
+ //#endregion
2625
+ //#region src/rules/tests-dir-helpers-in-fixtures.ts
2626
+ const testsDirHelpersInFixtures = defineRule({
2627
+ meta: {
2628
+ type: "problem",
2629
+ docs: { description: "Under tests/, the only non-test modules are helpers and fixtures, and they live inside tests/__fixtures__/." },
2630
+ schema: [],
2631
+ messages: { helperOutsideFixtures: MESSAGE$7 }
2632
+ },
2633
+ create(context) {
2634
+ const filename = context.filename;
2635
+ if (isUnderSrc(filename)) return {};
2636
+ const basename = basenameOf(filename);
2637
+ if (isTestFile(basename)) return {};
2638
+ const dirs = directoriesOf(filename);
2639
+ if (!dirs.includes("tests")) return {};
2640
+ if (dirs.includes("__fixtures__")) return {};
2641
+ return { Program(node) {
2642
+ context.report({
2643
+ node,
2644
+ messageId: "helperOutsideFixtures",
2645
+ data: {
2646
+ name: basename,
2647
+ expected: HELPER_EXPECTED,
2648
+ actual: HELPER_ACTUAL,
2649
+ fix: HELPER_FIX
2650
+ }
2651
+ });
2652
+ } };
2653
+ }
2654
+ });
2655
+ //#endregion
2656
+ //#region src/rules/tests-import-public-api.config.ts
2657
+ const REACH_IN_EXPECTED = "a package name or subpath, or a sibling helper under the test tree";
2658
+ const REACH_IN_ACTUAL = "a relative import that reaches src or climbs into an internal folder";
2659
+ const REACH_IN_FIX = "rewrite onto the published package name when the binding is public. Delete the test when the subject is an internal";
2660
+ const meta = {
2661
+ type: "problem",
2662
+ docs: { description: "Forbid package-level tests from relative-importing src or climbing into an internal folder" },
2663
+ schema: [],
2664
+ messages: { sourceReachIn: MESSAGE$7 }
2665
+ };
2666
+ //#endregion
2667
+ //#region src/rules/tests-import-public-api.ts
2668
+ const isForbiddenRelativeSpecifier = (value) => {
2669
+ if (!value.startsWith(".")) return false;
2670
+ let sawDotDot = false;
2671
+ for (const segment of value.split("/")) {
2672
+ if (segment === "..") sawDotDot = true;
2673
+ if (segment === "src") return true;
2674
+ if (sawDotDot && segment === "internal") return true;
2675
+ }
2676
+ return false;
2677
+ };
2678
+ const specifierOf = (node) => {
2679
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
2680
+ };
2681
+ const testsImportPublicApi = defineRule({
2682
+ meta,
2683
+ create(context) {
2684
+ if (!isInTestsImportScope(context.filename)) return {};
2685
+ const reportIfForbidden = (sourceNode) => {
2686
+ const value = specifierOf(sourceNode);
2687
+ if (value === void 0 || !isForbiddenRelativeSpecifier(value)) return;
2688
+ context.report({
2689
+ node: sourceNode,
2690
+ messageId: "sourceReachIn",
2691
+ data: {
2692
+ name: value,
2693
+ expected: REACH_IN_EXPECTED,
2694
+ actual: REACH_IN_ACTUAL,
2695
+ fix: REACH_IN_FIX
2696
+ }
2697
+ });
2698
+ };
2699
+ return {
2700
+ ImportDeclaration(node) {
2701
+ reportIfForbidden(node.source);
2702
+ },
2703
+ ExportNamedDeclaration(node) {
2704
+ if (node.source !== null) reportIfForbidden(node.source);
2705
+ },
2706
+ ExportAllDeclaration(node) {
2707
+ reportIfForbidden(node.source);
2708
+ },
2709
+ ImportExpression(node) {
2710
+ reportIfForbidden(node.source);
2711
+ },
2712
+ TSImportEqualsDeclaration(node) {
2713
+ if (node.moduleReference.type === "TSExternalModuleReference") reportIfForbidden(node.moduleReference.expression);
2714
+ }
2715
+ };
2716
+ }
2717
+ });
2718
+ //#endregion
2719
+ //#region src/index.ts
2720
+ const PLUGIN_NAME = "@systemfsoftware/oxlint-plugin-test-discipline";
2721
+ const rule = (name) => `${PLUGIN_NAME}/${name}`;
2722
+ const recommendedRules = {
2723
+ [rule("damp-test-naming")]: "error",
2724
+ [rule("no-behaviourless-assertion")]: "error",
2725
+ [rule("pbt-naming")]: "error",
2726
+ [rule("no-silent-return")]: "error",
2727
+ [rule("no-assert-in-property")]: "error",
2728
+ [rule("property-file-purity")]: "error",
2729
+ [rule("no-nested-quantification")]: "error",
2730
+ [rule("prop-arbitrary-schema-origin")]: "error",
2731
+ [rule("prop-fixture-schema-origin")]: "error",
2732
+ [rule("prop-generated-law-duplicate")]: "error",
2733
+ [rule("in-source-test-prop-only")]: "error",
2734
+ [rule("in-source-test-targets-private")]: "error",
2735
+ [rule("no-test-file-in-src")]: "error",
2736
+ [rule("src-property-test-cell")]: "error",
2737
+ [rule("test-file-outside-tests-dir")]: "error",
2738
+ [rule("test-suffix-outside-src")]: "error",
2739
+ [rule("behaviour-test-requires-gherkin")]: "error",
2740
+ [rule("behaviour-exercises-use-case")]: "error",
2741
+ [rule("behaviour-one-feature-per-file")]: "error",
2742
+ [rule("tests-dir-helpers-in-fixtures")]: "error",
2743
+ [rule("no-io-module-in-source-test")]: "error",
2744
+ [rule("tests-import-public-api")]: "error"
2745
+ };
2746
+ var src_default = {
2747
+ meta: { name: PLUGIN_NAME },
2748
+ rules: {
2749
+ "damp-test-naming": dampTestNaming,
2750
+ "no-behaviourless-assertion": noBehaviourlessAssertion,
2751
+ "pbt-naming": pbtNaming,
2752
+ "no-silent-return": noSilentReturn,
2753
+ "no-assert-in-property": noAssertInProperty,
2754
+ "property-file-purity": propertyFilePurity,
2755
+ "no-nested-quantification": noNestedQuantification,
2756
+ "prop-generated-law-duplicate": propGeneratedLawDuplicate,
2757
+ "prop-arbitrary-schema-origin": propArbitrarySchemaOrigin,
2758
+ "prop-fixture-schema-origin": propFixtureSchemaOrigin,
2759
+ "in-source-test-prop-only": inSourceTestPropOnly,
2760
+ "in-source-test-targets-private": inSourceTestTargetsPrivate,
2761
+ "no-test-file-in-src": noTestFileInSrc,
2762
+ "src-property-test-cell": srcPropertyTestCell,
2763
+ "test-file-outside-tests-dir": testFileOutsideTestsDir,
2764
+ "test-suffix-outside-src": testSuffixOutsideSrc,
2765
+ "behaviour-test-requires-gherkin": behaviourTestRequiresGherkin,
2766
+ "behaviour-exercises-use-case": behaviourExercisesUseCase,
2767
+ "behaviour-one-feature-per-file": behaviourOneFeaturePerFile,
2768
+ "no-pseudo-gherkin-unit-tests": noPseudoGherkinUnitTests,
2769
+ "tests-dir-helpers-in-fixtures": testsDirHelpersInFixtures,
2770
+ "no-io-module-in-source-test": noIoModuleInSourceTest,
2771
+ "tests-import-public-api": testsImportPublicApi
2772
+ },
2773
+ configs: { recommended: { rules: recommendedRules } }
2774
+ };
2775
+ //#endregion
2776
+ export { src_default as default };