@systemfsoftware/oxlint-plugin-test-discipline 4.1.0 → 6.1.3

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 CHANGED
@@ -8,7 +8,7 @@ const EMIT_CALLEES = [
8
8
  ];
9
9
  const EXPECTED$3 = "a span declared once with Span.declare({ id, name, attrs }) and started through the start(attrs) it returns";
10
10
  const FIX$3 = "declare the span with Span.declare({ id, name, attrs }) and emit it with SomeSpan.start(attrs)";
11
- const meta$28 = {
11
+ const meta$29 = {
12
12
  type: "problem",
13
13
  docs: { description: "Ban a raw span name passed to an emit API (startSpan, spanBuilder, startActiveSpan). Span names enter through Span.declare, never as a literal at the emit site." },
14
14
  schema: [],
@@ -23,7 +23,7 @@ const memberPropertyName$1 = (callee) => {
23
23
  if (callee.property.type !== "Identifier") return null;
24
24
  return callee.property.name;
25
25
  };
26
- const calleeName$2 = (callee) => {
26
+ const calleeName$3 = (callee) => {
27
27
  if (callee.type === "Identifier") return callee.name;
28
28
  if (callee.type === "MemberExpression") return memberPropertyName$1(callee);
29
29
  return null;
@@ -39,7 +39,7 @@ const argName = (arg) => {
39
39
  return inlineName(arg);
40
40
  };
41
41
  const inspect = (node) => {
42
- const callee = calleeName$2(node.callee);
42
+ const callee = calleeName$3(node.callee);
43
43
  if (callee === null || !isEmitCallee$1(callee)) return null;
44
44
  const name = argName(node.arguments[0]);
45
45
  if (name === null) return null;
@@ -49,7 +49,7 @@ const inspect = (node) => {
49
49
  };
50
50
  };
51
51
  const banRawSpanNameEmit = defineRule({
52
- meta: meta$28,
52
+ meta: meta$29,
53
53
  create(context) {
54
54
  return { CallExpression(node) {
55
55
  const emitCall = inspect(node);
@@ -74,6 +74,11 @@ const TEST_BASENAME = /\.(?:test|spec)\.[cm]?tsx?$/;
74
74
  const TYPE_TEST_BASENAME = /\.tst\.[cm]?tsx?$/;
75
75
  const SANCTIONED_TEST_DIRS = /* @__PURE__ */ new Set(["tests"]);
76
76
  /**
77
+ * The basename of a test the `effect-e2e` runner collects. Mirrors the runner's
78
+ * default pattern `tests/**\/*.e2e.ts`, which is `.ts` only.
79
+ */
80
+ const E2E_TEST_BASENAME = /\.e2e\.ts$/;
81
+ /**
77
82
  * The only test location sanctioned under `src/`. A workflow property test
78
83
  * earns colocation with the workflow it covers, but not adjacency: it lives in
79
84
  * a `__tests__` directory beside that workflow, never as a sibling file.
@@ -81,8 +86,6 @@ const SANCTIONED_TEST_DIRS = /* @__PURE__ */ new Set(["tests"]);
81
86
  const NESTED_TEST_DIR = "__tests__";
82
87
  const TEST_TREE_DIRS = /* @__PURE__ */ new Set([...SANCTIONED_TEST_DIRS, NESTED_TEST_DIR]);
83
88
  const PROPERTY_SUFFIX = ".property.test.ts";
84
- const INTEGRATION_SUFFIX = ".integration.test.ts";
85
- const DIFFERENTIAL_SUFFIX = ".differential.test.ts";
86
89
  /**
87
90
  * Forbidden outright. A schema's laws are generated, so a hand-written
88
91
  * `*.schema.test.ts` only ever restates coverage that already exists.
@@ -110,30 +113,31 @@ const FOREIGN_RUNNERS = {
110
113
  ...FOREIGN_VITEST_SOURCES,
111
114
  [LAWFUL_RUNNER]: true
112
115
  };
113
- /** Packages whose own tests drive Vitest directly: the runner and its conformance harness. */
114
- const RAW_VITEST_PACKAGES = {
115
- "effect-spec-runtime": true,
116
- "vitest": true,
117
- "vitest-conformance": true
116
+ /** fast-check calls that run a property themselves instead of handing an arbitrary to a harness. */
117
+ const RAW_FC_METHODS = {
118
+ assert: true,
119
+ check: true,
120
+ property: true,
121
+ asyncProperty: true
118
122
  };
119
123
  const RUNNER_NAMES = /* @__PURE__ */ new Set([
120
124
  "it",
121
125
  "test",
122
126
  "describe"
123
127
  ]);
124
- const MESSAGE$8 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
128
+ const MESSAGE$6 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
125
129
  const ABSENCE_MESSAGE = "{{name}} is untested. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
126
130
  //#endregion
127
131
  //#region src/rules/behaviour-exercises-use-case.config.ts
128
- const NO_SUBJECT_IMPORT_NAME = "a *.integration.test.ts that reaches no package code";
132
+ const NO_SUBJECT_IMPORT_NAME = "a behaviour test that reaches no package code";
129
133
  const NO_SUBJECT_IMPORT_EXPECTED = "an import of the package code under test";
130
- const NO_SUBJECT_IMPORT_ACTUAL = "a behaviour file whose every runtime import is vitest, @effect/vitest, @systemfsoftware/vitest, the gherkin spec package, effect, a Node builtin, or the file itself";
134
+ const NO_SUBJECT_IMPORT_ACTUAL = "a test importing @systemfsoftware/effect-gherkin-spec whose every runtime import is vitest, @effect/vitest, @systemfsoftware/vitest, the gherkin spec package, effect, a Node builtin, or the file itself";
131
135
  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.";
132
- const meta$27 = {
136
+ const meta$28 = {
133
137
  type: "problem",
134
- 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." },
138
+ docs: { description: "A behaviour test - any test file that imports @systemfsoftware/effect-gherkin-spec, whatever its name - 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." },
135
139
  schema: [],
136
- messages: { noSubjectImport: MESSAGE$8 }
140
+ messages: { noSubjectImport: MESSAGE$6 }
137
141
  };
138
142
  //#endregion
139
143
  //#region src/rules/path.ts
@@ -143,7 +147,6 @@ const basenameOf = (filename) => Array$1.lastNonEmpty(segmentsOf(filename));
143
147
  /** Directory segments only — the basename never counts as a directory. */
144
148
  const directoriesOf = (filename) => Array$1.initNonEmpty(segmentsOf(filename));
145
149
  const isUnderSrc = (filename) => directoriesOf(filename).includes("src");
146
- const isRawVitestPackage = (filename) => directoriesOf(filename).some((segment) => RAW_VITEST_PACKAGES[segment] === true);
147
150
  const isInSanctionedTestDir = (filename) => directoriesOf(filename).some((segment) => SANCTIONED_TEST_DIRS.has(segment));
148
151
  const isInTestsImportScope = (filename) => {
149
152
  const segments = segmentsOf(filename);
@@ -153,10 +156,146 @@ const isInTestsImportScope = (filename) => {
153
156
  };
154
157
  const isInConfiguredTestDir = (filename, dirs) => directoriesOf(filename).some((segment) => dirs.includes(segment));
155
158
  const isTestFile = (basename) => TEST_BASENAME.test(basename);
156
- const isBehaviourBasename = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
159
+ /**
160
+ * A file the `effect-e2e` runner collects: an `.e2e.ts` basename under a `tests`
161
+ * directory with no `__fixtures__` directory directly beneath a `tests`
162
+ * directory, which is the runner's `tests/__fixtures__/**` exclusion.
163
+ */
164
+ const isE2eTestFile = (filename) => {
165
+ const directories = directoriesOf(filename);
166
+ return E2E_TEST_BASENAME.test(basenameOf(filename)) && directories.some((segment) => SANCTIONED_TEST_DIRS.has(segment)) && !directories.some((segment, index) => segment === "__fixtures__" && SANCTIONED_TEST_DIRS.has(directories[index - 1] ?? ""));
167
+ };
157
168
  const CELL_SUFFIX = /\.([^.]+)\.[cm]?tsx?$/;
158
169
  const cellOf = (basename) => CELL_SUFFIX.exec(basename)?.[1];
159
170
  //#endregion
171
+ //#region src/rules/prop-call.ts
172
+ const PROP_MODIFIERS$1 = /* @__PURE__ */ new Set([
173
+ "only",
174
+ "skip",
175
+ "todo"
176
+ ]);
177
+ const isPropCallee$1 = (callee) => {
178
+ if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return false;
179
+ if (callee.property.name === "prop") {
180
+ const object = callee.object;
181
+ if (object.type === "Identifier") return object.name === "it";
182
+ return object.type === "MemberExpression" && object.property.type === "Identifier" && object.property.name === "effect" && object.object.type === "Identifier" && object.object.name === "it";
183
+ }
184
+ return PROP_MODIFIERS$1.has(callee.property.name) && isPropCallee$1(callee.object);
185
+ };
186
+ const getPredicate = (node) => Array$1.findLast(node.arguments, (arg) => arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression");
187
+ //#endregion
188
+ //#region src/rules/lane.ts
189
+ const HARNESS_LANES = [
190
+ [GHERKIN_PACKAGE, "behaviour"],
191
+ [CONFORMANCE_PACKAGE, "conformance"],
192
+ [DIFFERENTIAL_PACKAGE, "differential"],
193
+ [TRACE_SPEC_PACKAGE, "trace"]
194
+ ];
195
+ const isPackageOrSubpath = (source, pkg) => source === pkg || source.startsWith(`${pkg}/`);
196
+ /** The lane a harness import selects: the package itself or any subpath of it. */
197
+ const harnessLaneOf = (source) => HARNESS_LANES.find(([pkg]) => isPackageOrSubpath(source, pkg))?.[1];
198
+ const isTypeOnly = (node) => node.importKind === "type" || node.specifiers.length > 0 && node.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type");
199
+ /**
200
+ * Whether an import declaration registers end-to-end tests: it takes the
201
+ * runner entry's `test` (an alias counts) or its namespace as a value. The
202
+ * rest of the entry (`expect`, `describe`, hooks, the composition API, types)
203
+ * registers no test, so importing it selects no lane.
204
+ */
205
+ const importsE2eTest = (node) => node.source.value === "@systemfsoftware/effect-e2e/runner" && node.specifiers.some((specifier) => specifier.type === "ImportNamespaceSpecifier" || specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && specifier.imported.type === "Identifier" && specifier.imported.name === "test");
206
+ /**
207
+ * Where a value import brings FastCheck in: the whole declaration for the
208
+ * `fast-check` module (or a subpath), else each `FastCheck` named value import.
209
+ */
210
+ const fastCheckImportSites = (node) => isTypeOnly(node) ? [] : isPackageOrSubpath(node.source.value, "fast-check") ? [node] : node.specifiers.filter((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && specifier.imported.type === "Identifier" && specifier.imported.name === "FastCheck");
211
+ const isNode$5 = (value) => typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
212
+ const childrenOf = (node, keys) => (keys[node.type] ?? []).flatMap((key) => {
213
+ const value = Reflect.get(node, key);
214
+ if (Array.isArray(value)) return value.filter(isNode$5);
215
+ return isNode$5(value) ? [value] : [];
216
+ });
217
+ const callsProperty = (program, keys) => {
218
+ const pending = [program];
219
+ for (let node = pending.pop(); node !== void 0; node = pending.pop()) {
220
+ if (node.type === "CallExpression" && isPropCallee$1(node.callee)) return true;
221
+ pending.push(...childrenOf(node, keys));
222
+ }
223
+ return false;
224
+ };
225
+ const derive = (program, keys) => {
226
+ const lanes = /* @__PURE__ */ new Set();
227
+ for (const statement of program.body) {
228
+ if (statement.type !== "ImportDeclaration" || isTypeOnly(statement)) continue;
229
+ const lane = harnessLaneOf(statement.source.value);
230
+ if (lane !== void 0) lanes.add(lane);
231
+ if (importsE2eTest(statement)) lanes.add("e2e");
232
+ if (fastCheckImportSites(statement).length > 0) lanes.add("property");
233
+ }
234
+ if (!lanes.has("property") && callsProperty(program, keys)) lanes.add("property");
235
+ return lanes;
236
+ };
237
+ const NO_LANES = /* @__PURE__ */ new Set();
238
+ const cache = /* @__PURE__ */ new WeakMap();
239
+ /**
240
+ * The lanes the linted test file is in, derived once per program and shared by
241
+ * every rule. A file is in a lane when Vitest runs it as a test (the `.test.ts` /
242
+ * `.spec.ts` ending) or the `effect-e2e` runner collects it (`isE2eTestFile`);
243
+ * a fixture or helper a test imports is not a test.
244
+ * Call it from a visitor: the AST is complete once traversal starts.
245
+ */
246
+ const lanesOf = (context) => {
247
+ if (!isTestFile(basenameOf(context.filename)) && !isE2eTestFile(context.filename)) return NO_LANES;
248
+ const program = context.sourceCode.ast;
249
+ const cached = cache.get(program);
250
+ if (cached !== void 0) return cached;
251
+ const lanes = derive(program, context.sourceCode.visitorKeys);
252
+ cache.set(program, lanes);
253
+ return lanes;
254
+ };
255
+ /**
256
+ * The property lane holds a file only when no differential harness does: the
257
+ * differential requirements already include the property ones, and a
258
+ * differential file imports FastCheck for its arbitraries.
259
+ */
260
+ const isPropertyFile = (lanes) => lanes.has("property") && !lanes.has("differential");
261
+ /**
262
+ * A property test beside Gherkin, conformance or trace scenarios: the property
263
+ * belongs in its own workflow property file, never mixed into a harness test.
264
+ */
265
+ const mixesPropertyIntoAHarness = (lanes) => isPropertyFile(lanes) && (lanes.has("behaviour") || lanes.has("conformance") || lanes.has("trace"));
266
+ /**
267
+ * What a package is, declared once in its own lint config's `settings`:
268
+ * `vitest-runner` for the packages that are the test framework itself, `e2e`
269
+ * for the packages that hold end-to-end suites. Every rule reads the same
270
+ * declaration, so two rules can never disagree about it, and no rule takes a
271
+ * role option (oxlint refuses one as a configuration error). The rule cannot
272
+ * see how a package is wired, so it takes the declaration at its word; a
273
+ * package that declares a role without being what it names gets that role's
274
+ * treatment all the same, and the declaration is reviewed where it is made, in
275
+ * the config diff.
276
+ */
277
+ const SETTINGS_KEY = "@systemfsoftware/oxlint-plugin-test-discipline";
278
+ const Role = Schema.Literals(["vitest-runner", "e2e"]);
279
+ const PluginSettings = Schema.Struct({ [SETTINGS_KEY]: Schema.optional(Schema.Struct({ role: Schema.optional(Role) })) });
280
+ const declaresRole = (context, role) => Option.exists(Schema.decodeUnknownOption(PluginSettings)(context.settings), (settings) => settings[SETTINGS_KEY]?.role === role);
281
+ /**
282
+ * Whether the linted file's package declares itself the Vitest runner. Settings
283
+ * that do not decode declare no role, so every raw Vitest import is then refused.
284
+ */
285
+ const isRunnerPackage = (context) => declaresRole(context, "vitest-runner");
286
+ /**
287
+ * Whether the linted file's package declares itself an end-to-end package.
288
+ * Settings that do not decode declare no role.
289
+ */
290
+ const isE2ePackage = (context) => declaresRole(context, "e2e");
291
+ const importsARunner = (program) => program.body.some((statement) => statement.type === "ImportDeclaration" && !isTypeOnly(statement) && Object.keys(FOREIGN_RUNNERS).some((runner) => isPackageOrSubpath(statement.source.value, runner)));
292
+ /**
293
+ * The runner lane: a test in a package that declares the `vitest-runner` role
294
+ * and imports a Vitest runner module (`vitest`, `@effect/vitest`, the fork, or a
295
+ * subpath of one). Outside such a package the same imports select no lane.
296
+ */
297
+ const isInRunnerLane = (context) => isTestFile(basenameOf(context.filename)) && isRunnerPackage(context) && importsARunner(context.sourceCode.ast);
298
+ //#endregion
160
299
  //#region src/rules/behaviour-exercises-use-case.ts
161
300
  const EFFECT_PACKAGE = "effect";
162
301
  const FOUNDATION_PACKAGES = /* @__PURE__ */ new Set([
@@ -184,7 +323,7 @@ const isFoundationImport = (source, filename) => {
184
323
  return FOUNDATION_PACKAGES.has(source) || source.startsWith(`${EFFECT_PACKAGE}/`) || source.startsWith("node:");
185
324
  };
186
325
  /** The stem of a path with its final extension stripped, for identity comparisons. */
187
- const stemOf = (file) => file.replace(/\.[^/]+$/, "");
326
+ const stemOf = (file) => file.replace(/\.[^./]+$/, "");
188
327
  /**
189
328
  * A relative module specifier resolved against the linted file's directory,
190
329
  * with `..` segments collapsed. Existence is never checked: one file cannot
@@ -244,7 +383,7 @@ const DIST_SEGMENT = /(?:^|\/)dist\//;
244
383
  * rule the same way a real one does.
245
384
  */
246
385
  const behaviourExercisesUseCase = defineRule({
247
- meta: meta$27,
386
+ meta: meta$28,
248
387
  create(context) {
249
388
  let reached = false;
250
389
  return {
@@ -259,7 +398,7 @@ const behaviourExercisesUseCase = defineRule({
259
398
  if (typeof node.value === "string" && DIST_SEGMENT.test(node.value)) reached = true;
260
399
  },
261
400
  "Program:exit"(node) {
262
- if (!isBehaviourBasename(basenameOf(context.filename))) return;
401
+ if (!lanesOf(context).has("behaviour")) return;
263
402
  if (reached) return;
264
403
  for (const statement of node.body) {
265
404
  if (statement.type !== "ImportDeclaration") continue;
@@ -286,21 +425,21 @@ const behaviourExercisesUseCase = defineRule({
286
425
  });
287
426
  //#endregion
288
427
  //#region src/rules/behaviour-one-feature-per-file.config.ts
289
- const TOO_FEW_FEATURES_NAME = "a *.integration.test.ts that constructs no Feature";
428
+ const TOO_FEW_FEATURES_NAME = "a behaviour test that constructs no Feature";
290
429
  const TOO_FEW_FEATURES_EXPECTED = "exactly one Feature(...) — the capability the file proves";
291
- const TOO_FEW_FEATURES_ACTUAL = "a behaviour file with zero Feature(...) calls";
430
+ const TOO_FEW_FEATURES_ACTUAL = "a test importing @systemfsoftware/effect-gherkin-spec with zero Feature(...) calls";
292
431
  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.";
293
- const TOO_MANY_FEATURES_NAME = "a *.integration.test.ts accumulating multiple Feature(...) calls";
432
+ const TOO_MANY_FEATURES_NAME = "a behaviour test accumulating multiple Feature(...) calls";
294
433
  const TOO_MANY_FEATURES_EXPECTED = "exactly one Feature(...) — every additional one signals a junk drawer";
295
- const TOO_MANY_FEATURES_ACTUAL = "a behaviour file with two or more Feature(...) calls";
434
+ const TOO_MANY_FEATURES_ACTUAL = "a test importing @systemfsoftware/effect-gherkin-spec with two or more Feature(...) calls";
296
435
  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.";
297
- const meta$26 = {
436
+ const meta$27 = {
298
437
  type: "problem",
299
- 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." },
438
+ docs: { description: "A behaviour test - any test file that imports @systemfsoftware/effect-gherkin-spec, whatever its name - 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." },
300
439
  schema: [],
301
440
  messages: {
302
- tooFewFeatures: MESSAGE$8,
303
- tooManyFeatures: MESSAGE$8
441
+ tooFewFeatures: MESSAGE$6,
442
+ tooManyFeatures: MESSAGE$6
304
443
  }
305
444
  };
306
445
  //#endregion
@@ -338,10 +477,10 @@ const hasAnyFeatureCall = (program) => {
338
477
  return false;
339
478
  };
340
479
  const behaviourOneFeaturePerFile = defineRule({
341
- meta: meta$26,
480
+ meta: meta$27,
342
481
  create(context) {
343
482
  return { "Program:exit"(node) {
344
- if (!isBehaviourBasename(basenameOf(context.filename))) return;
483
+ if (!lanesOf(context).has("behaviour")) return;
345
484
  const excess = findSecondFeatureCall(node);
346
485
  if (excess !== null) {
347
486
  const total = countFeatureCalls(node);
@@ -372,71 +511,26 @@ const behaviourOneFeaturePerFile = defineRule({
372
511
  });
373
512
  //#endregion
374
513
  //#region src/rules/behaviour-test-requires-gherkin.config.ts
375
- const FOREIGN_RUNNER_EXPECTED = "it and layer imported from @systemfsoftware/effect-gherkin-spec";
376
- const FOREIGN_RUNNER_ACTUAL = "a test runner imported directly from vitest, @effect/vitest or @systemfsoftware/vitest in a behaviour file";
377
- const FOREIGN_RUNNER_FIX = "import { it } from @systemfsoftware/effect-gherkin-spec and build the suite with makeFeature({ it })";
378
- const MISSING_MAKE_FEATURE_NAME = "a *.integration.test.ts without makeFeature";
514
+ const MISSING_MAKE_FEATURE_NAME = "a behaviour test without makeFeature";
379
515
  const MISSING_MAKE_FEATURE_EXPECTED = "makeFeature imported from @systemfsoftware/effect-gherkin-spec";
380
- const MISSING_MAKE_FEATURE_ACTUAL = "a behaviour file that never constructs a Gherkin feature";
516
+ const MISSING_MAKE_FEATURE_ACTUAL = "a test that imports @systemfsoftware/effect-gherkin-spec but never constructs a Gherkin feature";
381
517
  const MISSING_MAKE_FEATURE_FIX = "import { makeFeature } from @systemfsoftware/effect-gherkin-spec and declare `const Feature = makeFeature({ it, layer })`";
382
- const meta$25 = {
518
+ const meta$26 = {
383
519
  type: "problem",
384
- 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, @effect/vitest or @systemfsoftware/vitest." },
520
+ docs: { description: "A test that imports @systemfsoftware/effect-gherkin-spec (the behaviour lane) drives its suite through makeFeature from that package." },
385
521
  schema: [],
386
- messages: {
387
- foreignRunner: MESSAGE$8,
388
- missingMakeFeature: MESSAGE$8
389
- }
522
+ messages: { missingMakeFeature: MESSAGE$6 }
390
523
  };
391
524
  //#endregion
392
525
  //#region src/rules/behaviour-test-requires-gherkin.ts
393
- const ImportedIdentifier = Schema.Struct({ name: Schema.String });
394
- const isMakeFeatureSpecifier = (specifier) => {
395
- const imported = specifier.imported;
396
- return imported.type === "Identifier" && imported.name === "makeFeature";
397
- };
398
- /**
399
- * `null` for any specifier that cannot name a runner. The decode is unreachable
400
- * for a string-literal import name because the narrowing above rejects it first;
401
- * it exists so removing that narrowing fails loudly instead of silently.
402
- */
403
- const foreignRunnerNameOf = (specifier) => {
404
- if (specifier.imported.type !== "Identifier") return null;
405
- const { name } = Schema.decodeSync(ImportedIdentifier)(specifier.imported);
406
- return RUNNER_NAMES.has(name) ? name : null;
407
- };
526
+ const importsMakeFeature = (statement) => harnessLaneOf(statement.source.value) === "behaviour" && statement.specifiers.some((specifier) => specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "makeFeature");
408
527
  const behaviourTestRequiresGherkin = defineRule({
409
- meta: meta$25,
528
+ meta: meta$26,
410
529
  create(context) {
411
- const basename = basenameOf(context.filename);
412
530
  return { Program(node) {
413
- if (!isBehaviourBasename(basename)) return;
414
- let hasMakeFeature = false;
415
- for (const statement of node.body) {
416
- if (statement.type !== "ImportDeclaration") continue;
417
- const sourceValue = statement.source.value;
418
- if (sourceValue === "@systemfsoftware/effect-gherkin-spec") for (const specifier of statement.specifiers) {
419
- if (specifier.type !== "ImportSpecifier") continue;
420
- if (isMakeFeatureSpecifier(specifier)) hasMakeFeature = true;
421
- }
422
- if (FOREIGN_RUNNERS[sourceValue] !== true) continue;
423
- for (const specifier of statement.specifiers) {
424
- if (specifier.type !== "ImportSpecifier") continue;
425
- const runnerName = foreignRunnerNameOf(specifier);
426
- if (runnerName === null) continue;
427
- context.report({
428
- node: specifier,
429
- messageId: "foreignRunner",
430
- data: {
431
- name: runnerName,
432
- expected: FOREIGN_RUNNER_EXPECTED,
433
- actual: FOREIGN_RUNNER_ACTUAL,
434
- fix: FOREIGN_RUNNER_FIX
435
- }
436
- });
437
- }
438
- }
439
- if (!hasMakeFeature) context.report({
531
+ if (!lanesOf(context).has("behaviour")) return;
532
+ if (node.body.some((statement) => statement.type === "ImportDeclaration" && importsMakeFeature(statement))) return;
533
+ context.report({
440
534
  node,
441
535
  messageId: "missingMakeFeature",
442
536
  data: {
@@ -455,33 +549,25 @@ const HARNESS_MEMBERS = {
455
549
  released: true
456
550
  };
457
551
  const LEGACY_CHECK_MEMBER = "check";
458
- const HARNESS_PRESCRIPTION$2 = "import { Conformance } from @systemfsoftware/conformance-spec and express the test as Conformance.linearizable(implementation, { commands, model, run, fibers, operations }) in the scenario body";
459
- const meta$24 = {
552
+ const HARNESS_PRESCRIPTION$1 = "import { Conformance } from @systemfsoftware/conformance-spec and express the test as Conformance.linearizable(implementation, { commands, model, run, fibers, operations }) in the scenario body";
553
+ const meta$25 = {
460
554
  type: "problem",
461
- docs: { description: "Conformance tests must use the @systemfsoftware/conformance-spec Conformance barrel: Conformance.linearizable, Conformance.sequential, or Conformance.released. Raw runner calls (it, test, describe, and member forms like it.effect) and direct runner imports are forbidden in *.conformance.test.ts files; the retired Linearizable.check / SequentialModel.check / Released.check shapes are rejected, and importing Conformance without invoking a check is equally non-compliant." },
555
+ docs: { description: "Conformance tests must use the @systemfsoftware/conformance-spec Conformance barrel: Conformance.linearizable, Conformance.sequential, or Conformance.released. A test that imports @systemfsoftware/conformance-spec is in the conformance lane. Runner imports and calls in this lane are vitest-from-systemfsoftware-vitest findings. The retired Linearizable.check / SequentialModel.check / Released.check shapes are rejected, and importing Conformance without invoking a check is equally non-compliant." },
462
556
  schema: [],
463
557
  messages: {
464
- rawRunnerCall: MESSAGE$8,
465
- runnerImport: MESSAGE$8,
466
- missingHarnessImport: MESSAGE$8,
467
- legacyHarnessCall: MESSAGE$8,
468
- missingHarnessUsage: MESSAGE$8
558
+ legacyHarnessCall: MESSAGE$6,
559
+ missingHarnessUsage: MESSAGE$6
469
560
  }
470
561
  };
471
562
  //#endregion
472
563
  //#region src/rules/conformance-test-requires-harness.ts
473
- const memberObjectName$1 = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
564
+ const memberObjectName$2 = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
474
565
  const memberPropertyName = (callee) => callee.type === "MemberExpression" && callee.property.type === "Identifier" ? callee.property.name : void 0;
475
566
  const memberName = (callee) => callee.type === "Identifier" ? callee.name : memberPropertyName(callee);
476
- const calleeName$1 = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName$1(callee);
477
- const isRawRunnerCall = (callee) => {
478
- const name = calleeName$1(callee);
479
- return name !== void 0 && RUNNER_NAMES.has(name);
480
- };
481
- const isForeignRunnerImport$1 = (node) => typeof node.source.value === "string" && FOREIGN_RUNNERS[node.source.value] === true;
567
+ const calleeName$2 = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName$2(callee);
482
568
  const namesConformanceExport = (specifier) => specifier.imported.type === "Identifier" && specifier.imported.name === "Conformance";
483
569
  const recordBindings = (node, packageBindings, conformanceBindings) => {
484
- if (node.source.value !== "@systemfsoftware/conformance-spec") return;
570
+ if (harnessLaneOf(node.source.value) !== "conformance") return;
485
571
  for (const specifier of node.specifiers) {
486
572
  packageBindings.add(specifier.local.name);
487
573
  if (specifier.type === "ImportSpecifier" && namesConformanceExport(specifier)) conformanceBindings.add(specifier.local.name);
@@ -489,65 +575,40 @@ const recordBindings = (node, packageBindings, conformanceBindings) => {
489
575
  };
490
576
  const isHarnessInvocation = (callee, conformanceBindings) => {
491
577
  const member = memberName(callee);
492
- const root = calleeName$1(callee);
578
+ const root = calleeName$2(callee);
493
579
  return member !== void 0 && HARNESS_MEMBERS[member] === true && root !== void 0 && conformanceBindings.has(root);
494
580
  };
495
581
  const isLegacyCheck = (callee, packageBindings) => {
496
- const root = calleeName$1(callee);
582
+ const root = calleeName$2(callee);
497
583
  return memberName(callee) === "check" && root !== void 0 && packageBindings.has(root);
498
584
  };
499
- const legacyCallText = (callee) => callee.type === "Identifier" ? callee.name : `${calleeName$1(callee)}.${LEGACY_CHECK_MEMBER}`;
585
+ const legacyCallText = (callee) => callee.type === "Identifier" ? callee.name : `${calleeName$2(callee)}.${LEGACY_CHECK_MEMBER}`;
500
586
  const legacyCheckError = (callee) => ({
501
587
  messageId: "legacyHarnessCall",
502
588
  data: {
503
589
  name: `a .${LEGACY_CHECK_MEMBER}(...) call in a conformance test file`,
504
- expected: HARNESS_PRESCRIPTION$2,
590
+ expected: HARNESS_PRESCRIPTION$1,
505
591
  actual: `${legacyCallText(callee)} is not a check the barrel exposes`,
506
- fix: HARNESS_PRESCRIPTION$2
592
+ fix: HARNESS_PRESCRIPTION$1
507
593
  }
508
594
  });
509
595
  const conformanceTestRequiresHarness = defineRule({
510
- meta: meta$24,
596
+ meta: meta$25,
511
597
  create(context) {
512
- if (!context.filename.endsWith(".conformance.test.ts")) return {};
513
598
  const packageBindings = /* @__PURE__ */ new Set();
514
599
  const conformanceBindings = /* @__PURE__ */ new Set();
600
+ let conformance = false;
515
601
  let violations = 0;
516
602
  let hasHarnessInvocation = false;
517
603
  return {
604
+ Program() {
605
+ conformance = lanesOf(context).has("conformance");
606
+ },
518
607
  ImportDeclaration(node) {
519
- if (isForeignRunnerImport$1(node)) {
520
- context.report({
521
- node,
522
- messageId: "runnerImport",
523
- data: {
524
- name: `runner import from ${String(node.source.value)} in a conformance test file`,
525
- expected: HARNESS_PRESCRIPTION$2,
526
- actual: "a direct vitest / @effect/vitest / @systemfsoftware/vitest runner import bypasses the conformance check",
527
- fix: `delete the runner import; ${HARNESS_PRESCRIPTION$2}`
528
- }
529
- });
530
- violations += 1;
531
- return;
532
- }
533
- recordBindings(node, packageBindings, conformanceBindings);
608
+ if (conformance) recordBindings(node, packageBindings, conformanceBindings);
534
609
  },
535
610
  CallExpression(node) {
536
- if (isRawRunnerCall(node.callee)) {
537
- const name = calleeName$1(node.callee) ?? "";
538
- context.report({
539
- node,
540
- messageId: "rawRunnerCall",
541
- data: {
542
- name: `raw runner call (${name}) in a conformance test file`,
543
- expected: HARNESS_PRESCRIPTION$2,
544
- actual: `${name}(...) bypasses the conformance check`,
545
- fix: `rewrite using ${HARNESS_PRESCRIPTION$2}`
546
- }
547
- });
548
- violations += 1;
549
- return;
550
- }
611
+ if (!conformance) return;
551
612
  if (isHarnessInvocation(node.callee, conformanceBindings)) {
552
613
  hasHarnessInvocation = true;
553
614
  return;
@@ -561,27 +622,14 @@ const conformanceTestRequiresHarness = defineRule({
561
622
  }
562
623
  },
563
624
  "Program:exit"(node) {
564
- if (packageBindings.size === 0) {
565
- context.report({
566
- node,
567
- messageId: "missingHarnessImport",
568
- data: {
569
- name: `conformance test file without the ${CONFORMANCE_PACKAGE} import`,
570
- expected: HARNESS_PRESCRIPTION$2,
571
- actual: "no conformance check import found",
572
- fix: HARNESS_PRESCRIPTION$2
573
- }
574
- });
575
- return;
576
- }
577
- if (!hasHarnessInvocation && violations === 0) context.report({
625
+ if (conformance && !hasHarnessInvocation && violations === 0) context.report({
578
626
  node,
579
627
  messageId: "missingHarnessUsage",
580
628
  data: {
581
629
  name: `conformance test file imports ${CONFORMANCE_PACKAGE} but never invokes it`,
582
- expected: HARNESS_PRESCRIPTION$2,
630
+ expected: HARNESS_PRESCRIPTION$1,
583
631
  actual: "no Conformance.linearizable, Conformance.sequential, or Conformance.released call runs",
584
- fix: HARNESS_PRESCRIPTION$2
632
+ fix: HARNESS_PRESCRIPTION$1
585
633
  }
586
634
  });
587
635
  }
@@ -608,7 +656,7 @@ const INVALID_BEHAVIOR_CASE_FIX = "Convert behavior to PascalCase (e.g., throwEr
608
656
  const INVALID_CONDITION_CASE_FIX = "Convert condition to PascalCase (e.g., passwordInvalid → PasswordInvalid)";
609
657
  const EMPTY_BEHAVIOR_ACTUAL = "Empty string between Should_ and _When_";
610
658
  const EMPTY_CONDITION_ACTUAL = "Empty string after _When_";
611
- const meta$23 = {
659
+ const meta$24 = {
612
660
  type: "suggestion",
613
661
  docs: { description: "Enforce DAMP (Descriptive and Meaningful Phrases) test naming format: Should_[ExpectedBehavior]_When_[Condition]" },
614
662
  schema: [],
@@ -688,7 +736,7 @@ const isTestFunctionCall = (node) => {
688
736
  return false;
689
737
  };
690
738
  const dampTestNaming = defineRule({
691
- meta: meta$23,
739
+ meta: meta$24,
692
740
  create(context) {
693
741
  if (TYPE_TEST_BASENAME.test(basenameOf(context.filename))) return {};
694
742
  return { CallExpression(node) {
@@ -712,92 +760,70 @@ const dampTestNaming = defineRule({
712
760
  });
713
761
  //#endregion
714
762
  //#region src/rules/differential-test-requires-harness.config.ts
715
- const meta$22 = {
763
+ const HARNESS_PRESCRIPTION = "import { Differential, Metamorphic } from @systemfsoftware/differential-spec and express the test as Differential.compare({ name, reference, candidate }).on(arb).assert(oracle) or Metamorphic.on({ name, system }).relation({ transformInput, assertOutput }).on(arb)";
764
+ const RAW_FAST_CHECK_EXPECTED$1 = "the harness owns fast-check — pass an fc.Arbitrary to the harness, never call fc.assert/fc.check directly";
765
+ const RAW_FAST_CHECK_ACTUAL$1 = "bypasses the differential harness oracle";
766
+ const RAW_FAST_CHECK_FIX$1 = "build the arbitrary in tests/__fixtures__ and hand it to Differential.compare(...).on(arb) or Metamorphic.on(...).relation(...).on(arb)";
767
+ const meta$23 = {
716
768
  type: "problem",
717
- docs: { description: "Differential tests must use @systemfsoftware/differential-spec harness. Raw runner calls (it, test, describe, and member forms like it.effect) and direct runner imports are forbidden in *.differential.test.ts files; importing the harness without invoking it is equally non-compliant." },
769
+ docs: { description: "A test that imports @systemfsoftware/differential-spec (the differential lane) runs through that harness: raw fc.assert/fc.check/fc.property/fc.asyncProperty calls are forbidden in it, and importing the harness without invoking it is non-compliant. Runner imports and calls in this lane are vitest-from-systemfsoftware-vitest findings." },
718
770
  schema: [],
719
771
  messages: {
720
- rawRunnerCall: "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.",
721
- runnerImport: "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.",
722
- missingHarnessImport: "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.",
723
- missingHarnessUsage: "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}."
772
+ rawFastCheck: MESSAGE$6,
773
+ missingHarnessUsage: MESSAGE$6
724
774
  }
725
775
  };
726
776
  //#endregion
727
777
  //#region src/rules/differential-test-requires-harness.ts
728
- const HARNESS_PRESCRIPTION$1 = "import { Differential, Metamorphic } from @systemfsoftware/differential-spec and express the test as Differential.compare({ name, reference, candidate }).on(arb).assert(oracle) or Metamorphic.on({ name, system }).relation({ transformInput, assertOutput }).on(arb)";
729
- const memberObjectName = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
730
- const calleeName = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName(callee);
778
+ const memberObjectName$1 = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
779
+ const calleeName$1 = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName$1(callee);
731
780
  const recordHarnessBindings = (node, bindings) => {
732
- if (node.source.value !== "@systemfsoftware/differential-spec") return;
781
+ if (harnessLaneOf(node.source.value) !== "differential") return;
733
782
  for (const specifier of node.specifiers) bindings.add(specifier.local.name);
734
783
  };
735
- const isForeignRunnerImport = (node) => typeof node.source.value === "string" && FOREIGN_RUNNERS[node.source.value] === true;
784
+ /** The fast-check runner a raw `fc.assert(...)` style call names, if any. */
785
+ const rawFastCheckMethod = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "fc" && callee.property.type === "Identifier" && RAW_FC_METHODS[callee.property.name] === true ? callee.property.name : void 0;
736
786
  const differentialTestRequiresHarness = defineRule({
737
- meta: meta$22,
787
+ meta: meta$23,
738
788
  create(context) {
739
- if (!context.filename.endsWith(".differential.test.ts")) return {};
740
789
  const harnessBindings = /* @__PURE__ */ new Set();
741
- let violations = 0;
790
+ let differential = false;
742
791
  let hasHarnessInvocation = false;
743
792
  return {
793
+ Program() {
794
+ differential = lanesOf(context).has("differential");
795
+ },
744
796
  ImportDeclaration(node) {
745
- if (isForeignRunnerImport(node)) {
746
- context.report({
747
- node,
748
- messageId: "runnerImport",
749
- data: {
750
- name: `runner import from ${String(node.source.value)} in a differential test file`,
751
- expected: HARNESS_PRESCRIPTION$1,
752
- actual: "a direct vitest / @effect/vitest / @systemfsoftware/vitest runner import bypasses the differential oracle",
753
- fix: `delete the runner import; ${HARNESS_PRESCRIPTION$1}`
754
- }
755
- });
756
- violations += 1;
757
- return;
758
- }
759
- recordHarnessBindings(node, harnessBindings);
797
+ if (differential) recordHarnessBindings(node, harnessBindings);
760
798
  },
761
799
  CallExpression(node) {
762
- const name = calleeName(node.callee);
763
- if (name !== void 0 && RUNNER_NAMES.has(name)) {
800
+ if (!differential) return;
801
+ const method = rawFastCheckMethod(node.callee);
802
+ if (method !== void 0) {
764
803
  context.report({
765
804
  node,
766
- messageId: "rawRunnerCall",
805
+ messageId: "rawFastCheck",
767
806
  data: {
768
- name: `raw runner call (${name}) in a differential test file`,
769
- expected: HARNESS_PRESCRIPTION$1,
770
- actual: `${name}(...) bypasses the differential oracle`,
771
- fix: `rewrite using ${HARNESS_PRESCRIPTION$1}`
807
+ name: `raw fc.${method}(...) in a differential test file`,
808
+ expected: RAW_FAST_CHECK_EXPECTED$1,
809
+ actual: `fc.${method}(...) ${RAW_FAST_CHECK_ACTUAL$1}`,
810
+ fix: RAW_FAST_CHECK_FIX$1
772
811
  }
773
812
  });
774
- violations += 1;
775
813
  return;
776
814
  }
815
+ const name = calleeName$1(node.callee);
777
816
  if (name !== void 0 && harnessBindings.has(name)) hasHarnessInvocation = true;
778
817
  },
779
818
  "Program:exit"(node) {
780
- if (harnessBindings.size === 0) {
781
- context.report({
782
- node,
783
- messageId: "missingHarnessImport",
784
- data: {
785
- name: `differential test file without ${DIFFERENTIAL_PACKAGE} import`,
786
- expected: HARNESS_PRESCRIPTION$1,
787
- actual: "no differential harness import found",
788
- fix: HARNESS_PRESCRIPTION$1
789
- }
790
- });
791
- return;
792
- }
793
- if (!hasHarnessInvocation && violations === 0) context.report({
819
+ if (differential && !hasHarnessInvocation) context.report({
794
820
  node,
795
821
  messageId: "missingHarnessUsage",
796
822
  data: {
797
823
  name: `differential test file imports ${DIFFERENTIAL_PACKAGE} but never invokes it`,
798
- expected: HARNESS_PRESCRIPTION$1,
824
+ expected: HARNESS_PRESCRIPTION,
799
825
  actual: "the harness is imported but no Differential.compare or Metamorphic.on chain runs",
800
- fix: HARNESS_PRESCRIPTION$1
826
+ fix: HARNESS_PRESCRIPTION
801
827
  }
802
828
  });
803
829
  }
@@ -805,16 +831,64 @@ const differentialTestRequiresHarness = defineRule({
805
831
  }
806
832
  });
807
833
  //#endregion
834
+ //#region src/rules/e2e-tier-placement.config.ts
835
+ const OUTSIDE_PACKAGE_EXPECTED = "end-to-end tests only in a package that declares the e2e role in its lint settings";
836
+ const OUTSIDE_PACKAGE_ACTUAL = "a test registered with the effect-e2e runner in a package that declares no e2e role";
837
+ const OUTSIDE_PACKAGE_FIX = "move the suite into an end-to-end package; if this package is one, declare the e2e role under the plugin key in its lint config settings";
838
+ const INTEGRATION_IN_PACKAGE_EXPECTED = "only end-to-end tests in a package that declares the e2e role";
839
+ const INTEGRATION_IN_PACKAGE_ACTUAL = "an integration test (Gherkin harness) in an end-to-end package";
840
+ const INTEGRATION_IN_PACKAGE_FIX = "move the integration test into the package it covers, or write it with the runner test so it is an end-to-end test";
841
+ //#endregion
842
+ //#region src/rules/e2e-tier-placement.ts
843
+ const e2eTierPlacement = defineRule({
844
+ meta: {
845
+ type: "problem",
846
+ docs: { description: "Reports an end-to-end test (a test file that value-imports `test`, or the namespace, from `@systemfsoftware/effect-e2e/runner`, whatever its name or location) in a package whose lint settings do not declare the `e2e` role; in a package that declares it, reports an integration test (a file importing the Gherkin harness and not in the e2e lane). The role is declared once in the package's own lint config settings under the plugin's key, and no rule takes a role option." },
847
+ schema: [],
848
+ messages: {
849
+ e2eOutsideE2ePackage: MESSAGE$6,
850
+ integrationInE2ePackage: MESSAGE$6
851
+ }
852
+ },
853
+ create(context) {
854
+ return { Program(node) {
855
+ const lanes = lanesOf(context);
856
+ const name = basenameOf(context.filename);
857
+ const e2ePackage = isE2ePackage(context);
858
+ if (lanes.has("e2e") && !e2ePackage) context.report({
859
+ node,
860
+ messageId: "e2eOutsideE2ePackage",
861
+ data: {
862
+ name,
863
+ expected: OUTSIDE_PACKAGE_EXPECTED,
864
+ actual: OUTSIDE_PACKAGE_ACTUAL,
865
+ fix: OUTSIDE_PACKAGE_FIX
866
+ }
867
+ });
868
+ if (lanes.has("behaviour") && !lanes.has("e2e") && e2ePackage) context.report({
869
+ node,
870
+ messageId: "integrationInE2ePackage",
871
+ data: {
872
+ name,
873
+ expected: INTEGRATION_IN_PACKAGE_EXPECTED,
874
+ actual: INTEGRATION_IN_PACKAGE_ACTUAL,
875
+ fix: INTEGRATION_IN_PACKAGE_FIX
876
+ }
877
+ });
878
+ } };
879
+ }
880
+ });
881
+ //#endregion
808
882
  //#region src/rules/in-source-test-prop-only.config.ts
809
883
  const NON_PROP_CALL_NAME = "a non-property test call inside an `import.meta.vitest` block";
810
884
  const NON_PROP_CALL_EXPECTED = "only `it.prop` or `it.effect.prop` member-chain calls (standard modifiers included) with boolean predicates";
811
885
  const NON_PROP_CALL_ACTUAL = "a bare or member-chain test call other than `it.prop`/`it.effect.prop`";
812
- 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";
886
+ 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 a Gherkin behaviour test under tests/, or rewrite a real invariant as `it.prop` over a schema-derived arbitrary";
813
887
  const meta$21 = {
814
888
  type: "problem",
815
889
  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." },
816
890
  schema: [],
817
- messages: { nonPropCall: MESSAGE$8 }
891
+ messages: { nonPropCall: MESSAGE$6 }
818
892
  };
819
893
  //#endregion
820
894
  //#region src/rules/vitest-guard.ts
@@ -841,19 +915,19 @@ const isInsideConsequent = (node, consequent) => {
841
915
  };
842
916
  //#endregion
843
917
  //#region src/rules/in-source-test-prop-only.ts
844
- const PROP_MODIFIERS$1 = /* @__PURE__ */ new Set([
918
+ const PROP_MODIFIERS = /* @__PURE__ */ new Set([
845
919
  "only",
846
920
  "skip",
847
921
  "todo"
848
922
  ]);
849
- const isPropCallee$1 = (callee) => {
923
+ const isPropCallee = (callee) => {
850
924
  if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return false;
851
925
  if (callee.property.name === "prop") {
852
926
  const object = callee.object;
853
927
  if (object.type === "Identifier") return object.name === "it";
854
928
  return object.type === "MemberExpression" && object.property.type === "Identifier" && object.property.name === "effect" && object.object.type === "Identifier" && object.object.name === "it";
855
929
  }
856
- return PROP_MODIFIERS$1.has(callee.property.name) && isPropCallee$1(callee.object);
930
+ return PROP_MODIFIERS.has(callee.property.name) && isPropCallee(callee.object);
857
931
  };
858
932
  const BANNED_TEST_ROOTS = /* @__PURE__ */ new Set([
859
933
  "it",
@@ -888,7 +962,7 @@ const inSourceTestPropOnly = defineRule({
888
962
  },
889
963
  CallExpression(node) {
890
964
  if (guards.length === 0) return;
891
- if (isPropCallee$1(node.callee)) return;
965
+ if (isPropCallee(node.callee)) return;
892
966
  const root = rootNameOf(node.callee);
893
967
  if (root === void 0 || !BANNED_TEST_ROOTS.has(root)) return;
894
968
  if (guards.find((g) => isInsideConsequent(node, g.consequent)) === void 0) return;
@@ -918,14 +992,14 @@ const NOT_MODULE_LEVEL_FIX = "move the block to module level — a nested block
918
992
  const NO_PRIVATE_TARGET_NAME = "an `import.meta.vitest` block touching no module-level binding";
919
993
  const NO_PRIVATE_TARGET_EXPECTED = "an in-source test exercising this module's own code — a private helper, or an exported operation named in the `it.prop` `subject` slot";
920
994
  const NO_PRIVATE_TARGET_ACTUAL = "an in-source block referencing only exported-call or imported names";
921
- const NO_PRIVATE_TARGET_FIX = "touch this module. A private helper is referenced directly; an exported operation is named in the `it.prop` `subject` slot beside its declaration — that is the one home for its laws and the file whose mutants they run. What the block cannot hold is a behaviour binding no module-level name: test the public surface from tests/ as *.integration.test.ts instead";
995
+ const NO_PRIVATE_TARGET_FIX = "touch this module. A private helper is referenced directly; an exported operation is named in the `it.prop` `subject` slot beside its declaration — that is the one home for its laws and the file whose mutants they run. What the block cannot hold is a behaviour binding no module-level name: test the public surface from tests/ as a Gherkin behaviour test instead";
922
996
  const meta$20 = {
923
997
  type: "problem",
924
- docs: { description: "In-source `if (import.meta.vitest)` blocks under src/ must be at module level and exercise this module's own code — a private helper, or an exported operation named in the `it.prop` `subject` slot; a public-surface test belongs in tests/ as an integration test." },
998
+ docs: { description: "In-source `if (import.meta.vitest)` blocks under src/ must be at module level and exercise this module's own code — a private helper, or an exported operation named in the `it.prop` `subject` slot; a public-surface test belongs in tests/ as a Gherkin behaviour test." },
925
999
  schema: [],
926
1000
  messages: {
927
- notModuleLevel: MESSAGE$8,
928
- noPrivateTarget: MESSAGE$8
1001
+ notModuleLevel: MESSAGE$6,
1002
+ noPrivateTarget: MESSAGE$6
929
1003
  }
930
1004
  };
931
1005
  //#endregion
@@ -1041,8 +1115,8 @@ const meta$19 = {
1041
1115
  docs: { description: "A *.model.ts fixture is the pure model a conformance check stands the implementation against, so it may not import the package whose tests it serves. The package is derived from the path: the directory the tests tree lives in. An import, re-export, dynamic import, or require of that package name — or of any subpath of it — is the implementation reaching its own oracle. A relative specifier is judged the same way lexically: resolved against the fixture directory, it must stay inside the tests tree, so ../../src/Lock.js is the implementation again while ./Locks.js is a sibling fixture." },
1042
1116
  schema: [],
1043
1117
  messages: {
1044
- subjectImport: MESSAGE$8,
1045
- relativeEscape: MESSAGE$8
1118
+ subjectImport: MESSAGE$6,
1119
+ relativeEscape: MESSAGE$6
1046
1120
  }
1047
1121
  };
1048
1122
  //#endregion
@@ -1146,42 +1220,25 @@ const modelFixtureImportsSubject = defineRule({
1146
1220
  });
1147
1221
  //#endregion
1148
1222
  //#region src/rules/no-assert-in-property.config.ts
1149
- const MESSAGE$7 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1223
+ const MESSAGE$5 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1150
1224
  const meta$18 = {
1151
1225
  type: "problem",
1152
1226
  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." },
1153
1227
  schema: [],
1154
1228
  messages: {
1155
- expectCall: MESSAGE$7,
1156
- assertCall: MESSAGE$7,
1157
- rawFcRun: MESSAGE$7
1229
+ expectCall: MESSAGE$5,
1230
+ assertCall: MESSAGE$5,
1231
+ rawFcRun: MESSAGE$5
1158
1232
  }
1159
1233
  };
1160
1234
  //#endregion
1161
- //#region src/rules/prop-call.ts
1162
- const PROP_MODIFIERS = /* @__PURE__ */ new Set([
1163
- "only",
1164
- "skip",
1165
- "todo"
1166
- ]);
1167
- const isPropCallee = (callee) => {
1168
- if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return false;
1169
- if (callee.property.name === "prop") {
1170
- const object = callee.object;
1171
- if (object.type === "Identifier") return object.name === "it";
1172
- return object.type === "MemberExpression" && object.property.type === "Identifier" && object.property.name === "effect" && object.object.type === "Identifier" && object.object.name === "it";
1173
- }
1174
- return PROP_MODIFIERS.has(callee.property.name) && isPropCallee(callee.object);
1175
- };
1176
- const getPredicate = (node) => Array$1.findLast(node.arguments, (arg) => arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression");
1177
- //#endregion
1178
1235
  //#region src/rules/no-assert-in-property.ts
1179
1236
  const ASSERT_IDENTIFIER = /^assert/;
1180
1237
  const FC_RUN_METHODS = /* @__PURE__ */ new Set(["assert", "check"]);
1181
1238
  const isInsidePropPredicate = (node) => {
1182
1239
  const parent = node.parent;
1183
1240
  if (parent === null) return false;
1184
- if ((parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") && Option.fromNullishOr(parent.parent).pipe(Option.exists((grandparent) => grandparent.type === "CallExpression" && isPropCallee(grandparent.callee)))) return true;
1241
+ if ((parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") && Option.fromNullishOr(parent.parent).pipe(Option.exists((grandparent) => grandparent.type === "CallExpression" && isPropCallee$1(grandparent.callee)))) return true;
1185
1242
  return isInsidePropPredicate(parent);
1186
1243
  };
1187
1244
  const noAssertInProperty = defineRule({
@@ -1370,7 +1427,7 @@ const IO_SPECIFIERS = {
1370
1427
  "node:https": true,
1371
1428
  "node:dns": true,
1372
1429
  "effect/FileSystem": true,
1373
- "effect/unstable/process/ChildProcessSpawner": true,
1430
+ "effect/process/ChildProcessSpawner": true,
1374
1431
  "@effect/platform/FileSystem": true,
1375
1432
  "@effect/platform/CommandExecutor": true,
1376
1433
  "@effect/platform-node": true,
@@ -1444,7 +1501,7 @@ const noIoModuleInSourceTest = defineRule({
1444
1501
  });
1445
1502
  //#endregion
1446
1503
  //#region src/rules/no-nested-quantification.config.ts
1447
- const MESSAGE$6 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1504
+ const MESSAGE$4 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1448
1505
  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([]))) });
1449
1506
  const ITERATOR_METHODS = /* @__PURE__ */ new Set([
1450
1507
  "every",
@@ -1471,7 +1528,7 @@ const meta$15 = {
1471
1528
  type: "problem",
1472
1529
  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." },
1473
1530
  schema: [Schema.toJsonSchemaDocument(Options$2).schema],
1474
- messages: { nestedQuantification: MESSAGE$6 }
1531
+ messages: { nestedQuantification: MESSAGE$4 }
1475
1532
  };
1476
1533
  //#endregion
1477
1534
  //#region src/rules/no-nested-quantification.ts
@@ -1631,7 +1688,7 @@ const noNestedQuantification = defineRule({
1631
1688
  const basename = context.filename.slice(context.filename.lastIndexOf("/") + 1);
1632
1689
  return { CallExpression(node) {
1633
1690
  if (exempt.has(basename)) return;
1634
- if (!isPropCallee(node.callee)) return;
1691
+ if (!isPropCallee$1(node.callee)) return;
1635
1692
  Option.match(getPredicate(node), {
1636
1693
  onNone: () => {},
1637
1694
  onSome: (predicate) => check(context, node, predicate)
@@ -1641,15 +1698,15 @@ const noNestedQuantification = defineRule({
1641
1698
  });
1642
1699
  //#endregion
1643
1700
  //#region src/rules/no-pseudo-gherkin-unit-tests.config.ts
1644
- const NO_LAYER_IN_FEATURE_NAME = "a *.integration.test.ts feature with no environment double";
1701
+ const NO_LAYER_IN_FEATURE_NAME = "a behaviour test feature with no environment double";
1645
1702
  const NO_LAYER_IN_FEATURE_EXPECTED = "a Feature builder chained with .withLayer(layer) or .withScenarioLayer(layer)";
1646
1703
  const NO_LAYER_IN_FEATURE_ACTUAL = "a Feature(...) call without .withLayer or .withScenarioLayer";
1647
- const NO_LAYER_IN_FEATURE_FIX = "an integration test under WGI-CLS1 must declare its collaborator environment via .withLayer(...) or .withScenarioLayer(...). Chain .withLayer(Layer.empty) (or .withScenarioLayer(Layer.empty)) if the feature exercises in-memory collaborators without custom services, or provide the external boundary Layer it exercises (e.g. .withLayer(MyService.Live)).";
1704
+ const NO_LAYER_IN_FEATURE_FIX = "a behaviour test under WGI-CLS1 must declare its collaborator environment via .withLayer(...) or .withScenarioLayer(...). Chain .withLayer(Layer.empty) (or .withScenarioLayer(Layer.empty)) if the feature exercises in-memory collaborators without custom services, or provide the external boundary Layer it exercises (e.g. .withLayer(MyService.Live)).";
1648
1705
  const meta$14 = {
1649
1706
  type: "problem",
1650
- 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." },
1707
+ docs: { description: "In a behaviour test - any test file that imports @systemfsoftware/effect-gherkin-spec, whatever its name - every Feature builder must configure at least one environment Layer (.withLayer or .withScenarioLayer) to enforce real integration boundaries under WGI-CLS1." },
1651
1708
  schema: [],
1652
- messages: { noLayerInFeature: MESSAGE$8 }
1709
+ messages: { noLayerInFeature: MESSAGE$6 }
1653
1710
  };
1654
1711
  //#endregion
1655
1712
  //#region src/rules/no-pseudo-gherkin-unit-tests.ts
@@ -1681,38 +1738,44 @@ const findRootFeatureCall = (callNode) => {
1681
1738
  const noPseudoGherkinUnitTests = defineRule({
1682
1739
  meta: meta$14,
1683
1740
  create(context) {
1684
- if (!isBehaviourBasename(basenameOf(context.filename))) return {};
1685
- return { CallExpression(node) {
1686
- if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier") {
1687
- if (node.callee.property.name === "body") {
1688
- const rootFeature = findRootFeatureCall(node);
1689
- if (rootFeature !== null && !hasLayerInChain(node)) context.report({
1690
- node: rootFeature,
1691
- messageId: "noLayerInFeature",
1692
- data: {
1693
- name: NO_LAYER_IN_FEATURE_NAME,
1694
- expected: NO_LAYER_IN_FEATURE_EXPECTED,
1695
- actual: NO_LAYER_IN_FEATURE_ACTUAL,
1696
- fix: NO_LAYER_IN_FEATURE_FIX
1697
- }
1698
- });
1741
+ let behaviour = false;
1742
+ return {
1743
+ Program() {
1744
+ behaviour = lanesOf(context).has("behaviour");
1745
+ },
1746
+ CallExpression(node) {
1747
+ if (!behaviour) return;
1748
+ if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier") {
1749
+ if (node.callee.property.name === "body") {
1750
+ const rootFeature = findRootFeatureCall(node);
1751
+ if (rootFeature !== null && !hasLayerInChain(node)) context.report({
1752
+ node: rootFeature,
1753
+ messageId: "noLayerInFeature",
1754
+ data: {
1755
+ name: NO_LAYER_IN_FEATURE_NAME,
1756
+ expected: NO_LAYER_IN_FEATURE_EXPECTED,
1757
+ actual: NO_LAYER_IN_FEATURE_ACTUAL,
1758
+ fix: NO_LAYER_IN_FEATURE_FIX
1759
+ }
1760
+ });
1761
+ }
1699
1762
  }
1700
1763
  }
1701
- } };
1764
+ };
1702
1765
  }
1703
1766
  });
1704
1767
  //#endregion
1705
1768
  //#region src/rules/no-silent-return.config.ts
1706
- const MESSAGE$5 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1769
+ const MESSAGE$3 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1707
1770
  const meta$13 = {
1708
1771
  type: "problem",
1709
1772
  docs: { description: "Property predicates (it.prop / it.effect.prop from @systemfsoftware/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." },
1710
1773
  schema: [],
1711
1774
  messages: {
1712
- bareReturn: MESSAGE$5,
1713
- nonBooleanReturn: MESSAGE$5,
1714
- missingReturn: MESSAGE$5,
1715
- nonBooleanBody: MESSAGE$5
1775
+ bareReturn: MESSAGE$3,
1776
+ nonBooleanReturn: MESSAGE$3,
1777
+ missingReturn: MESSAGE$3,
1778
+ nonBooleanBody: MESSAGE$3
1716
1779
  }
1717
1780
  };
1718
1781
  //#endregion
@@ -1855,7 +1918,7 @@ const noSilentReturn = defineRule({
1855
1918
  meta: meta$13,
1856
1919
  create(context) {
1857
1920
  return { CallExpression(node) {
1858
- if (!isPropCallee(node.callee)) return;
1921
+ if (!isPropCallee$1(node.callee)) return;
1859
1922
  Option.match(getPredicate(node), {
1860
1923
  onNone: () => {},
1861
1924
  onSome: (predicate) => checkFn(context, predicate)
@@ -1871,6 +1934,12 @@ const testFileInSrcDetail = (dir) => ({
1871
1934
  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`,
1872
1935
  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`
1873
1936
  });
1937
+ /** A file with the sanctioned name and place whose content selects no property lane. */
1938
+ const notAPropertyDetail = (dir) => ({
1939
+ expected: testFileInSrcDetail(dir).expected,
1940
+ actual: `a <stem>.workflow.property.test.ts inside a ${dir} directory whose content is not a property test (no FastCheck import and no it.prop / it.effect.prop call, or a differential harness import)`,
1941
+ fix: `state each workflow law as it.prop(...) / it.effect.prop(...) over FastCheck arbitraries. A scenario or differential suite -> move it outside src/ to tests/, named for the lane its imports select`
1942
+ });
1874
1943
  const propertyTestLocationDetail = (dir) => ({
1875
1944
  expected: `src/**/${dir}/<stem>.workflow.property.test.ts — a property test one directory down from the workflow it covers, never beside it`,
1876
1945
  actual: `a property test under src/ that is not a single-segment <stem>.workflow.property.test.ts inside a ${dir} directory`,
@@ -1883,12 +1952,12 @@ const SCHEMA_TEST_DETAIL = {
1883
1952
  };
1884
1953
  const meta$12 = {
1885
1954
  type: "problem",
1886
- 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." },
1955
+ docs: { description: "Under src/, the only sanctioned test file is a single-segment <stem>.workflow.property.test.ts inside a sanctioned test directory whose content is a property test (it imports FastCheck or calls it.prop / it.effect.prop), 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 a Gherkin behaviour test. The sanctioned directory list is the sanctionedDirs option, defaulting to the one directory this repo runs." },
1887
1956
  schema: [Schema.toJsonSchemaDocument(Options$1).schema],
1888
1957
  messages: {
1889
- testFileInSrc: MESSAGE$8,
1890
- schemaTestInSrc: MESSAGE$8,
1891
- propertyTestOutsideTestsDir: MESSAGE$8
1958
+ testFileInSrc: MESSAGE$6,
1959
+ schemaTestInSrc: MESSAGE$6,
1960
+ propertyTestOutsideTestsDir: MESSAGE$6
1892
1961
  }
1893
1962
  };
1894
1963
  //#endregion
@@ -1905,9 +1974,10 @@ const noTestFileInSrc = defineRule({
1905
1974
  const isPropertyTest = basename.endsWith(PROPERTY_SUFFIX);
1906
1975
  const isSchemaTest = basename.endsWith(SCHEMA_SUFFIX);
1907
1976
  const colocated = isInConfiguredTestDir(context.filename, sanctionedDirs);
1908
- if (!isSchemaTest && WORKFLOW_TEST_BASENAME.test(basename) && colocated) return {};
1909
- const [messageId, detail] = violationOf(basename, isPropertyTest, sanctionedDirs[0]);
1977
+ const sanctionedName = !isSchemaTest && WORKFLOW_TEST_BASENAME.test(basename) && colocated;
1978
+ const [messageId, detail] = sanctionedName ? ["testFileInSrc", notAPropertyDetail(sanctionedDirs[0])] : violationOf(basename, isPropertyTest, sanctionedDirs[0]);
1910
1979
  return { Program(node) {
1980
+ if (sanctionedName && isPropertyFile(lanesOf(context))) return;
1911
1981
  context.report({
1912
1982
  node,
1913
1983
  messageId,
@@ -2085,7 +2155,7 @@ const pbtNaming = defineRule({
2085
2155
  });
2086
2156
  //#endregion
2087
2157
  //#region src/rules/prop-arbitrary-schema-origin.config.ts
2088
- const MESSAGE$4 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2158
+ const MESSAGE$2 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2089
2159
  const VIOLATION_NAME$2 = "a hand-built fast-check arbitrary with no schema underneath";
2090
2160
  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";
2091
2161
  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";
@@ -2106,8 +2176,8 @@ const meta$10 = {
2106
2176
  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." },
2107
2177
  schema: [],
2108
2178
  messages: {
2109
- handBuiltArbitrary: MESSAGE$4,
2110
- stockDerivedArbitrary: MESSAGE$4
2179
+ handBuiltArbitrary: MESSAGE$2,
2180
+ stockDerivedArbitrary: MESSAGE$2
2111
2181
  }
2112
2182
  };
2113
2183
  //#endregion
@@ -2188,7 +2258,7 @@ const dynamicEdgeOf = (declarator, init, name) => {
2188
2258
  };
2189
2259
  const vocabularyOf = (edge) => {
2190
2260
  if (SCHEMA_SOURCE_PATTERN.test(edge.source)) return "schema";
2191
- if (edge.source.startsWith("effect/unstable/arbitrary")) return "schema";
2261
+ if (edge.source === "effect/Arbitrary") return "schema";
2192
2262
  if (edge.source === "effect" && edge.imported !== null) {
2193
2263
  if (SCHEMA_NAMESPACE_NAMES[edge.imported] === true) return "schema";
2194
2264
  if (FASTCHECK_NAMESPACE_NAMES[edge.imported] === true) return "handBuilt";
@@ -2220,7 +2290,7 @@ var Provenance = class {
2220
2290
  if (resolveLocal$1(name, node, this.getScope).kind !== "import") return false;
2221
2291
  const edge = this.imports.get(name);
2222
2292
  if (edge === void 0) return false;
2223
- if (edge.source.startsWith("effect/unstable/arbitrary")) return true;
2293
+ if (edge.source === "effect/Arbitrary") return true;
2224
2294
  return edge.source === "effect" && edge.imported === "Arbitrary";
2225
2295
  }
2226
2296
  classifyCall(name, node) {
@@ -2417,7 +2487,7 @@ const propArbitrarySchemaOrigin = defineRule({
2417
2487
  }
2418
2488
  if (!isNode$2(value)) return;
2419
2489
  if (value.type === "IfStatement" && mentionsImportMetaVitest$2(value.test)) return;
2420
- if (value.type === "CallExpression" && isPropCallee(value.callee)) out.push(value);
2490
+ if (value.type === "CallExpression" && isPropCallee$1(value.callee)) out.push(value);
2421
2491
  for (const [key, child] of Object.entries(value)) {
2422
2492
  if (key === "parent") continue;
2423
2493
  collect(child, out);
@@ -2466,7 +2536,7 @@ const propArbitrarySchemaOrigin = defineRule({
2466
2536
  });
2467
2537
  //#endregion
2468
2538
  //#region src/rules/prop-fixture-schema-origin.config.ts
2469
- const MESSAGE$3 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2539
+ const MESSAGE$1 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2470
2540
  const VIOLATION_NAME$1 = "a hand-rolled recursive schema fixture inside a test block";
2471
2541
  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";
2472
2542
  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";
@@ -2475,7 +2545,7 @@ const meta$9 = {
2475
2545
  type: "problem",
2476
2546
  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." },
2477
2547
  schema: [],
2478
- messages: { handRolledRecursiveFixture: MESSAGE$3 }
2548
+ messages: { handRolledRecursiveFixture: MESSAGE$1 }
2479
2549
  };
2480
2550
  //#endregion
2481
2551
  //#region src/rules/prop-fixture-schema-origin.ts
@@ -2677,7 +2747,7 @@ const propFixtureSchemaOrigin = defineRule({
2677
2747
  });
2678
2748
  //#endregion
2679
2749
  //#region src/rules/prop-generated-law-duplicate.config.ts
2680
- const MESSAGE$2 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2750
+ const MESSAGE = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2681
2751
  const COMPILER_NAME = "a runtime probe of a compile-time guarantee";
2682
2752
  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";
2683
2753
  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";
@@ -2691,8 +2761,8 @@ const meta$8 = {
2691
2761
  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." },
2692
2762
  schema: [],
2693
2763
  messages: {
2694
- compilerDuplicate: MESSAGE$2,
2695
- noDomainFunction: MESSAGE$2
2764
+ compilerDuplicate: MESSAGE,
2765
+ noDomainFunction: MESSAGE
2696
2766
  }
2697
2767
  };
2698
2768
  //#endregion
@@ -2851,7 +2921,7 @@ const propGeneratedLawDuplicate = defineRule({
2851
2921
  return;
2852
2922
  }
2853
2923
  if (!isNode(value)) return;
2854
- if (value.type === "CallExpression" && isPropCallee(value.callee)) out.push(value);
2924
+ if (value.type === "CallExpression" && isPropCallee$1(value.callee)) out.push(value);
2855
2925
  for (const [key, child] of Object.entries(value)) {
2856
2926
  if (key === "parent") continue;
2857
2927
  collect(child, out);
@@ -2884,129 +2954,110 @@ const propGeneratedLawDuplicate = defineRule({
2884
2954
  });
2885
2955
  //#endregion
2886
2956
  //#region src/rules/property-file-purity.config.ts
2887
- const PROPERTY_TEST_SUFFIX = ".property.test.ts";
2888
- const MESSAGE$1 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2957
+ const PLAIN_EXPECTED = "it.prop(...) or it.effect.prop(...) — property files never mix with scenario tests";
2958
+ const PLAIN_FIX = "move the scenario test to a test file that imports no FastCheck and calls no it.prop, or rewrite it as a property with arbitraries and a boolean-returning predicate";
2959
+ const RAW_FAST_CHECK_EXPECTED = "it.prop(...) or it.effect.prop(...) from @systemfsoftware/vitest";
2960
+ const RAW_FAST_CHECK_ACTUAL = "bypasses the vitest/Effect integration";
2961
+ const RAW_FAST_CHECK_FIX = "rewrite as it.prop(name, { of, subject, runs }, holds) returning a boolean; fc.* stays for building arbitraries (fc.pre, fc.stringMatching, ...)";
2962
+ const PROPERTY_HOME = "a property test in src/<dir>/__tests__/<stem>.workflow.property.test.ts, apart from any harness test";
2963
+ const MIXED_FIX = "move the property beside the workflow it covers in its own <stem>.workflow.property.test.ts; this file keeps the harness scenarios only";
2964
+ const MIXED_FAST_CHECK_IMPORT_DATA = {
2965
+ name: "a FastCheck import in a Gherkin, conformance or trace test",
2966
+ expected: PROPERTY_HOME,
2967
+ actual: "a test that imports a Gherkin, conformance or trace harness also imports FastCheck",
2968
+ fix: MIXED_FIX
2969
+ };
2970
+ const MIXED_PROP_CALL_DATA = {
2971
+ name: "a property test in a Gherkin, conformance or trace test",
2972
+ expected: PROPERTY_HOME,
2973
+ actual: "a test that imports a Gherkin, conformance or trace harness also calls it.prop / it.effect.prop",
2974
+ fix: MIXED_FIX
2975
+ };
2889
2976
  const meta$7 = {
2890
2977
  type: "problem",
2891
- 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." },
2978
+ docs: { description: "A test that imports FastCheck or calls it.prop / it.effect.prop is a property test file, and it holds only property tests: no plain it()/test()/it.effect(), no raw fc.assert/fc.check/fc.property/fc.asyncProperty. A test that also imports the Gherkin, conformance or trace harness is reported for each FastCheck import and each it.prop / it.effect.prop call: property tests never mix with harness scenarios. A test that imports @systemfsoftware/differential-spec is held to the differential harness rule instead." },
2892
2979
  schema: [],
2893
2980
  messages: {
2894
- plainIt: MESSAGE$1,
2895
- plainEffectIt: MESSAGE$1,
2896
- rawFastCheck: MESSAGE$1,
2897
- fastCheckImport: MESSAGE$1,
2898
- propCall: MESSAGE$1
2981
+ plainIt: MESSAGE$6,
2982
+ plainEffectIt: MESSAGE$6,
2983
+ rawFastCheck: MESSAGE$6,
2984
+ fastCheckImport: MESSAGE$6,
2985
+ propCall: MESSAGE$6
2899
2986
  }
2900
2987
  };
2901
2988
  //#endregion
2902
2989
  //#region src/rules/property-file-purity.ts
2903
- const RAW_FC_METHODS = /* @__PURE__ */ new Set([
2904
- "assert",
2905
- "check",
2906
- "property",
2907
- "asyncProperty"
2908
- ]);
2909
- const PROPERTY_FILE_KIND = {
2910
- suffix: PROPERTY_TEST_SUFFIX,
2911
- plainExpected: "it.prop(...) or it.effect.prop(...) — property files never mix with scenario tests",
2912
- plainFix: "move the scenario test to a plain *.test.ts file, or rewrite it as a property with arbitraries and a boolean-returning predicate",
2913
- rawFcExpected: "it.prop(...) or it.effect.prop(...) from @systemfsoftware/vitest",
2914
- rawFcActualSuffix: "bypasses the vitest/Effect integration",
2915
- rawFcFix: "rewrite as it.prop(name, { of, subject, runs }, holds) returning a boolean; fc.* stays for building arbitraries (fc.pre, fc.stringMatching, ...)"
2916
- };
2917
- const DIFFERENTIAL_FILE_KIND = {
2918
- suffix: DIFFERENTIAL_SUFFIX,
2919
- plainExpected: "the differential harness — Differential.compare(...).on(arb).assert(oracle) or Metamorphic.on(system).relation(...).on(arb)",
2920
- plainFix: "rewrite via @systemfsoftware/differential-spec; scenario tests do not belong in a differential file",
2921
- rawFcExpected: "the harness owns fast-check — pass an fc.Arbitrary to the harness, never call fc.assert/fc.check directly",
2922
- rawFcActualSuffix: "bypasses the differential harness oracle",
2923
- rawFcFix: "build the arbitrary in tests/__fixtures__ and hand it to Differential.compare(...).on(arb) or Metamorphic.on(...).relation(...).on(arb)"
2924
- };
2925
- const reportPlain = (context, node, messageId, actual, kind) => {
2990
+ const reportPlain = (context, node, messageId, actual) => {
2926
2991
  context.report({
2927
2992
  node,
2928
2993
  messageId,
2929
2994
  data: {
2930
- name: `scenario test (${actual}) in a ${kind.suffix} file`,
2931
- expected: kind.plainExpected,
2995
+ name: `scenario test (${actual}) in a property test file`,
2996
+ expected: PLAIN_EXPECTED,
2932
2997
  actual: `${actual} runs a single example, not a property`,
2933
- fix: kind.plainFix
2998
+ fix: PLAIN_FIX
2934
2999
  }
2935
3000
  });
2936
3001
  };
2937
- const reportRawFc = (context, node, method, kind) => {
3002
+ const reportRawFc = (context, node, method) => {
2938
3003
  context.report({
2939
3004
  node,
2940
3005
  messageId: "rawFastCheck",
2941
3006
  data: {
2942
- name: `raw fc.${method}(...) in a ${kind.suffix} file`,
2943
- expected: kind.rawFcExpected,
2944
- actual: `fc.${method}(...) ${kind.rawFcActualSuffix}`,
2945
- fix: kind.rawFcFix
3007
+ name: `raw fc.${method}(...) in a property test file`,
3008
+ expected: RAW_FAST_CHECK_EXPECTED,
3009
+ actual: `fc.${method}(...) ${RAW_FAST_CHECK_ACTUAL}`,
3010
+ fix: RAW_FAST_CHECK_FIX
2946
3011
  }
2947
3012
  });
2948
3013
  };
2949
- const createPropertyFileVisitors = (context, kind) => ({ CallExpression(node) {
3014
+ const checkCall = (context, node) => {
2950
3015
  const callee = node.callee;
2951
3016
  if (callee.type === "Identifier") {
2952
- if (callee.name === "it" || callee.name === "test") reportPlain(context, node, "plainIt", `${callee.name}(...)`, kind);
3017
+ if (callee.name === "it" || callee.name === "test") reportPlain(context, node, "plainIt", `${callee.name}(...)`);
2953
3018
  return;
2954
3019
  }
2955
3020
  if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return;
2956
3021
  const object = callee.object;
2957
- if (object.type === "Identifier" && object.name === "fc" && RAW_FC_METHODS.has(callee.property.name)) {
2958
- reportRawFc(context, node, callee.property.name, kind);
3022
+ if (object.type === "Identifier" && object.name === "fc" && RAW_FC_METHODS[callee.property.name] === true) {
3023
+ reportRawFc(context, node, callee.property.name);
2959
3024
  return;
2960
3025
  }
2961
3026
  if (object.type === "Identifier" && object.name === "it") {
2962
- if (callee.property.name === "effect") reportPlain(context, node, "plainEffectIt", "it.effect(...)", kind);
2963
- else if (PROP_MODIFIERS.has(callee.property.name)) reportPlain(context, node, "plainIt", `it.${callee.property.name}(...)`, kind);
3027
+ if (callee.property.name === "effect") reportPlain(context, node, "plainEffectIt", "it.effect(...)");
3028
+ else if (PROP_MODIFIERS$1.has(callee.property.name)) reportPlain(context, node, "plainIt", `it.${callee.property.name}(...)`);
2964
3029
  return;
2965
3030
  }
2966
- 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}(...)`, kind);
2967
- } });
2968
- const FAST_CHECK_IMPORT_DATA = {
2969
- name: "FastCheck import in a scenario test file",
2970
- expected: `property tests (and every FastCheck usage) live in ${PROPERTY_TEST_SUFFIX} files`,
2971
- actual: `FastCheck imported by a file that is not ${PROPERTY_TEST_SUFFIX}`,
2972
- fix: "move the property test to a *.property.test.ts file; this file keeps plain it() scenario tests only"
2973
- };
2974
- const reportFastCheckImport = (context, node) => {
2975
- context.report({
2976
- node,
2977
- messageId: "fastCheckImport",
2978
- data: FAST_CHECK_IMPORT_DATA
2979
- });
3031
+ if (PROP_MODIFIERS$1.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}(...)`);
2980
3032
  };
2981
- const createScenarioFileVisitors = (context) => ({
2982
- ImportDeclaration(node) {
2983
- if (node.source.value === "fast-check" || node.source.value.startsWith("fast-check/")) {
2984
- reportFastCheckImport(context, node);
2985
- return;
2986
- }
2987
- for (const specifier of node.specifiers) if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "FastCheck") reportFastCheckImport(context, specifier);
2988
- },
2989
- CallExpression(node) {
2990
- if (!isPropCallee(node.callee)) return;
2991
- context.report({
2992
- node,
2993
- messageId: "propCall",
2994
- data: {
2995
- name: "property test in a non-property test file",
2996
- expected: `it.prop / it.effect.prop calls live in ${PROPERTY_TEST_SUFFIX} files`,
2997
- actual: "a property test mixed into a test file that is not a property file",
2998
- fix: "move this test to a *.property.test.ts file — property and non-property tests never mix"
2999
- }
3000
- });
3001
- }
3002
- });
3003
- const kindOf = (filename) => filename.endsWith(".differential.test.ts") ? DIFFERENTIAL_FILE_KIND : PROPERTY_FILE_KIND;
3004
3033
  const propertyFilePurity = defineRule({
3005
3034
  meta: meta$7,
3006
3035
  create(context) {
3007
- if (!isTestFile(basenameOf(context.filename))) return {};
3008
- if (context.filename.endsWith(".property.test.ts") || context.filename.endsWith(".differential.test.ts")) return createPropertyFileVisitors(context, kindOf(context.filename));
3009
- return createScenarioFileVisitors(context);
3036
+ let property = false;
3037
+ let mixed = false;
3038
+ return {
3039
+ Program() {
3040
+ const lanes = lanesOf(context);
3041
+ mixed = mixesPropertyIntoAHarness(lanes);
3042
+ property = !mixed && isPropertyFile(lanes);
3043
+ },
3044
+ ImportDeclaration(node) {
3045
+ if (!mixed) return;
3046
+ for (const site of fastCheckImportSites(node)) context.report({
3047
+ node: site,
3048
+ messageId: "fastCheckImport",
3049
+ data: MIXED_FAST_CHECK_IMPORT_DATA
3050
+ });
3051
+ },
3052
+ CallExpression(node) {
3053
+ if (mixed && isPropCallee$1(node.callee)) context.report({
3054
+ node,
3055
+ messageId: "propCall",
3056
+ data: MIXED_PROP_CALL_DATA
3057
+ });
3058
+ else if (property) checkCall(context, node);
3059
+ }
3060
+ };
3010
3061
  }
3011
3062
  });
3012
3063
  //#endregion
@@ -3023,7 +3074,7 @@ const meta$6 = {
3023
3074
  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." },
3024
3075
  schema: [Schema.toJsonSchemaDocument(Options).schema],
3025
3076
  messages: {
3026
- unsanctionedCell: MESSAGE$8,
3077
+ unsanctionedCell: MESSAGE$6,
3027
3078
  missingCellTest: ABSENCE_MESSAGE
3028
3079
  }
3029
3080
  };
@@ -3084,7 +3135,7 @@ const testFileOutsideTestsDir = defineRule({
3084
3135
  type: "problem",
3085
3136
  docs: { description: "Test files outside src/ must live under a tests/ directory; free-standing test files have no sanctioned home." },
3086
3137
  schema: [],
3087
- messages: { strayTestFile: MESSAGE$8 }
3138
+ messages: { strayTestFile: MESSAGE$6 }
3088
3139
  },
3089
3140
  create(context) {
3090
3141
  if (isUnderSrc(context.filename)) return {};
@@ -3106,34 +3157,112 @@ const testFileOutsideTestsDir = defineRule({
3106
3157
  });
3107
3158
  //#endregion
3108
3159
  //#region src/rules/test-suffix-outside-src.config.ts
3109
- const UNSANCTIONED_SUFFIX_EXPECTED = "*.integration.test.ts, *.differential.test.ts, *.trace.test.ts, or *.conformance.test.ts";
3110
- const UNSANCTIONED_SUFFIX_ACTUAL = "another test suffix outside src/";
3111
- const UNSANCTIONED_SUFFIX_FIX = "public surface or real medium -> *.integration.test.ts; parity -> *.differential.test.ts; span graph -> *.trace.test.ts; concurrent or stateful behaviour -> *.conformance.test.ts; pure-cell property -> in-source; restated literal -> delete";
3160
+ /** The lane word each lane is named by: the dotted segment just before `.test.ts`. */
3161
+ const LANE_WORDS = {
3162
+ behaviour: "integration",
3163
+ conformance: "conformance",
3164
+ differential: "differential",
3165
+ trace: "trace",
3166
+ runner: "runner"
3167
+ };
3168
+ /**
3169
+ * The lanes that take precedence over behaviour when a file is in several: its
3170
+ * name is a harness's. None outranks another, so a file importing several of
3171
+ * these harnesses may take the word of any one of them.
3172
+ */
3173
+ const SPECIFIC_LANES = [
3174
+ "conformance",
3175
+ "differential",
3176
+ "trace"
3177
+ ];
3178
+ const suffixOf = (lane) => `.${LANE_WORDS[lane]}.test.ts`;
3179
+ /** The name the effect-e2e runner collects: a bare `.e2e.ts` suffix, not a `.<word>.test.ts` lane word. */
3180
+ const E2E_SUFFIX = ".e2e.ts";
3181
+ const LANE_MISMATCH_ACTUAL = "a lane word that names no lane its imports select";
3182
+ const LANE_MISMATCH_FIX = "rename the file for a lane it imports: conformance-spec -> *.conformance.test.ts; differential-spec -> *.differential.test.ts; trace-spec -> *.trace.test.ts (a file importing several of these may take any of their words); effect-gherkin-spec alone -> *.integration.test.ts; a runner module alone, in a vitest-runner package -> *.runner.test.ts; a test registered with the effect-e2e runner -> <subject>.e2e.ts";
3183
+ const NO_LANE_EXPECTED = "an import of @systemfsoftware/effect-gherkin-spec, conformance-spec, differential-spec or trace-spec, or of the effect-e2e runner's test";
3184
+ const NO_LANE_ACTUAL = "a test outside src/ whose imports select no lane";
3185
+ const NO_LANE_FIX = "public surface or real medium -> drive it through makeFeature from @systemfsoftware/effect-gherkin-spec; parity -> differential-spec; span graph -> trace-spec; concurrent or stateful behaviour -> conformance-spec; runner suite -> register tests with test from @systemfsoftware/effect-e2e/runner in a <subject>.e2e.ts file; restated literal -> delete";
3186
+ const PROPERTY_OUTSIDE_SRC_EXPECTED = "a property test in src/<dir>/__tests__/<stem>.workflow.property.test.ts beside the workflow it covers";
3187
+ const PROPERTY_OUTSIDE_SRC_ACTUAL = "a test outside src/ whose only lane is the property lane";
3188
+ const PROPERTY_OUTSIDE_SRC_FIX = "move the property beside the workflow it covers, or drive the behaviour through a harness and name the file for that lane";
3189
+ const meta$4 = {
3190
+ type: "problem",
3191
+ docs: { description: "Outside src/, a test file is named for the lanes its imports select: the dotted segment before .test.ts is integration (the Gherkin harness), conformance, differential or trace, and when a file imports a harness besides the Gherkin one it takes that harness's word; a file importing several of the conformance, differential and trace harnesses may take any of their words. In a package whose lint settings declare the vitest-runner role, a test importing a Vitest runner module and no harness is in the runner lane and is named *.runner.test.ts. A file importing the effect-e2e runner's test or its namespace is in the e2e lane and is named <subject>.e2e.ts (the runner's collection, under tests/ outside tests/__fixtures__/); that name outranks every other lane word, and *.e2e.test.ts is not that name. A test whose imports select no lane, or only the property lane, is reported. The name triggers no requirement; the imports do." },
3192
+ schema: [],
3193
+ messages: {
3194
+ laneMismatch: MESSAGE$6,
3195
+ noLane: MESSAGE$6,
3196
+ propertyOutsideSrc: MESSAGE$6
3197
+ }
3198
+ };
3112
3199
  //#endregion
3113
3200
  //#region src/rules/test-suffix-outside-src.ts
3201
+ /** The lane word: the dotted segment immediately before `.test.ts`; earlier segments name the subject. */
3202
+ const LANE_WORD = /\.([^.]+)\.test\.ts$/;
3203
+ /**
3204
+ * The lanes the file may be named for: every conformance, differential or trace
3205
+ * lane it selects (any one of their words will do), else behaviour, else the
3206
+ * runner lane a `vitest-runner` package grants.
3207
+ */
3208
+ const nameableLanes = (lanes, runner) => {
3209
+ const specific = SPECIFIC_LANES.filter((lane) => lanes.has(lane));
3210
+ if (specific.length > 0) return specific;
3211
+ if (lanes.has("behaviour")) return ["behaviour"];
3212
+ return runner ? ["runner"] : [];
3213
+ };
3114
3214
  const testSuffixOutsideSrc = defineRule({
3115
- meta: {
3116
- type: "problem",
3117
- docs: { description: "Outside src/, a test file ends .integration.test.ts, .differential.test.ts, .trace.test.ts, or .conformance.test.ts. A runner-driving package (Vitest itself, the fork, effect-spec-runtime) keeps plain *.test.ts runners and nested-run probes, so the lane taxonomy does not apply there." },
3118
- schema: [],
3119
- messages: { unsanctionedSuffix: MESSAGE$8 }
3120
- },
3215
+ meta: meta$4,
3121
3216
  create(context) {
3122
3217
  const filename = context.filename;
3123
- if (isUnderSrc(filename)) return {};
3124
- if (isRawVitestPackage(filename)) return {};
3125
3218
  const basename = basenameOf(filename);
3126
- if (!isTestFile(basename)) return {};
3127
- if (isBehaviourBasename(basename) || basename.endsWith(".differential.test.ts") || basename.endsWith(".trace.test.ts") || basename.endsWith(".conformance.test.ts")) return {};
3219
+ if (isUnderSrc(filename) || !(isTestFile(basename) || isE2eTestFile(filename))) return {};
3128
3220
  return { Program(node) {
3221
+ const lanes = lanesOf(context);
3222
+ if (lanes.has("e2e")) {
3223
+ if (E2E_TEST_BASENAME.test(basename)) return;
3224
+ context.report({
3225
+ node,
3226
+ messageId: "laneMismatch",
3227
+ data: {
3228
+ name: basename,
3229
+ expected: E2E_SUFFIX,
3230
+ actual: LANE_MISMATCH_ACTUAL,
3231
+ fix: LANE_MISMATCH_FIX
3232
+ }
3233
+ });
3234
+ return;
3235
+ }
3236
+ const nameable = nameableLanes(lanes, isInRunnerLane(context));
3237
+ if (nameable.length === 0) {
3238
+ const property = lanes.has("property");
3239
+ context.report({
3240
+ node,
3241
+ messageId: property ? "propertyOutsideSrc" : "noLane",
3242
+ data: property ? {
3243
+ name: basename,
3244
+ expected: PROPERTY_OUTSIDE_SRC_EXPECTED,
3245
+ actual: PROPERTY_OUTSIDE_SRC_ACTUAL,
3246
+ fix: PROPERTY_OUTSIDE_SRC_FIX
3247
+ } : {
3248
+ name: basename,
3249
+ expected: NO_LANE_EXPECTED,
3250
+ actual: NO_LANE_ACTUAL,
3251
+ fix: NO_LANE_FIX
3252
+ }
3253
+ });
3254
+ return;
3255
+ }
3256
+ const word = LANE_WORD.exec(basename)?.[1];
3257
+ if (nameable.some((lane) => LANE_WORDS[lane] === word)) return;
3129
3258
  context.report({
3130
3259
  node,
3131
- messageId: "unsanctionedSuffix",
3260
+ messageId: "laneMismatch",
3132
3261
  data: {
3133
3262
  name: basename,
3134
- expected: UNSANCTIONED_SUFFIX_EXPECTED,
3135
- actual: UNSANCTIONED_SUFFIX_ACTUAL,
3136
- fix: UNSANCTIONED_SUFFIX_FIX
3263
+ expected: nameable.map(suffixOf).join(" or "),
3264
+ actual: LANE_MISMATCH_ACTUAL,
3265
+ fix: LANE_MISMATCH_FIX
3137
3266
  }
3138
3267
  });
3139
3268
  } };
@@ -3149,15 +3278,15 @@ const HELPER_FIX = "move it under tests/__fixtures__/ — *.schema.ts if it decl
3149
3278
  const testsDirHelpersInFixtures = defineRule({
3150
3279
  meta: {
3151
3280
  type: "problem",
3152
- docs: { description: "Under tests/, the only non-test modules are helpers and fixtures, and they live inside tests/__fixtures__/." },
3281
+ docs: { description: "Under tests/, the only non-test modules are helpers and fixtures, and they live inside tests/__fixtures__/; the .e2e.ts files the effect-e2e runner collects are tests, not helpers." },
3153
3282
  schema: [],
3154
- messages: { helperOutsideFixtures: MESSAGE$8 }
3283
+ messages: { helperOutsideFixtures: MESSAGE$6 }
3155
3284
  },
3156
3285
  create(context) {
3157
3286
  const filename = context.filename;
3158
3287
  if (isUnderSrc(filename)) return {};
3159
3288
  const basename = basenameOf(filename);
3160
- if (isTestFile(basename)) return {};
3289
+ if (isTestFile(basename) || isE2eTestFile(filename)) return {};
3161
3290
  const dirs = directoriesOf(filename);
3162
3291
  if (!dirs.includes("tests")) return {};
3163
3292
  if (dirs.includes("__fixtures__")) return {};
@@ -3184,7 +3313,7 @@ const meta$2 = {
3184
3313
  type: "problem",
3185
3314
  docs: { description: "Forbid package-level tests from relative-importing src or climbing into an internal folder" },
3186
3315
  schema: [],
3187
- messages: { sourceReachIn: MESSAGE$8 }
3316
+ messages: { sourceReachIn: MESSAGE$6 }
3188
3317
  };
3189
3318
  //#endregion
3190
3319
  //#region src/rules/tests-import-public-api.ts
@@ -3240,8 +3369,6 @@ const testsImportPublicApi = defineRule({
3240
3369
  });
3241
3370
  //#endregion
3242
3371
  //#region src/rules/trace-test-requires-taxonomy.config.ts
3243
- const HARNESS_PRESCRIPTION = "import { Suite, Case, Rel, Graph, Observe, Stimulus } from @systemfsoftware/trace-spec and terminate every case on a relation hold — .holds(Rel.all(Rel.exists(PlaceOrder), Rel.child(PlaceOrder, PaymentCapture)))";
3244
- const MISSING_HARNESS_ACTUAL = "no trace-spec import found in a file named as a trace spec";
3245
3372
  const HTTP_MEMBERS = {
3246
3373
  body: true,
3247
3374
  status: true,
@@ -3262,12 +3389,11 @@ const RAW_EMIT_ACTUAL = "a raw span emit call inside a trace spec";
3262
3389
  const RAW_EMIT_FIX = "delete the emit; declare the span with Span.declare in the system under test and assert on the graph the spec receives";
3263
3390
  const meta$1 = {
3264
3391
  type: "problem",
3265
- docs: { description: "A *.trace.test.ts must import its harness from @systemfsoftware/trace-spec and terminate cases on relation holds over the declared span graph. Raw emit calls (startSpan, spanBuilder, startActiveSpan) and HTTP status, statusText, or body assertions (expect(res.status).toBe(200), expect(res).toHaveProperty(\"status\", 200), expect(res).resolves.toMatchObject({ status: 200 }), expect(res).toEqual(expect.objectContaining({ status: 200 }))) are forbidden — replacing that altitude is what a trace spec exists for." },
3392
+ docs: { description: "A test that imports @systemfsoftware/trace-spec (the trace lane) terminates its cases on relation holds over the declared span graph. Raw emit calls (startSpan, spanBuilder, startActiveSpan) and HTTP status, statusText, or body assertions (expect(res.status).toBe(200), expect(res).toHaveProperty(\"status\", 200), expect(res).resolves.toMatchObject({ status: 200 }), expect(res).toEqual(expect.objectContaining({ status: 200 }))) are forbidden — replacing that altitude is what a trace spec exists for." },
3266
3393
  schema: [],
3267
3394
  messages: {
3268
- missingHarnessImport: MESSAGE$8,
3269
- httpTermination: MESSAGE$8,
3270
- rawEmitCall: MESSAGE$8
3395
+ httpTermination: MESSAGE$6,
3396
+ rawEmitCall: MESSAGE$6
3271
3397
  }
3272
3398
  };
3273
3399
  //#endregion
@@ -3304,7 +3430,6 @@ const rawEmitName = (node) => {
3304
3430
  if (name === null || !isEmitCallee(name)) return null;
3305
3431
  return name;
3306
3432
  };
3307
- const hasHarnessBinding = (node) => node.source.value === "@systemfsoftware/trace-spec" && node.specifiers.length > 0;
3308
3433
  const literalValueOf = (node) => {
3309
3434
  if (node === void 0 || node.type !== "Literal") return null;
3310
3435
  return node.value;
@@ -3436,58 +3561,68 @@ const reportHttpTermination = (context, node) => {
3436
3561
  const traceTestRequiresTaxonomy = defineRule({
3437
3562
  meta: meta$1,
3438
3563
  create(context) {
3439
- if (!context.filename.endsWith(".trace.test.ts")) return {};
3440
- let hasHarnessImport = false;
3564
+ let trace = false;
3441
3565
  return {
3442
- ImportDeclaration(node) {
3443
- if (hasHarnessBinding(node)) hasHarnessImport = true;
3566
+ Program() {
3567
+ trace = lanesOf(context).has("trace");
3444
3568
  },
3445
3569
  CallExpression(node) {
3570
+ if (!trace) return;
3446
3571
  reportRawEmit(context, node);
3447
3572
  reportHttpTermination(context, node);
3448
- },
3449
- "Program:exit"(node) {
3450
- if (hasHarnessImport) return;
3451
- context.report({
3452
- node,
3453
- messageId: "missingHarnessImport",
3454
- data: {
3455
- name: `a *.trace.test.ts without ${TRACE_SPEC_PACKAGE}`,
3456
- expected: HARNESS_PRESCRIPTION,
3457
- actual: MISSING_HARNESS_ACTUAL,
3458
- fix: HARNESS_PRESCRIPTION
3459
- }
3460
- });
3461
3573
  }
3462
3574
  };
3463
3575
  }
3464
3576
  });
3465
3577
  //#endregion
3466
3578
  //#region src/rules/vitest-from-systemfsoftware-vitest.config.ts
3467
- const MESSAGE = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
3468
3579
  const VIOLATION_NAME = "a value import from a foreign vitest runner";
3469
3580
  const VIOLATION_EXPECTED = "every test value imported from @systemfsoftware/vitest, which re-exports all of Vitest";
3470
3581
  const VIOLATION_ACTUAL = "a named, namespace, default, side-effect or dynamic value import from vitest or @effect/vitest";
3471
3582
  const VIOLATION_FIX = "import it from @systemfsoftware/vitest instead of vitest or @effect/vitest";
3583
+ const RUNNER_LANES = [
3584
+ "conformance",
3585
+ "differential",
3586
+ "behaviour"
3587
+ ];
3588
+ /** The lanes in which a plain runner call bypasses the harness. */
3589
+ const CALL_LANES = {
3590
+ conformance: true,
3591
+ differential: true,
3592
+ behaviour: false
3593
+ };
3594
+ const PRESCRIPTIONS = {
3595
+ conformance: HARNESS_PRESCRIPTION$1,
3596
+ differential: HARNESS_PRESCRIPTION,
3597
+ behaviour: "import { it } from @systemfsoftware/effect-gherkin-spec and build the suite with makeFeature({ it, layer })"
3598
+ };
3472
3599
  const meta = {
3473
3600
  type: "problem",
3474
- docs: { description: "Every value a test uses from Vitest is imported from @systemfsoftware/vitest, which re-exports all of Vitest. Any value import from vitest or the upstream @effect/vitest — named, namespace, default, side-effect or dynamic — is refused; a type-only import stays legal." },
3601
+ docs: { description: "Every value a test uses from Vitest is imported from @systemfsoftware/vitest, which re-exports all of Vitest. Any value import from vitest or the upstream @effect/vitest — named, namespace, default, side-effect or dynamic — is refused; a type-only import stays legal. A package whose config declares { role: \"vitest-runner\" } is the test framework itself, so its raw Vitest imports are its own lane and are not refused; the rule takes the declaration at its word, and a package that declares the role without being the runner is treated the same. In a test that imports the Gherkin, differential or conformance harness, that harness owns the runner: it, test and describe imported from any runner package are refused, and in the differential and conformance lanes so is a plain runner call." },
3475
3602
  schema: [],
3476
- messages: { vitestImport: MESSAGE }
3603
+ messages: {
3604
+ vitestImport: MESSAGE$6,
3605
+ runnerImport: MESSAGE$6,
3606
+ rawRunnerCall: MESSAGE$6
3607
+ }
3477
3608
  };
3478
3609
  //#endregion
3479
3610
  //#region src/rules/vitest-from-systemfsoftware-vitest.ts
3480
- const isTypeOnly = (node) => {
3481
- if (node.importKind === "type") return true;
3482
- if (node.specifiers.length === 0) return false;
3483
- return node.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type");
3484
- };
3485
3611
  const isForeignVitestLiteral = (node) => node.type === "Literal" && typeof node.value === "string" && FOREIGN_VITEST_SOURCES[node.value] === true;
3612
+ /** The runner a value import specifier names; a string-literal import name never names one. */
3613
+ const runnerNameOf = (specifier) => specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && specifier.imported.type === "Identifier" && RUNNER_NAMES.has(specifier.imported.name) ? specifier.imported.name : void 0;
3614
+ /** A value the declaration binds besides a runner: what still makes it a foreign vitest import. */
3615
+ const bindsANonRunnerValue = (node) => node.specifiers.length === 0 || node.specifiers.some((specifier) => runnerNameOf(specifier) === void 0 && !(specifier.type === "ImportSpecifier" && specifier.importKind === "type"));
3616
+ const memberObjectName = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
3617
+ const calleeName = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName(callee);
3486
3618
  const vitestFromSystemfsoftwareVitest = defineRule({
3487
3619
  meta,
3488
3620
  create(context) {
3489
- if (isRawVitestPackage(context.filename)) return {};
3621
+ const runnerPackage = isRunnerPackage(context);
3622
+ const importedRunners = /* @__PURE__ */ new Set();
3623
+ let lane;
3490
3624
  const report = (node) => {
3625
+ if (runnerPackage) return;
3491
3626
  context.report({
3492
3627
  node,
3493
3628
  messageId: "vitestImport",
@@ -3499,12 +3634,56 @@ const vitestFromSystemfsoftwareVitest = defineRule({
3499
3634
  }
3500
3635
  });
3501
3636
  };
3637
+ /**
3638
+ * In a harness lane the harness owns the runner: one finding per runner the
3639
+ * import names, and the lane arm is the only reporter of that runner.
3640
+ */
3641
+ const reportRunnerImports = (node, harness) => {
3642
+ for (const specifier of node.specifiers) {
3643
+ const runner = runnerNameOf(specifier);
3644
+ if (runner === void 0) continue;
3645
+ importedRunners.add(specifier.local.name);
3646
+ context.report({
3647
+ node: specifier,
3648
+ messageId: "runnerImport",
3649
+ data: {
3650
+ name: `${runner} imported from ${node.source.value} in a ${harness} test`,
3651
+ expected: PRESCRIPTIONS[harness],
3652
+ actual: `a runner imported from a runner package bypasses the ${harness} harness`,
3653
+ fix: `delete the runner import; ${PRESCRIPTIONS[harness]}`
3654
+ }
3655
+ });
3656
+ }
3657
+ };
3502
3658
  return {
3659
+ Program() {
3660
+ const lanes = lanesOf(context);
3661
+ lane = RUNNER_LANES.find((harness) => lanes.has(harness));
3662
+ },
3503
3663
  ImportDeclaration(node) {
3504
- if (FOREIGN_VITEST_SOURCES[node.source.value] !== true) return;
3505
3664
  if (isTypeOnly(node)) return;
3665
+ const source = node.source.value;
3666
+ const harness = FOREIGN_RUNNERS[source] === true ? lane : void 0;
3667
+ if (harness !== void 0) reportRunnerImports(node, harness);
3668
+ if (FOREIGN_VITEST_SOURCES[source] !== true) return;
3669
+ if (harness !== void 0 && !bindsANonRunnerValue(node)) return;
3506
3670
  report(node);
3507
3671
  },
3672
+ CallExpression(node) {
3673
+ if (lane === void 0 || !CALL_LANES[lane]) return;
3674
+ const name = calleeName(node.callee);
3675
+ if (name === void 0 || !RUNNER_NAMES.has(name) || importedRunners.has(name)) return;
3676
+ context.report({
3677
+ node,
3678
+ messageId: "rawRunnerCall",
3679
+ data: {
3680
+ name: `raw runner call (${name}) in a ${lane} test`,
3681
+ expected: PRESCRIPTIONS[lane],
3682
+ actual: `${name}(...) bypasses the ${lane} harness`,
3683
+ fix: `rewrite using ${PRESCRIPTIONS[lane]}`
3684
+ }
3685
+ });
3686
+ },
3508
3687
  ImportExpression(node) {
3509
3688
  if (!isForeignVitestLiteral(node.source)) return;
3510
3689
  report(node);
@@ -3543,6 +3722,7 @@ const recommendedRules = {
3543
3722
  [rule("src-property-test-cell")]: "error",
3544
3723
  [rule("test-file-outside-tests-dir")]: "error",
3545
3724
  [rule("test-suffix-outside-src")]: "error",
3725
+ [rule("e2e-tier-placement")]: "error",
3546
3726
  [rule("behaviour-test-requires-gherkin")]: "error",
3547
3727
  [rule("behaviour-exercises-use-case")]: "error",
3548
3728
  [rule("behaviour-one-feature-per-file")]: "error",
@@ -3576,6 +3756,7 @@ var src_default = {
3576
3756
  "src-property-test-cell": srcPropertyTestCell,
3577
3757
  "test-file-outside-tests-dir": testFileOutsideTestsDir,
3578
3758
  "test-suffix-outside-src": testSuffixOutsideSrc,
3759
+ "e2e-tier-placement": e2eTierPlacement,
3579
3760
  "behaviour-test-requires-gherkin": behaviourTestRequiresGherkin,
3580
3761
  "behaviour-exercises-use-case": behaviourExercisesUseCase,
3581
3762
  "behaviour-one-feature-per-file": behaviourOneFeaturePerFile,