@systemfsoftware/oxlint-plugin-test-discipline 3.8.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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$25 = {
11
+ const meta$28 = {
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: [],
@@ -18,14 +18,14 @@ const meta$25 = {
18
18
  //#region src/rules/ban-raw-span-name-emit.ts
19
19
  const INTERPOLATION = "${}";
20
20
  const isEmitCallee$1 = (name) => EMIT_CALLEES.some((emit) => emit === name);
21
- const memberPropertyName = (callee) => {
21
+ const memberPropertyName$1 = (callee) => {
22
22
  if (callee.computed) return null;
23
23
  if (callee.property.type !== "Identifier") return null;
24
24
  return callee.property.name;
25
25
  };
26
- const calleeName$1 = (callee) => {
26
+ const calleeName$2 = (callee) => {
27
27
  if (callee.type === "Identifier") return callee.name;
28
- if (callee.type === "MemberExpression") return memberPropertyName(callee);
28
+ if (callee.type === "MemberExpression") return memberPropertyName$1(callee);
29
29
  return null;
30
30
  };
31
31
  const templateName = (node) => node.quasis.map((quasi) => quasi.value.raw).join(INTERPOLATION);
@@ -39,7 +39,7 @@ const argName = (arg) => {
39
39
  return inlineName(arg);
40
40
  };
41
41
  const inspect = (node) => {
42
- const callee = calleeName$1(node.callee);
42
+ const callee = calleeName$2(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$25,
52
+ meta: meta$28,
53
53
  create(context) {
54
54
  return { CallExpression(node) {
55
55
  const emitCall = inspect(node);
@@ -71,6 +71,7 @@ const banRawSpanNameEmit = defineRule({
71
71
  //#endregion
72
72
  //#region src/rules/path.config.ts
73
73
  const TEST_BASENAME = /\.(?:test|spec)\.[cm]?tsx?$/;
74
+ const TYPE_TEST_BASENAME = /\.tst\.[cm]?tsx?$/;
74
75
  const SANCTIONED_TEST_DIRS = /* @__PURE__ */ new Set(["tests"]);
75
76
  /**
76
77
  * The only test location sanctioned under `src/`. A workflow property test
@@ -80,7 +81,6 @@ const SANCTIONED_TEST_DIRS = /* @__PURE__ */ new Set(["tests"]);
80
81
  const NESTED_TEST_DIR = "__tests__";
81
82
  const TEST_TREE_DIRS = /* @__PURE__ */ new Set([...SANCTIONED_TEST_DIRS, NESTED_TEST_DIR]);
82
83
  const PROPERTY_SUFFIX = ".property.test.ts";
83
- const INTEGRATION_SUFFIX = ".integration.test.ts";
84
84
  const DIFFERENTIAL_SUFFIX = ".differential.test.ts";
85
85
  /**
86
86
  * Forbidden outright. A schema's laws are generated, so a hand-written
@@ -97,26 +97,42 @@ const SCHEMA_SUFFIX = ".schema.test.ts";
97
97
  const WORKFLOW_TEST_BASENAME = /^[^.]+\.workflow\.property\.test\.ts$/;
98
98
  const GHERKIN_PACKAGE = "@systemfsoftware/effect-gherkin-spec";
99
99
  const DIFFERENTIAL_PACKAGE = "@systemfsoftware/differential-spec";
100
+ const CONFORMANCE_PACKAGE = "@systemfsoftware/conformance-spec";
100
101
  const TRACE_SPEC_PACKAGE = "@systemfsoftware/trace-spec";
101
- const FOREIGN_RUNNERS = /* @__PURE__ */ new Set(["vitest", "@effect/vitest"]);
102
+ const LAWFUL_RUNNER = "@systemfsoftware/vitest";
103
+ const FOREIGN_VITEST_SOURCES = {
104
+ "vitest": true,
105
+ "@effect/vitest": true
106
+ };
107
+ /** Runners a harness-owned test (Gherkin, conformance, differential) must not import directly. */
108
+ const FOREIGN_RUNNERS = {
109
+ ...FOREIGN_VITEST_SOURCES,
110
+ [LAWFUL_RUNNER]: true
111
+ };
112
+ /** Packages whose own tests drive Vitest directly: the runner and its conformance harness. */
113
+ const RAW_VITEST_PACKAGES = {
114
+ "effect-spec-runtime": true,
115
+ "vitest": true,
116
+ "vitest-conformance": true
117
+ };
102
118
  const RUNNER_NAMES = /* @__PURE__ */ new Set([
103
119
  "it",
104
120
  "test",
105
121
  "describe"
106
122
  ]);
107
- const MESSAGE$7 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
123
+ const MESSAGE$8 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
108
124
  const ABSENCE_MESSAGE = "{{name}} is untested. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
109
125
  //#endregion
110
126
  //#region src/rules/behaviour-exercises-use-case.config.ts
111
127
  const NO_SUBJECT_IMPORT_NAME = "a *.integration.test.ts that reaches no package code";
112
128
  const NO_SUBJECT_IMPORT_EXPECTED = "an import of the package code under test";
113
- const NO_SUBJECT_IMPORT_ACTUAL = "a behaviour file whose every runtime import is vitest, @effect/vitest, the gherkin spec package, effect, a Node builtin, or the file itself";
129
+ 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";
114
130
  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.";
115
- const meta$24 = {
131
+ const meta$27 = {
116
132
  type: "problem",
117
133
  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." },
118
134
  schema: [],
119
- messages: { noSubjectImport: MESSAGE$7 }
135
+ messages: { noSubjectImport: MESSAGE$8 }
120
136
  };
121
137
  //#endregion
122
138
  //#region src/rules/path.ts
@@ -126,6 +142,7 @@ const basenameOf = (filename) => Array$1.lastNonEmpty(segmentsOf(filename));
126
142
  /** Directory segments only — the basename never counts as a directory. */
127
143
  const directoriesOf = (filename) => Array$1.initNonEmpty(segmentsOf(filename));
128
144
  const isUnderSrc = (filename) => directoriesOf(filename).includes("src");
145
+ const isRawVitestPackage = (filename) => directoriesOf(filename).some((segment) => RAW_VITEST_PACKAGES[segment] === true);
129
146
  const isInSanctionedTestDir = (filename) => directoriesOf(filename).some((segment) => SANCTIONED_TEST_DIRS.has(segment));
130
147
  const isInTestsImportScope = (filename) => {
131
148
  const segments = segmentsOf(filename);
@@ -135,13 +152,14 @@ const isInTestsImportScope = (filename) => {
135
152
  };
136
153
  const isInConfiguredTestDir = (filename, dirs) => directoriesOf(filename).some((segment) => dirs.includes(segment));
137
154
  const isTestFile = (basename) => TEST_BASENAME.test(basename);
155
+ const isBehaviourBasename = (basename) => basename.endsWith(".integration.test.ts") || basename.endsWith(".contract.test.ts");
138
156
  const CELL_SUFFIX = /\.([^.]+)\.[cm]?tsx?$/;
139
157
  const cellOf = (basename) => CELL_SUFFIX.exec(basename)?.[1];
140
158
  //#endregion
141
159
  //#region src/rules/behaviour-exercises-use-case.ts
142
160
  const EFFECT_PACKAGE = "effect";
143
161
  const FOUNDATION_PACKAGES = /* @__PURE__ */ new Set([
144
- ...FOREIGN_RUNNERS,
162
+ ...Object.keys(FOREIGN_RUNNERS),
145
163
  GHERKIN_PACKAGE,
146
164
  EFFECT_PACKAGE
147
165
  ]);
@@ -164,7 +182,6 @@ const isFoundationImport = (source, filename) => {
164
182
  if (source === "@systemfsoftware/effect-gherkin-spec" && isGherkinPackageTree(filename)) return false;
165
183
  return FOUNDATION_PACKAGES.has(source) || source.startsWith(`${EFFECT_PACKAGE}/`) || source.startsWith("node:");
166
184
  };
167
- const isBehaviourTest$3 = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
168
185
  /** The stem of a path with its final extension stripped, for identity comparisons. */
169
186
  const stemOf = (file) => file.replace(/\.[^/]+$/, "");
170
187
  /**
@@ -216,16 +233,17 @@ const DIST_SEGMENT = /(?:^|\/)dist\//;
216
233
  * name documents it for the reader without pretending to be evidence.
217
234
  *
218
235
  * What counts as reaching: a runtime import from anything that is not the
219
- * scaffolding - vitest, @effect/vitest, the gherkin spec package, effect or a
220
- * subpath of it, or a Node builtin - and not the test file itself, or a dynamic
221
- * `import(...)` of such a source. Type-only specifiers are erased and never
222
- * count; a side-effect import (`import "./x.js"`, `import {} from "./x.js"`)
223
- * executes its module and counts for whatever that module is. Whether a named
224
- * module actually exists is not observable from one file, so an import whose
225
- * path names nothing satisfies the rule the same way a real one does.
236
+ * scaffolding - vitest, @effect/vitest, @systemfsoftware/vitest, the gherkin
237
+ * spec package, effect or a subpath of it, or a Node builtin - and not the test
238
+ * file itself, or a dynamic `import(...)` of such a source. Type-only
239
+ * specifiers are erased and never count; a side-effect import (`import
240
+ * "./x.js"`, `import {} from "./x.js"`) executes its module and counts for
241
+ * whatever that module is. Whether a named module actually exists is not
242
+ * observable from one file, so an import whose path names nothing satisfies the
243
+ * rule the same way a real one does.
226
244
  */
227
245
  const behaviourExercisesUseCase = defineRule({
228
- meta: meta$24,
246
+ meta: meta$27,
229
247
  create(context) {
230
248
  let reached = false;
231
249
  return {
@@ -240,7 +258,7 @@ const behaviourExercisesUseCase = defineRule({
240
258
  if (typeof node.value === "string" && DIST_SEGMENT.test(node.value)) reached = true;
241
259
  },
242
260
  "Program:exit"(node) {
243
- if (!isBehaviourTest$3(basenameOf(context.filename))) return;
261
+ if (!isBehaviourBasename(basenameOf(context.filename))) return;
244
262
  if (reached) return;
245
263
  for (const statement of node.body) {
246
264
  if (statement.type !== "ImportDeclaration") continue;
@@ -275,13 +293,13 @@ const TOO_MANY_FEATURES_NAME = "a *.integration.test.ts accumulating multiple Fe
275
293
  const TOO_MANY_FEATURES_EXPECTED = "exactly one Feature(...) — every additional one signals a junk drawer";
276
294
  const TOO_MANY_FEATURES_ACTUAL = "a behaviour file with two or more Feature(...) calls";
277
295
  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.";
278
- const meta$23 = {
296
+ const meta$26 = {
279
297
  type: "problem",
280
298
  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." },
281
299
  schema: [],
282
300
  messages: {
283
- tooFewFeatures: MESSAGE$7,
284
- tooManyFeatures: MESSAGE$7
301
+ tooFewFeatures: MESSAGE$8,
302
+ tooManyFeatures: MESSAGE$8
285
303
  }
286
304
  };
287
305
  //#endregion
@@ -300,7 +318,6 @@ const isFeatureCallStatement = (statement) => {
300
318
  if (statement.expression.type !== "CallExpression") return false;
301
319
  return rootCalleeName(statement.expression.callee) === "Feature";
302
320
  };
303
- const isBehaviourTest$2 = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
304
321
  const findSecondFeatureCall = (program) => {
305
322
  let seen = 0;
306
323
  for (const statement of program.body) {
@@ -320,10 +337,10 @@ const hasAnyFeatureCall = (program) => {
320
337
  return false;
321
338
  };
322
339
  const behaviourOneFeaturePerFile = defineRule({
323
- meta: meta$23,
340
+ meta: meta$26,
324
341
  create(context) {
325
342
  return { "Program:exit"(node) {
326
- if (!isBehaviourTest$2(basenameOf(context.filename))) return;
343
+ if (!isBehaviourBasename(basenameOf(context.filename))) return;
327
344
  const excess = findSecondFeatureCall(node);
328
345
  if (excess !== null) {
329
346
  const total = countFeatureCalls(node);
@@ -355,19 +372,19 @@ const behaviourOneFeaturePerFile = defineRule({
355
372
  //#endregion
356
373
  //#region src/rules/behaviour-test-requires-gherkin.config.ts
357
374
  const FOREIGN_RUNNER_EXPECTED = "it and layer imported from @systemfsoftware/effect-gherkin-spec";
358
- const FOREIGN_RUNNER_ACTUAL = "a test runner imported directly from vitest or @effect/vitest in a behaviour file";
359
- const FOREIGN_RUNNER_FIX = "import { it, layer } from @systemfsoftware/effect-gherkin-spec and build the suite with makeFeature({ it, layer })";
375
+ const FOREIGN_RUNNER_ACTUAL = "a test runner imported directly from vitest, @effect/vitest or @systemfsoftware/vitest in a behaviour file";
376
+ const FOREIGN_RUNNER_FIX = "import { it } from @systemfsoftware/effect-gherkin-spec and build the suite with makeFeature({ it })";
360
377
  const MISSING_MAKE_FEATURE_NAME = "a *.integration.test.ts without makeFeature";
361
378
  const MISSING_MAKE_FEATURE_EXPECTED = "makeFeature imported from @systemfsoftware/effect-gherkin-spec";
362
379
  const MISSING_MAKE_FEATURE_ACTUAL = "a behaviour file that never constructs a Gherkin feature";
363
380
  const MISSING_MAKE_FEATURE_FIX = "import { makeFeature } from @systemfsoftware/effect-gherkin-spec and declare `const Feature = makeFeature({ it, layer })`";
364
- const meta$22 = {
381
+ const meta$25 = {
365
382
  type: "problem",
366
- docs: { description: "A *.integration.test.ts must drive its suite through makeFeature from @systemfsoftware/effect-gherkin-spec and must not import test runners from vitest or @effect/vitest." },
383
+ 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." },
367
384
  schema: [],
368
385
  messages: {
369
- foreignRunner: MESSAGE$7,
370
- missingMakeFeature: MESSAGE$7
386
+ foreignRunner: MESSAGE$8,
387
+ missingMakeFeature: MESSAGE$8
371
388
  }
372
389
  };
373
390
  //#endregion
@@ -387,13 +404,12 @@ const foreignRunnerNameOf = (specifier) => {
387
404
  const { name } = Schema.decodeSync(ImportedIdentifier)(specifier.imported);
388
405
  return RUNNER_NAMES.has(name) ? name : null;
389
406
  };
390
- const isBehaviourTest$1 = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
391
407
  const behaviourTestRequiresGherkin = defineRule({
392
- meta: meta$22,
408
+ meta: meta$25,
393
409
  create(context) {
394
410
  const basename = basenameOf(context.filename);
395
411
  return { Program(node) {
396
- if (!isBehaviourTest$1(basename)) return;
412
+ if (!isBehaviourBasename(basename)) return;
397
413
  let hasMakeFeature = false;
398
414
  for (const statement of node.body) {
399
415
  if (statement.type !== "ImportDeclaration") continue;
@@ -402,7 +418,7 @@ const behaviourTestRequiresGherkin = defineRule({
402
418
  if (specifier.type !== "ImportSpecifier") continue;
403
419
  if (isMakeFeatureSpecifier(specifier)) hasMakeFeature = true;
404
420
  }
405
- if (!FOREIGN_RUNNERS.has(sourceValue)) continue;
421
+ if (FOREIGN_RUNNERS[sourceValue] !== true) continue;
406
422
  for (const specifier of statement.specifiers) {
407
423
  if (specifier.type !== "ImportSpecifier") continue;
408
424
  const runnerName = foreignRunnerNameOf(specifier);
@@ -432,6 +448,145 @@ const behaviourTestRequiresGherkin = defineRule({
432
448
  } };
433
449
  }
434
450
  });
451
+ const HARNESS_MEMBERS = {
452
+ linearizable: true,
453
+ sequential: true,
454
+ released: true
455
+ };
456
+ const LEGACY_CHECK_MEMBER = "check";
457
+ 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";
458
+ const meta$24 = {
459
+ type: "problem",
460
+ 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." },
461
+ schema: [],
462
+ messages: {
463
+ rawRunnerCall: MESSAGE$8,
464
+ runnerImport: MESSAGE$8,
465
+ missingHarnessImport: MESSAGE$8,
466
+ legacyHarnessCall: MESSAGE$8,
467
+ missingHarnessUsage: MESSAGE$8
468
+ }
469
+ };
470
+ //#endregion
471
+ //#region src/rules/conformance-test-requires-harness.ts
472
+ const memberObjectName$1 = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
473
+ const memberPropertyName = (callee) => callee.type === "MemberExpression" && callee.property.type === "Identifier" ? callee.property.name : void 0;
474
+ const memberName = (callee) => callee.type === "Identifier" ? callee.name : memberPropertyName(callee);
475
+ const calleeName$1 = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName$1(callee);
476
+ const isRawRunnerCall = (callee) => {
477
+ const name = calleeName$1(callee);
478
+ return name !== void 0 && RUNNER_NAMES.has(name);
479
+ };
480
+ const isForeignRunnerImport$1 = (node) => typeof node.source.value === "string" && FOREIGN_RUNNERS[node.source.value] === true;
481
+ const namesConformanceExport = (specifier) => specifier.imported.type === "Identifier" && specifier.imported.name === "Conformance";
482
+ const recordBindings = (node, packageBindings, conformanceBindings) => {
483
+ if (node.source.value !== "@systemfsoftware/conformance-spec") return;
484
+ for (const specifier of node.specifiers) {
485
+ packageBindings.add(specifier.local.name);
486
+ if (specifier.type === "ImportSpecifier" && namesConformanceExport(specifier)) conformanceBindings.add(specifier.local.name);
487
+ }
488
+ };
489
+ const isHarnessInvocation = (callee, conformanceBindings) => {
490
+ const member = memberName(callee);
491
+ const root = calleeName$1(callee);
492
+ return member !== void 0 && HARNESS_MEMBERS[member] === true && root !== void 0 && conformanceBindings.has(root);
493
+ };
494
+ const isLegacyCheck = (callee, packageBindings) => {
495
+ const root = calleeName$1(callee);
496
+ return memberName(callee) === "check" && root !== void 0 && packageBindings.has(root);
497
+ };
498
+ const legacyCallText = (callee) => callee.type === "Identifier" ? callee.name : `${calleeName$1(callee)}.${LEGACY_CHECK_MEMBER}`;
499
+ const legacyCheckError = (callee) => ({
500
+ messageId: "legacyHarnessCall",
501
+ data: {
502
+ name: `a .${LEGACY_CHECK_MEMBER}(...) call in a conformance test file`,
503
+ expected: HARNESS_PRESCRIPTION$2,
504
+ actual: `${legacyCallText(callee)} is not a check the barrel exposes`,
505
+ fix: HARNESS_PRESCRIPTION$2
506
+ }
507
+ });
508
+ const conformanceTestRequiresHarness = defineRule({
509
+ meta: meta$24,
510
+ create(context) {
511
+ if (!context.filename.endsWith(".conformance.test.ts")) return {};
512
+ const packageBindings = /* @__PURE__ */ new Set();
513
+ const conformanceBindings = /* @__PURE__ */ new Set();
514
+ let violations = 0;
515
+ let hasHarnessInvocation = false;
516
+ return {
517
+ ImportDeclaration(node) {
518
+ if (isForeignRunnerImport$1(node)) {
519
+ context.report({
520
+ node,
521
+ messageId: "runnerImport",
522
+ data: {
523
+ name: `runner import from ${String(node.source.value)} in a conformance test file`,
524
+ expected: HARNESS_PRESCRIPTION$2,
525
+ actual: "a direct vitest / @effect/vitest / @systemfsoftware/vitest runner import bypasses the conformance check",
526
+ fix: `delete the runner import; ${HARNESS_PRESCRIPTION$2}`
527
+ }
528
+ });
529
+ violations += 1;
530
+ return;
531
+ }
532
+ recordBindings(node, packageBindings, conformanceBindings);
533
+ },
534
+ CallExpression(node) {
535
+ if (isRawRunnerCall(node.callee)) {
536
+ const name = calleeName$1(node.callee) ?? "";
537
+ context.report({
538
+ node,
539
+ messageId: "rawRunnerCall",
540
+ data: {
541
+ name: `raw runner call (${name}) in a conformance test file`,
542
+ expected: HARNESS_PRESCRIPTION$2,
543
+ actual: `${name}(...) bypasses the conformance check`,
544
+ fix: `rewrite using ${HARNESS_PRESCRIPTION$2}`
545
+ }
546
+ });
547
+ violations += 1;
548
+ return;
549
+ }
550
+ if (isHarnessInvocation(node.callee, conformanceBindings)) {
551
+ hasHarnessInvocation = true;
552
+ return;
553
+ }
554
+ if (isLegacyCheck(node.callee, packageBindings)) {
555
+ context.report({
556
+ node,
557
+ ...legacyCheckError(node.callee)
558
+ });
559
+ violations += 1;
560
+ }
561
+ },
562
+ "Program:exit"(node) {
563
+ if (packageBindings.size === 0) {
564
+ context.report({
565
+ node,
566
+ messageId: "missingHarnessImport",
567
+ data: {
568
+ name: `conformance test file without the ${CONFORMANCE_PACKAGE} import`,
569
+ expected: HARNESS_PRESCRIPTION$2,
570
+ actual: "no conformance check import found",
571
+ fix: HARNESS_PRESCRIPTION$2
572
+ }
573
+ });
574
+ return;
575
+ }
576
+ if (!hasHarnessInvocation && violations === 0) context.report({
577
+ node,
578
+ messageId: "missingHarnessUsage",
579
+ data: {
580
+ name: `conformance test file imports ${CONFORMANCE_PACKAGE} but never invokes it`,
581
+ expected: HARNESS_PRESCRIPTION$2,
582
+ actual: "no Conformance.linearizable, Conformance.sequential, or Conformance.released call runs",
583
+ fix: HARNESS_PRESCRIPTION$2
584
+ }
585
+ });
586
+ }
587
+ };
588
+ }
589
+ });
435
590
  //#endregion
436
591
  //#region src/rules/damp-test-naming.config.ts
437
592
  const PASCAL_CASE$1 = /^[A-Z][a-z][a-zA-Z0-9]*$/;
@@ -452,7 +607,7 @@ const INVALID_BEHAVIOR_CASE_FIX = "Convert behavior to PascalCase (e.g., throwEr
452
607
  const INVALID_CONDITION_CASE_FIX = "Convert condition to PascalCase (e.g., passwordInvalid → PasswordInvalid)";
453
608
  const EMPTY_BEHAVIOR_ACTUAL = "Empty string between Should_ and _When_";
454
609
  const EMPTY_CONDITION_ACTUAL = "Empty string after _When_";
455
- const meta$21 = {
610
+ const meta$23 = {
456
611
  type: "suggestion",
457
612
  docs: { description: "Enforce DAMP (Descriptive and Meaningful Phrases) test naming format: Should_[ExpectedBehavior]_When_[Condition]" },
458
613
  schema: [],
@@ -532,8 +687,9 @@ const isTestFunctionCall = (node) => {
532
687
  return false;
533
688
  };
534
689
  const dampTestNaming = defineRule({
535
- meta: meta$21,
690
+ meta: meta$23,
536
691
  create(context) {
692
+ if (TYPE_TEST_BASENAME.test(basenameOf(context.filename))) return {};
537
693
  return { CallExpression(node) {
538
694
  if (!isTestFunctionCall(node)) return;
539
695
  const firstArg = node.arguments[0];
@@ -555,7 +711,7 @@ const dampTestNaming = defineRule({
555
711
  });
556
712
  //#endregion
557
713
  //#region src/rules/differential-test-requires-harness.config.ts
558
- const meta$20 = {
714
+ const meta$22 = {
559
715
  type: "problem",
560
716
  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." },
561
717
  schema: [],
@@ -568,16 +724,16 @@ const meta$20 = {
568
724
  };
569
725
  //#endregion
570
726
  //#region src/rules/differential-test-requires-harness.ts
571
- const HARNESS_PRESCRIPTION$1 = "import { Differential, Metamorphic } from @systemfsoftware/differential-spec and express the test as Differential.compare({ reference, candidate }).on(arb).assert(oracle) or Metamorphic.on(system).relation({ transformInput, assertOutput }).on(arb)";
727
+ 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)";
572
728
  const memberObjectName = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
573
729
  const calleeName = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName(callee);
574
730
  const recordHarnessBindings = (node, bindings) => {
575
731
  if (node.source.value !== "@systemfsoftware/differential-spec") return;
576
732
  for (const specifier of node.specifiers) bindings.add(specifier.local.name);
577
733
  };
578
- const isForeignRunnerImport = (node) => typeof node.source.value === "string" && FOREIGN_RUNNERS.has(node.source.value);
734
+ const isForeignRunnerImport = (node) => typeof node.source.value === "string" && FOREIGN_RUNNERS[node.source.value] === true;
579
735
  const differentialTestRequiresHarness = defineRule({
580
- meta: meta$20,
736
+ meta: meta$22,
581
737
  create(context) {
582
738
  if (!context.filename.endsWith(".differential.test.ts")) return {};
583
739
  const harnessBindings = /* @__PURE__ */ new Set();
@@ -592,7 +748,7 @@ const differentialTestRequiresHarness = defineRule({
592
748
  data: {
593
749
  name: `runner import from ${String(node.source.value)} in a differential test file`,
594
750
  expected: HARNESS_PRESCRIPTION$1,
595
- actual: "a direct vitest / @effect/vitest runner import bypasses the differential oracle",
751
+ actual: "a direct vitest / @effect/vitest / @systemfsoftware/vitest runner import bypasses the differential oracle",
596
752
  fix: `delete the runner import; ${HARNESS_PRESCRIPTION$1}`
597
753
  }
598
754
  });
@@ -653,11 +809,11 @@ const NON_PROP_CALL_NAME = "a non-property test call inside an `import.meta.vite
653
809
  const NON_PROP_CALL_EXPECTED = "only `it.prop` or `it.effect.prop` member-chain calls (standard modifiers included) with boolean predicates";
654
810
  const NON_PROP_CALL_ACTUAL = "a bare or member-chain test call other than `it.prop`/`it.effect.prop`";
655
811
  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";
656
- const meta$19 = {
812
+ const meta$21 = {
657
813
  type: "problem",
658
814
  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." },
659
815
  schema: [],
660
- messages: { nonPropCall: MESSAGE$7 }
816
+ messages: { nonPropCall: MESSAGE$8 }
661
817
  };
662
818
  //#endregion
663
819
  //#region src/rules/vitest-guard.ts
@@ -717,7 +873,7 @@ const rootNameOf = (node) => {
717
873
  if (node.type === "CallExpression") return rootNameOf(node.callee);
718
874
  };
719
875
  const inSourceTestPropOnly = defineRule({
720
- meta: meta$19,
876
+ meta: meta$21,
721
877
  create(context) {
722
878
  const filename = context.filename;
723
879
  const basename = basenameOf(filename);
@@ -758,17 +914,17 @@ const NOT_MODULE_LEVEL_NAME = "a nested `import.meta.vitest` block";
758
914
  const NOT_MODULE_LEVEL_EXPECTED = "the in-source test block as a direct statement of the module body";
759
915
  const NOT_MODULE_LEVEL_ACTUAL = "an `import.meta.vitest` block nested inside another statement";
760
916
  const NOT_MODULE_LEVEL_FIX = "move the block to module level — a nested block does not run under vitest includeSource";
761
- const NO_PRIVATE_TARGET_NAME = "an `import.meta.vitest` block touching no private binding";
762
- const NO_PRIVATE_TARGET_EXPECTED = "an in-source test exercising a non-exported module-level binding";
763
- const NO_PRIVATE_TARGET_ACTUAL = "an in-source block referencing only exported or imported names";
917
+ const NO_PRIVATE_TARGET_NAME = "an `import.meta.vitest` block touching no module-level binding";
918
+ const NO_PRIVATE_TARGET_EXPECTED = "an in-source test exercising this module's own code — a private helper, or a module-level function the lawful prop's `subject` names";
919
+ const NO_PRIVATE_TARGET_ACTUAL = "an in-source block referencing only exported-call or imported names";
764
920
  const NO_PRIVATE_TARGET_FIX = "test the public surface from tests/ as *.integration.test.ts; in-source blocks exist for private helpers only — if the public behaviour you meant to cover is a pure function, delete the assertion: the type system already proves it";
765
- const meta$18 = {
921
+ const meta$20 = {
766
922
  type: "problem",
767
923
  docs: { description: "In-source `if (import.meta.vitest)` blocks under src/ must be at module level and exercise at least one non-exported binding; other tests belong in tests/." },
768
924
  schema: [],
769
925
  messages: {
770
- notModuleLevel: MESSAGE$7,
771
- noPrivateTarget: MESSAGE$7
926
+ notModuleLevel: MESSAGE$8,
927
+ noPrivateTarget: MESSAGE$8
772
928
  }
773
929
  };
774
930
  //#endregion
@@ -795,8 +951,25 @@ const collectPrivateNames = (body, out) => {
795
951
  if (isCollectable(node)) collectDeclaration(node, out);
796
952
  }
797
953
  };
954
+ const collectModuleNames = (body, out) => {
955
+ for (const node of body) {
956
+ if (node.type === "ExportNamedDeclaration") {
957
+ if (node.source === null && node.declaration !== null && isCollectable(node.declaration)) collectDeclaration(node.declaration, out);
958
+ continue;
959
+ }
960
+ if (node.type === "ExportDefaultDeclaration" && isCollectable(node.declaration)) {
961
+ collectDeclaration(node.declaration, out);
962
+ continue;
963
+ }
964
+ if (isCollectable(node)) collectDeclaration(node, out);
965
+ }
966
+ };
967
+ const isSubjectValue = (node) => {
968
+ const parent = node.parent;
969
+ return parent.type === "Property" && parent.computed !== true && parent.key.type === "Identifier" && parent.key.name === "subject" && parent.value === node;
970
+ };
798
971
  const inSourceTestTargetsPrivate = defineRule({
799
- meta: meta$18,
972
+ meta: meta$20,
800
973
  create(context) {
801
974
  const filename = context.filename;
802
975
  const basename = basenameOf(filename);
@@ -804,10 +977,12 @@ const inSourceTestTargetsPrivate = defineRule({
804
977
  const inTestFile = isTestFile(basename);
805
978
  if (!underSrc || inTestFile) return {};
806
979
  const privateNames = /* @__PURE__ */ new Set();
980
+ const moduleNames = /* @__PURE__ */ new Set();
807
981
  const guards = [];
808
982
  return {
809
983
  Program(node) {
810
984
  collectPrivateNames(node.body, privateNames);
985
+ collectModuleNames(node.body, moduleNames);
811
986
  },
812
987
  IfStatement(node) {
813
988
  if (!isVitestGuard(node.test)) return;
@@ -830,7 +1005,7 @@ const inSourceTestTargetsPrivate = defineRule({
830
1005
  });
831
1006
  },
832
1007
  Identifier(node) {
833
- if (!privateNames.has(node.name)) return;
1008
+ if (!(privateNames.has(node.name) || moduleNames.has(node.name) && isSubjectValue(node))) return;
834
1009
  const guard = guards.find((g) => isInsideConsequent(node, g.node.consequent));
835
1010
  if (guard !== void 0) guard.hit = true;
836
1011
  },
@@ -853,16 +1028,132 @@ const inSourceTestTargetsPrivate = defineRule({
853
1028
  }
854
1029
  });
855
1030
  //#endregion
1031
+ //#region src/rules/model-fixture-imports-subject.config.ts
1032
+ const SUBJECT_IMPORT_NAME = "a *.model.ts fixture that imports the package under test";
1033
+ const SUBJECT_IMPORT_EXPECTED = "a pure model that imports only effect, node builtins, and harness types";
1034
+ const SUBJECT_IMPORT_FIX = "delete the import; the model derives its state from the declared Schema it is given, so it needs no symbol from the package it stands in for";
1035
+ const RELATIVE_ESCAPE_NAME = "a *.model.ts fixture that climbs out of the tests tree";
1036
+ const RELATIVE_ESCAPE_EXPECTED = "a relative import that stays inside the tests tree — a sibling fixture under tests/__fixtures__/";
1037
+ const RELATIVE_ESCAPE_FIX = "import the sibling fixture with ./<name>.js, or move the model; a model that reaches src/ is the implementation checking itself";
1038
+ const meta$19 = {
1039
+ type: "problem",
1040
+ 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." },
1041
+ schema: [],
1042
+ messages: {
1043
+ subjectImport: MESSAGE$8,
1044
+ relativeEscape: MESSAGE$8
1045
+ }
1046
+ };
1047
+ //#endregion
1048
+ //#region src/rules/model-fixture-imports-subject.ts
1049
+ /**
1050
+ * The basename of the directory whose tests tree the fixture lives in. A
1051
+ * `*.model.ts` serves that package's tests, so the directory name is the
1052
+ * unscoped name of the package under test. Read lexically, the way every rule
1053
+ * here reads a path: one file cannot know what else is on disk (OX-TS2), and
1054
+ * with no tests tree in the path the subject is unknown — an unknown subject is
1055
+ * never reported.
1056
+ */
1057
+ const packageDirOf = (filename) => {
1058
+ const dirs = directoriesOf(filename);
1059
+ const index = dirs.findIndex((segment) => TEST_TREE_DIRS.has(segment));
1060
+ return index <= 0 ? void 0 : dirs[index - 1];
1061
+ };
1062
+ /**
1063
+ * The unscoped, subpath-stripped head of a package specifier:
1064
+ * `@systemfsoftware/effect-lock/testing` -> `effect-lock`. A subpath is still
1065
+ * the same dependency, and admitting it would let a fixture reach the
1066
+ * implementation through a nested entry.
1067
+ */
1068
+ const packageHeadOf = (source) => {
1069
+ const unscoped = source.startsWith("@") ? source.slice(source.indexOf("/") + 1) : source;
1070
+ return unscoped.includes("/") ? unscoped.slice(0, unscoped.indexOf("/")) : unscoped;
1071
+ };
1072
+ const importsSubject = (source, subjectDir) => subjectDir !== void 0 && packageHeadOf(source) === subjectDir;
1073
+ const specifierOf$1 = (node) => node !== void 0 && node.type === "Literal" && typeof node.value === "string" ? node.value : void 0;
1074
+ const isRelativeSpecifier = (source) => source.startsWith("./") || source.startsWith("../");
1075
+ /**
1076
+ * The directory stack a relative specifier resolves to against the linted
1077
+ * file's own directory, `..` segments collapsed. Existence is never checked:
1078
+ * one file cannot know what else is on disk (OX-TS2), so only the lexical
1079
+ * identity is decided.
1080
+ */
1081
+ const resolveRelative = (source, filename) => {
1082
+ const stack = [...directoriesOf(filename)];
1083
+ for (const segment of source.split("/")) {
1084
+ if (segment === "" || segment === ".") continue;
1085
+ if (segment === "..") {
1086
+ stack.pop();
1087
+ continue;
1088
+ }
1089
+ stack.push(segment);
1090
+ }
1091
+ return stack;
1092
+ };
1093
+ const inTestsTree = (segments) => segments.some((segment) => TEST_TREE_DIRS.has(segment));
1094
+ const modelFixtureImportsSubject = defineRule({
1095
+ meta: meta$19,
1096
+ create(context) {
1097
+ if (!basenameOf(context.filename).endsWith(".model.ts")) return {};
1098
+ const subjectDir = packageDirOf(context.filename);
1099
+ const fixtureInTestsTree = inTestsTree(directoriesOf(context.filename));
1100
+ const checkSource = (node, source) => {
1101
+ const specifier = specifierOf$1(source);
1102
+ if (specifier === void 0) return;
1103
+ if (importsSubject(specifier, subjectDir)) {
1104
+ context.report({
1105
+ node,
1106
+ messageId: "subjectImport",
1107
+ data: {
1108
+ name: SUBJECT_IMPORT_NAME,
1109
+ expected: SUBJECT_IMPORT_EXPECTED,
1110
+ actual: `a dependency on ${specifier} in a *.model.ts fixture`,
1111
+ fix: SUBJECT_IMPORT_FIX
1112
+ }
1113
+ });
1114
+ return;
1115
+ }
1116
+ if (fixtureInTestsTree && isRelativeSpecifier(specifier) && !inTestsTree(resolveRelative(specifier, context.filename))) context.report({
1117
+ node,
1118
+ messageId: "relativeEscape",
1119
+ data: {
1120
+ name: RELATIVE_ESCAPE_NAME,
1121
+ expected: RELATIVE_ESCAPE_EXPECTED,
1122
+ actual: `a relative import of ${specifier}, resolving outside the tests tree`,
1123
+ fix: RELATIVE_ESCAPE_FIX
1124
+ }
1125
+ });
1126
+ };
1127
+ return {
1128
+ ImportDeclaration(node) {
1129
+ checkSource(node, node.source);
1130
+ },
1131
+ ExportNamedDeclaration(node) {
1132
+ if (node.source !== null) checkSource(node, node.source);
1133
+ },
1134
+ ExportAllDeclaration(node) {
1135
+ checkSource(node, node.source);
1136
+ },
1137
+ ImportExpression(node) {
1138
+ checkSource(node, node.source);
1139
+ },
1140
+ TSImportEqualsDeclaration(node) {
1141
+ if (node.moduleReference.type === "TSExternalModuleReference") checkSource(node, node.moduleReference.expression);
1142
+ }
1143
+ };
1144
+ }
1145
+ });
1146
+ //#endregion
856
1147
  //#region src/rules/no-assert-in-property.config.ts
857
- const MESSAGE$6 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
858
- const meta$17 = {
1148
+ const MESSAGE$7 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1149
+ const meta$18 = {
859
1150
  type: "problem",
860
1151
  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." },
861
1152
  schema: [],
862
1153
  messages: {
863
- expectCall: MESSAGE$6,
864
- assertCall: MESSAGE$6,
865
- rawFcRun: MESSAGE$6
1154
+ expectCall: MESSAGE$7,
1155
+ assertCall: MESSAGE$7,
1156
+ rawFcRun: MESSAGE$7
866
1157
  }
867
1158
  };
868
1159
  //#endregion
@@ -893,7 +1184,7 @@ const isInsidePropPredicate = (node) => {
893
1184
  return isInsidePropPredicate(parent);
894
1185
  };
895
1186
  const noAssertInProperty = defineRule({
896
- meta: meta$17,
1187
+ meta: meta$18,
897
1188
  create(context) {
898
1189
  return { CallExpression(node) {
899
1190
  const callee = node.callee;
@@ -932,6 +1223,20 @@ const noAssertInProperty = defineRule({
932
1223
  }
933
1224
  });
934
1225
  //#endregion
1226
+ //#region src/rules/expect-call.ts
1227
+ const EXPECT = "expect";
1228
+ const isExpectCallee = (callee) => callee.type === "Identifier" && callee.name === EXPECT;
1229
+ const expectCallOf = (node, acceptsMember) => {
1230
+ if (node.callee.type !== "MemberExpression") return void 0;
1231
+ let target = node.callee.object;
1232
+ while (target.type === "MemberExpression") {
1233
+ if (acceptsMember !== void 0 && acceptsMember(target) === false) return void 0;
1234
+ target = target.object;
1235
+ }
1236
+ if (target.type !== "CallExpression" || isExpectCallee(target.callee) === false) return void 0;
1237
+ return target;
1238
+ };
1239
+ //#endregion
935
1240
  //#region src/rules/no-behaviourless-assertion.config.ts
936
1241
  const TEST_FILE = /\.(test|spec)\.[cm]?tsx?$/;
937
1242
  const BEHAVIOUR_NODES = {
@@ -946,7 +1251,7 @@ const SKIP_WALK_KEYS = /* @__PURE__ */ new Set([
946
1251
  "range",
947
1252
  "parent"
948
1253
  ]);
949
- const meta$16 = {
1254
+ const meta$17 = {
950
1255
  type: "problem",
951
1256
  docs: { description: "Flag an assertion whose subject and expectation are both built only from imported declarations and literals. Such an assertion invokes nothing, so no change to the behaviour under test can make it fail." },
952
1257
  schema: [],
@@ -996,15 +1301,6 @@ const dependsOnBehaviour = (node, imported) => {
996
1301
  }
997
1302
  return false;
998
1303
  };
999
- const expectCallOf = (node) => {
1000
- if (node.callee.type !== "MemberExpression") return void 0;
1001
- let target = node.callee.object;
1002
- while (target.type === "MemberExpression") target = target.object;
1003
- if (target.type !== "CallExpression") return void 0;
1004
- const callee = target.callee;
1005
- if (!("name" in callee && callee.name === "expect")) return void 0;
1006
- return target;
1007
- };
1008
1304
  const importedNames = (program) => {
1009
1305
  const names = /* @__PURE__ */ new Set();
1010
1306
  for (const statement of program.body) {
@@ -1014,7 +1310,7 @@ const importedNames = (program) => {
1014
1310
  return names;
1015
1311
  };
1016
1312
  const noBehaviourlessAssertion = defineRule({
1017
- meta: meta$16,
1313
+ meta: meta$17,
1018
1314
  create(context) {
1019
1315
  if (!TEST_FILE.test(context.filename)) return {};
1020
1316
  const imported = importedNames(context.sourceCode.ast);
@@ -1084,7 +1380,7 @@ const IO_SOURCE_TEST_NAME = "An in-source `import.meta.vitest` test block";
1084
1380
  const IO_SOURCE_TEST_EXPECTED = "the tests of a module whose own source calls an I/O binding to live outside it — a separate test file, or a composition test with a double at the port";
1085
1381
  const IO_SOURCE_TEST_ACTUAL = "this module calls a binding imported from a filesystem, process or network module and guards tests in-source with `import.meta.vitest`";
1086
1382
  const IO_SOURCE_TEST_FIX = "test the module from outside its own source — a separate test file or a composition test doubling the boundary — or, when an assertion merely restates a literal the module already computes, it is a change detector: delete it. The verdict here is the file's own imports and calls, never its name";
1087
- const meta$15 = {
1383
+ const meta$16 = {
1088
1384
  type: "problem",
1089
1385
  docs: { description: "Reports an in-source `import.meta.vitest` test block in a module whose own syntax shows a called, non-type import from a filesystem, process or network module. Judges only the in-source-test idiom — a module whose tests live in separate files is a no-op for this rule, whatever it imports." },
1090
1386
  schema: [],
@@ -1105,7 +1401,7 @@ const bindingBase = (callee) => {
1105
1401
  if (callee.type === "Identifier") return callee.name;
1106
1402
  };
1107
1403
  const noIoModuleInSourceTest = defineRule({
1108
- meta: meta$15,
1404
+ meta: meta$16,
1109
1405
  create(context) {
1110
1406
  /** Local binding name -> specifier it was (non-type) imported from. */
1111
1407
  const ioBindings = {};
@@ -1147,7 +1443,7 @@ const noIoModuleInSourceTest = defineRule({
1147
1443
  });
1148
1444
  //#endregion
1149
1445
  //#region src/rules/no-nested-quantification.config.ts
1150
- const MESSAGE$5 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1446
+ const MESSAGE$6 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1151
1447
  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([]))) });
1152
1448
  const ITERATOR_METHODS = /* @__PURE__ */ new Set([
1153
1449
  "every",
@@ -1166,15 +1462,15 @@ const ITERATOR_METHODS = /* @__PURE__ */ new Set([
1166
1462
  ]);
1167
1463
  const CONSTANT_POOL_ARBITRARIES = /* @__PURE__ */ new Set(["constant", "constantFrom"]);
1168
1464
  const FASTCHECK_NAMESPACES = /* @__PURE__ */ new Set(["fc"]);
1169
- const VIOLATION_NAME$2 = "quantification nested inside a property predicate";
1465
+ const VIOLATION_NAME$3 = "quantification nested inside a property predicate";
1170
1466
  const EXPECTED$2 = "per-case cost bounded by the draw, not by the draw times a second traversal — inspect a generated value with a fold whose body calls nothing, or move the inner quantifier into the generator so the shrinker can see it";
1171
1467
  const ACTUAL$2 = "the predicate iterates a value derived from a generated parameter and calls a free function inside that loop, so cost scales with the drawn size times whatever that call costs";
1172
1468
  const FIX$2 = "hoist the inner call out of the loop when its result does not vary per element; otherwise assert one drawn element per case and let numRuns supply the quantifier, or generate the pair and compare directly. If the cost is understood and accepted, add this file's basename to the rule's exempt option";
1173
- const meta$14 = {
1469
+ const meta$15 = {
1174
1470
  type: "problem",
1175
1471
  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." },
1176
1472
  schema: [Schema.toJsonSchemaDocument(Options$2).schema],
1177
- messages: { nestedQuantification: MESSAGE$5 }
1473
+ messages: { nestedQuantification: MESSAGE$6 }
1178
1474
  };
1179
1475
  //#endregion
1180
1476
  //#region src/rules/no-nested-quantification.ts
@@ -1229,11 +1525,17 @@ const isConstantPool = (element) => {
1229
1525
  };
1230
1526
  const boundedPoolIndices = (call) => {
1231
1527
  const bounded = /* @__PURE__ */ new Set();
1232
- const generators = call.arguments.find((argument) => argument.type === "ArrayExpression");
1233
- if (generators === void 0) return bounded;
1234
- generators.elements.forEach((element, index) => {
1235
- if (isConstantPool(element)) bounded.add(index);
1236
- });
1528
+ const options = call.arguments[1];
1529
+ if (options === void 0 || options.type !== "ObjectExpression") return bounded;
1530
+ for (const property of options.properties) {
1531
+ if (property.type !== "Property" || property.computed) continue;
1532
+ if (property.key.type !== "Identifier" || property.key.name !== "of") continue;
1533
+ if (property.value.type !== "ArrayExpression") return bounded;
1534
+ property.value.elements.forEach((element, index) => {
1535
+ if (isConstantPool(element)) bounded.add(index);
1536
+ });
1537
+ return bounded;
1538
+ }
1237
1539
  return bounded;
1238
1540
  };
1239
1541
  const drawnNamesIn = (predicate, bounded) => {
@@ -1241,12 +1543,12 @@ const drawnNamesIn = (predicate, bounded) => {
1241
1543
  const addFrom = (value) => {
1242
1544
  for (const name of identifiersIn(value)) drawn.add(name);
1243
1545
  };
1244
- const [first, ...rest] = predicate.params;
1245
- if (isNode$3(first) && first.type === "ArrayPattern") first.elements.forEach((element, index) => {
1546
+ const [, values, ...rest] = predicate.params;
1547
+ if (isNode$3(values) && values.type === "ArrayPattern") values.elements.forEach((element, index) => {
1246
1548
  if (bounded.has(index)) return;
1247
1549
  addFrom(element);
1248
1550
  });
1249
- else addFrom(first);
1551
+ else addFrom(values);
1250
1552
  addFrom(rest);
1251
1553
  visit(predicate.body, (node) => {
1252
1554
  if (node.type !== "VariableDeclarator") return;
@@ -1306,7 +1608,7 @@ const check = (context, call, predicate) => {
1306
1608
  node: iteration.node,
1307
1609
  messageId: "nestedQuantification",
1308
1610
  data: {
1309
- name: VIOLATION_NAME$2,
1611
+ name: VIOLATION_NAME$3,
1310
1612
  expected: EXPECTED$2,
1311
1613
  actual: ACTUAL$2,
1312
1614
  fix: FIX$2
@@ -1321,7 +1623,7 @@ const check = (context, call, predicate) => {
1321
1623
  * and excluded; DISCHARGED_BY in the suite pins the unbounded recipe shape.
1322
1624
  */
1323
1625
  const noNestedQuantification = defineRule({
1324
- meta: meta$14,
1626
+ meta: meta$15,
1325
1627
  create(context) {
1326
1628
  const options = Schema.decodeUnknownSync(Options$2)(context.options[0] ?? {});
1327
1629
  const exempt = new Set(options.exempt);
@@ -1342,11 +1644,11 @@ const NO_LAYER_IN_FEATURE_NAME = "a *.integration.test.ts feature with no enviro
1342
1644
  const NO_LAYER_IN_FEATURE_EXPECTED = "a Feature builder chained with .withLayer(layer) or .withScenarioLayer(layer)";
1343
1645
  const NO_LAYER_IN_FEATURE_ACTUAL = "a Feature(...) call without .withLayer or .withScenarioLayer";
1344
1646
  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)).";
1345
- const meta$13 = {
1647
+ const meta$14 = {
1346
1648
  type: "problem",
1347
1649
  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." },
1348
1650
  schema: [],
1349
- messages: { noLayerInFeature: MESSAGE$7 }
1651
+ messages: { noLayerInFeature: MESSAGE$8 }
1350
1652
  };
1351
1653
  //#endregion
1352
1654
  //#region src/rules/no-pseudo-gherkin-unit-tests.ts
@@ -1354,7 +1656,6 @@ const LAYER_METHODS = {
1354
1656
  withLayer: true,
1355
1657
  withScenarioLayer: true
1356
1658
  };
1357
- const isBehaviourTest = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
1358
1659
  const hasLayerInChain = (callNode) => {
1359
1660
  let current = callNode;
1360
1661
  while (current.type === "CallExpression") {
@@ -1377,9 +1678,9 @@ const findRootFeatureCall = (callNode) => {
1377
1678
  return null;
1378
1679
  };
1379
1680
  const noPseudoGherkinUnitTests = defineRule({
1380
- meta: meta$13,
1681
+ meta: meta$14,
1381
1682
  create(context) {
1382
- if (!isBehaviourTest(basenameOf(context.filename))) return {};
1683
+ if (!isBehaviourBasename(basenameOf(context.filename))) return {};
1383
1684
  return { CallExpression(node) {
1384
1685
  if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier") {
1385
1686
  if (node.callee.property.name === "body") {
@@ -1401,16 +1702,16 @@ const noPseudoGherkinUnitTests = defineRule({
1401
1702
  });
1402
1703
  //#endregion
1403
1704
  //#region src/rules/no-silent-return.config.ts
1404
- const MESSAGE$4 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1405
- const meta$12 = {
1705
+ const MESSAGE$5 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1706
+ const meta$13 = {
1406
1707
  type: "problem",
1407
- docs: { description: "Property predicates (it.prop / it.effect.prop from @effect/vitest) must return a boolean verdict on every code path. fast-check counts undefined as success, so a bare return, a non-boolean return, or falling off the end of the body is a silent pass. Opaque values (identifiers, member expressions, calls) are trusted to be boolean; literals and operators are checked." },
1708
+ 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." },
1408
1709
  schema: [],
1409
1710
  messages: {
1410
- bareReturn: MESSAGE$4,
1411
- nonBooleanReturn: MESSAGE$4,
1412
- missingReturn: MESSAGE$4,
1413
- nonBooleanBody: MESSAGE$4
1711
+ bareReturn: MESSAGE$5,
1712
+ nonBooleanReturn: MESSAGE$5,
1713
+ missingReturn: MESSAGE$5,
1714
+ nonBooleanBody: MESSAGE$5
1414
1715
  }
1415
1716
  };
1416
1717
  //#endregion
@@ -1550,7 +1851,7 @@ const checkFn = (context, fn) => {
1550
1851
  if (!Array$1.last(body.body).pipe(Option.exists(pathExits))) report(context, fn, "missingReturn", "the predicate can fall off the end without returning — undefined is a silent pass");
1551
1852
  };
1552
1853
  const noSilentReturn = defineRule({
1553
- meta: meta$12,
1854
+ meta: meta$13,
1554
1855
  create(context) {
1555
1856
  return { CallExpression(node) {
1556
1857
  if (!isPropCallee(node.callee)) return;
@@ -1579,21 +1880,21 @@ const SCHEMA_TEST_DETAIL = {
1579
1880
  actual: "an authored *.schema.test.ts restating generated coverage",
1580
1881
  fix: "delete it. The generated laws already state round-trip identity and encode stability. What they cannot state is rejection — every input they draw comes from the arbitrary the schema itself supplies — so a refusal belongs in an in-source if (import.meta.vitest) block in the schema file, never here"
1581
1882
  };
1582
- const meta$11 = {
1883
+ const meta$12 = {
1583
1884
  type: "problem",
1584
1885
  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." },
1585
1886
  schema: [Schema.toJsonSchemaDocument(Options$1).schema],
1586
1887
  messages: {
1587
- testFileInSrc: MESSAGE$7,
1588
- schemaTestInSrc: MESSAGE$7,
1589
- propertyTestOutsideTestsDir: MESSAGE$7
1888
+ testFileInSrc: MESSAGE$8,
1889
+ schemaTestInSrc: MESSAGE$8,
1890
+ propertyTestOutsideTestsDir: MESSAGE$8
1590
1891
  }
1591
1892
  };
1592
1893
  //#endregion
1593
1894
  //#region src/rules/no-test-file-in-src.ts
1594
1895
  const violationOf = (basename, isPropertyTest, dir) => basename.endsWith(".schema.test.ts") ? ["schemaTestInSrc", SCHEMA_TEST_DETAIL] : isPropertyTest ? ["propertyTestOutsideTestsDir", propertyTestLocationDetail(dir)] : ["testFileInSrc", testFileInSrcDetail(dir)];
1595
1896
  const noTestFileInSrc = defineRule({
1596
- meta: meta$11,
1897
+ meta: meta$12,
1597
1898
  create(context) {
1598
1899
  const { sanctionedDirs } = Schema.decodeUnknownSync(Options$1)(context.options[0] ?? {});
1599
1900
  const basename = basenameOf(context.filename);
@@ -1641,12 +1942,13 @@ const PREDICATE_SYMBOLS = /* @__PURE__ */ new Set([
1641
1942
  "∘",
1642
1943
  "∩",
1643
1944
  "∪",
1644
- "⊥"
1945
+ "⊥",
1946
+ "⊨"
1645
1947
  ]);
1646
1948
  const NULLARY_PREDICATE_SYMBOLS = /* @__PURE__ */ new Set(["⊥"]);
1647
1949
  const PASCAL_CASE = /^[A-Z][a-z][a-zA-Z0-9]*$/;
1648
1950
  const DAMP_WORDS = /When|Should|Given|Then|Otherwise|After|Before/;
1649
- const meta$10 = {
1951
+ const meta$11 = {
1650
1952
  type: "suggestion",
1651
1953
  docs: { description: "Enforce a complete formal-specification name for property-based tests (it.prop / it.effect.prop). Format: [ScopeSymbol][binder]_[Domain]_[PredicateSymbol][operand] (e.g., ∀x_DecodeEncode_=x, ∀l_Filter_⊆Input, →Shipped_Cancel_⊥Allowed). Both the quantifier and the predicate must carry an operand — a bare symbol specifies nothing." },
1652
1954
  schema: [],
@@ -1656,7 +1958,7 @@ const meta$10 = {
1656
1958
  incompleteScope: "Expected: a bound variable after the quantifier \"{{symbol}}\" in scope segment \"{{scope}}\" (e.g., ∀x, ∀order, ∃e, →Shipped). A property quantifies over a named input drawn from a generator; name it. A lone quantifier binds nothing and specifies no domain.",
1657
1959
  emptyDomain: "Expected: a non-empty PascalCase domain between the two underscores in \"{{actual}}\". If this isn't a universal invariant, delete the test. Otherwise name the thing under test, e.g., ∀x_DecodeEncode_=x.",
1658
1960
  domainLeaksDAMP: "Expected: an invariant domain, not scenario language. Actual: domain \"{{domain}}\" contains \"{{word}}\" — that describes one case, not a universal law. Delete this test. It is not a property. Find the actual invariant and write that instead.",
1659
- invalidPredicateSymbol: "Expected: a relation symbol (≡ ≠ = ≤ ≥ ∈ ⊆ ⊇ → ¬ ∘ ∩ ∪ ⊥) starting the last segment of \"{{actual}}\". Actual: it ends with \"{{firstChar}}\". If this isn't a universal invariant, delete the test. Otherwise relate the output: = / ≡ (roundtrip or oracle), ⊆ / ∈ (invariant), ≠ (distinctness), ⊥ (impossibility).",
1961
+ invalidPredicateSymbol: "Expected: a relation symbol (≡ ≠ = ≤ ≥ ∈ ⊆ ⊇ → ¬ ∘ ∩ ∪ ⊥ ⊨) starting the last segment of \"{{actual}}\". Actual: it ends with \"{{firstChar}}\". If this isn't a universal invariant, delete the test. Otherwise relate the output: = / ≡ (roundtrip or oracle), ⊆ / ∈ (invariant), ≠ (distinctness), ⊨ (satisfies a declared model), ⊥ (impossibility).",
1660
1962
  incompletePredicate: "Expected: an operand after the relation symbol \"{{symbol}}\" in predicate \"{{predicate}}\", naming what the output is related to: =x / ≡Oracle (roundtrip or reference), ⊆Input / ∈Ignored (invariant), ≠Zero (distinctness), ⊥Cancellable (impossibility — name the outcome that cannot occur). A bare symbol relates the output to nothing and so asserts no property."
1661
1963
  }
1662
1964
  };
@@ -1690,7 +1992,7 @@ const parseSegments = (name) => {
1690
1992
  };
1691
1993
  };
1692
1994
  const pbtNaming = defineRule({
1693
- meta: meta$10,
1995
+ meta: meta$11,
1694
1996
  create(context) {
1695
1997
  return { CallExpression(node) {
1696
1998
  if (!isPropCall(node)) return;
@@ -1782,8 +2084,8 @@ const pbtNaming = defineRule({
1782
2084
  });
1783
2085
  //#endregion
1784
2086
  //#region src/rules/prop-arbitrary-schema-origin.config.ts
1785
- const MESSAGE$3 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1786
- const VIOLATION_NAME$1 = "a hand-built fast-check arbitrary with no schema underneath";
2087
+ const MESSAGE$4 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2088
+ const VIOLATION_NAME$2 = "a hand-built fast-check arbitrary with no schema underneath";
1787
2089
  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";
1788
2090
  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";
1789
2091
  const FIX$1 = "delete the block, or grow the schema the input needs and derive the arbitrary from it — Arbitrary.schema(schema) is the sanctioned derivation; nothing rewrites a hand-built generator into a property";
@@ -1798,13 +2100,13 @@ const SCHEMA_NAMESPACE_NAMES = {
1798
2100
  };
1799
2101
  const FASTCHECK_NAMESPACE_NAMES = { FastCheck: true };
1800
2102
  const COMBINATOR_CALLEES = { pipe: true };
1801
- const meta$9 = {
2103
+ const meta$10 = {
1802
2104
  type: "problem",
1803
2105
  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." },
1804
2106
  schema: [],
1805
2107
  messages: {
1806
- handBuiltArbitrary: MESSAGE$3,
1807
- stockDerivedArbitrary: MESSAGE$3
2108
+ handBuiltArbitrary: MESSAGE$4,
2109
+ stockDerivedArbitrary: MESSAGE$4
1808
2110
  }
1809
2111
  };
1810
2112
  //#endregion
@@ -2062,26 +2364,49 @@ const checkArbitraryFactory = (provenance, context, call) => {
2062
2364
  }
2063
2365
  });
2064
2366
  };
2065
- const checkPropCall$1 = (provenance, context, call) => {
2066
- const arbitraries = call.arguments.find((argument) => argument.type === "ArrayExpression");
2067
- if (arbitraries === void 0) return;
2068
- for (const element of arbitraries.elements) {
2367
+ const reportHandBuilt = (provenance, context, node) => {
2368
+ if (provenance.verdictOf(node, 0) !== "handBuilt") return;
2369
+ context.report({
2370
+ node,
2371
+ messageId: "handBuiltArbitrary",
2372
+ data: {
2373
+ name: VIOLATION_NAME$2,
2374
+ expected: EXPECTED$1,
2375
+ actual: ACTUAL$1,
2376
+ fix: FIX$1
2377
+ }
2378
+ });
2379
+ };
2380
+ const checkArrayOf = (provenance, context, of) => {
2381
+ for (const element of of.elements) {
2069
2382
  if (element === null) continue;
2070
- if (provenance.verdictOf(element, 0) !== "handBuilt") continue;
2071
- context.report({
2072
- node: element,
2073
- messageId: "handBuiltArbitrary",
2074
- data: {
2075
- name: VIOLATION_NAME$1,
2076
- expected: EXPECTED$1,
2077
- actual: ACTUAL$1,
2078
- fix: FIX$1
2079
- }
2080
- });
2383
+ reportHandBuilt(provenance, context, element);
2384
+ }
2385
+ };
2386
+ const checkRecordOf = (provenance, context, of) => {
2387
+ for (const member of of.properties) {
2388
+ if (member.type !== "Property" || member.computed) continue;
2389
+ reportHandBuilt(provenance, context, member.value);
2390
+ }
2391
+ };
2392
+ const checkPropCall$1 = (provenance, context, call) => {
2393
+ const options = call.arguments[1];
2394
+ if (options === void 0 || options.type !== "ObjectExpression") return;
2395
+ for (const property of options.properties) {
2396
+ if (property.type !== "Property" || property.computed) continue;
2397
+ if (property.key.type !== "Identifier" || property.key.name !== "of") continue;
2398
+ if (property.value.type === "ArrayExpression") {
2399
+ checkArrayOf(provenance, context, property.value);
2400
+ return;
2401
+ }
2402
+ if (property.value.type === "ObjectExpression") {
2403
+ checkRecordOf(provenance, context, property.value);
2404
+ return;
2405
+ }
2081
2406
  }
2082
2407
  };
2083
2408
  const propArbitrarySchemaOrigin = defineRule({
2084
- meta: meta$9,
2409
+ meta: meta$10,
2085
2410
  create(context) {
2086
2411
  const provenance = new Provenance(context.sourceCode.getScope);
2087
2412
  const collect = (value, out) => {
@@ -2140,16 +2465,16 @@ const propArbitrarySchemaOrigin = defineRule({
2140
2465
  });
2141
2466
  //#endregion
2142
2467
  //#region src/rules/prop-fixture-schema-origin.config.ts
2143
- const MESSAGE$2 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2144
- const VIOLATION_NAME = "a hand-rolled recursive schema fixture inside a test block";
2468
+ const MESSAGE$3 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2469
+ const VIOLATION_NAME$1 = "a hand-rolled recursive schema fixture inside a test block";
2145
2470
  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";
2146
2471
  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";
2147
2472
  const FIX = "hoist the members into a named builder the annotated and counterfactual fixtures share, or declare the recursion point's generation in its annotation";
2148
- const meta$8 = {
2473
+ const meta$9 = {
2149
2474
  type: "problem",
2150
2475
  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." },
2151
2476
  schema: [],
2152
- messages: { handRolledRecursiveFixture: MESSAGE$2 }
2477
+ messages: { handRolledRecursiveFixture: MESSAGE$3 }
2153
2478
  };
2154
2479
  //#endregion
2155
2480
  //#region src/rules/prop-fixture-schema-origin.ts
@@ -2323,7 +2648,7 @@ const isNamedBuilderOrigin = (union, getScope) => {
2323
2648
  return false;
2324
2649
  };
2325
2650
  const propFixtureSchemaOrigin = defineRule({
2326
- meta: meta$8,
2651
+ meta: meta$9,
2327
2652
  create(context) {
2328
2653
  const getScope = context.sourceCode.getScope;
2329
2654
  return { CallExpression(node) {
@@ -2340,7 +2665,7 @@ const propFixtureSchemaOrigin = defineRule({
2340
2665
  node,
2341
2666
  messageId: "handRolledRecursiveFixture",
2342
2667
  data: {
2343
- name: VIOLATION_NAME,
2668
+ name: VIOLATION_NAME$1,
2344
2669
  expected: EXPECTED,
2345
2670
  actual: ACTUAL,
2346
2671
  fix: FIX
@@ -2351,7 +2676,7 @@ const propFixtureSchemaOrigin = defineRule({
2351
2676
  });
2352
2677
  //#endregion
2353
2678
  //#region src/rules/prop-generated-law-duplicate.config.ts
2354
- const MESSAGE$1 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2679
+ const MESSAGE$2 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2355
2680
  const COMPILER_NAME = "a runtime probe of a compile-time guarantee";
2356
2681
  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";
2357
2682
  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";
@@ -2360,13 +2685,13 @@ const NO_FUNCTION_NAME = "a property that tests no function";
2360
2685
  const NO_FUNCTION_EXPECTED = "every in-source property exercises domain logic — at least one call to a module-local function that is not a schema codec accessor — because a predicate that only feeds values through encode or decode tests the schema declaration, not code, and decode acceptance and refusal alike are generated or declared elsewhere";
2361
2686
  const NO_FUNCTION_ACTUAL = "no call in the predicate reaches module code — every call is a codec accessor, a schema wrapper, or an iteration combinator — so the property cannot fail unless the declaration it restates changes meaning";
2362
2687
  const NO_FUNCTION_FIX = "delete the prop; test the function that owns the decision — the workflow or a private helper in this module — with its input derived from a domain schema";
2363
- const meta$7 = {
2688
+ const meta$8 = {
2364
2689
  type: "problem",
2365
2690
  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." },
2366
2691
  schema: [],
2367
2692
  messages: {
2368
- compilerDuplicate: MESSAGE$1,
2369
- noDomainFunction: MESSAGE$1
2693
+ compilerDuplicate: MESSAGE$2,
2694
+ noDomainFunction: MESSAGE$2
2370
2695
  }
2371
2696
  };
2372
2697
  //#endregion
@@ -2433,6 +2758,16 @@ const NEUTRAL_METHODS = {
2433
2758
  match: true,
2434
2759
  gen: true
2435
2760
  };
2761
+ /**
2762
+ * The fork's lawful `it.prop(name, { of, subject }, holds)` hands the function
2763
+ * under test to `holds` as its first parameter. Calling that parameter is
2764
+ * calling the subject, so it reaches this module's code by construction — even
2765
+ * when the parameter name would otherwise read as a codec accessor.
2766
+ */
2767
+ const subjectParameterOf = (predicate) => {
2768
+ const first = predicate.params[0];
2769
+ return first !== void 0 && first.type === "Identifier" ? first.name : void 0;
2770
+ };
2436
2771
  const isNode = (value) => value !== null && typeof value === "object" && "type" in value;
2437
2772
  const mentionsImportMetaVitest = (value) => {
2438
2773
  if (Array.isArray(value)) return value.some((item) => mentionsImportMetaVitest(item));
@@ -2444,9 +2779,9 @@ const mentionsImportMetaVitest = (value) => {
2444
2779
  }
2445
2780
  return false;
2446
2781
  };
2447
- const collectPredicateShape = (provenance, node, shape) => {
2782
+ const collectPredicateShape = (provenance, node, shape, subjectParameter) => {
2448
2783
  if (Array.isArray(node)) {
2449
- for (const item of node) collectPredicateShape(provenance, item, shape);
2784
+ for (const item of node) collectPredicateShape(provenance, item, shape, subjectParameter);
2450
2785
  return;
2451
2786
  }
2452
2787
  if (!isNode(node)) return;
@@ -2464,12 +2799,12 @@ const collectPredicateShape = (provenance, node, shape) => {
2464
2799
  if (provenance.classifyCall(name, node) === "domain" || receiverIsLocal) shape.callsDomainFunction = true;
2465
2800
  }
2466
2801
  } else if (callee.type === "Identifier") {
2467
- if (CODEC_ACCESSORS[callee.name] !== true && NEUTRAL_METHODS[callee.name] !== true && provenance.classifyCall(callee.name, node) === "domain") shape.callsDomainFunction = true;
2802
+ if (subjectParameter !== void 0 && callee.name === subjectParameter || CODEC_ACCESSORS[callee.name] !== true && NEUTRAL_METHODS[callee.name] !== true && provenance.classifyCall(callee.name, node) === "domain") shape.callsDomainFunction = true;
2468
2803
  }
2469
2804
  }
2470
2805
  for (const [key, child] of Object.entries(node)) {
2471
2806
  if (key === "parent") continue;
2472
- collectPredicateShape(provenance, child, shape);
2807
+ collectPredicateShape(provenance, child, shape, subjectParameter);
2473
2808
  }
2474
2809
  };
2475
2810
  const checkPropCall = (provenance, context, call) => {
@@ -2479,7 +2814,7 @@ const checkPropCall = (provenance, context, call) => {
2479
2814
  usesCompilerProbe: false,
2480
2815
  callsDomainFunction: false
2481
2816
  };
2482
- collectPredicateShape(provenance, predicate.body, shape);
2817
+ collectPredicateShape(provenance, predicate.body, shape, subjectParameterOf(predicate));
2483
2818
  if (shape.usesCompilerProbe) {
2484
2819
  context.report({
2485
2820
  node: call,
@@ -2506,7 +2841,7 @@ const checkPropCall = (provenance, context, call) => {
2506
2841
  });
2507
2842
  };
2508
2843
  const propGeneratedLawDuplicate = defineRule({
2509
- meta: meta$7,
2844
+ meta: meta$8,
2510
2845
  create(context) {
2511
2846
  const provenance = new Provenance(context.sourceCode.getScope);
2512
2847
  const collect = (value, out) => {
@@ -2549,17 +2884,17 @@ const propGeneratedLawDuplicate = defineRule({
2549
2884
  //#endregion
2550
2885
  //#region src/rules/property-file-purity.config.ts
2551
2886
  const PROPERTY_TEST_SUFFIX = ".property.test.ts";
2552
- const MESSAGE = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2553
- const meta$6 = {
2887
+ const MESSAGE$1 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2888
+ const meta$7 = {
2554
2889
  type: "problem",
2555
2890
  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." },
2556
2891
  schema: [],
2557
2892
  messages: {
2558
- plainIt: MESSAGE,
2559
- plainEffectIt: MESSAGE,
2560
- rawFastCheck: MESSAGE,
2561
- fastCheckImport: MESSAGE,
2562
- propCall: MESSAGE
2893
+ plainIt: MESSAGE$1,
2894
+ plainEffectIt: MESSAGE$1,
2895
+ rawFastCheck: MESSAGE$1,
2896
+ fastCheckImport: MESSAGE$1,
2897
+ propCall: MESSAGE$1
2563
2898
  }
2564
2899
  };
2565
2900
  //#endregion
@@ -2574,9 +2909,9 @@ const PROPERTY_FILE_KIND = {
2574
2909
  suffix: PROPERTY_TEST_SUFFIX,
2575
2910
  plainExpected: "it.prop(...) or it.effect.prop(...) — property files never mix with scenario tests",
2576
2911
  plainFix: "move the scenario test to a plain *.test.ts file, or rewrite it as a property with arbitraries and a boolean-returning predicate",
2577
- rawFcExpected: "it.prop(...) or it.effect.prop(...) from @effect/vitest",
2912
+ rawFcExpected: "it.prop(...) or it.effect.prop(...) from @systemfsoftware/vitest",
2578
2913
  rawFcActualSuffix: "bypasses the vitest/Effect integration",
2579
- rawFcFix: "rewrite as it.prop(name, [arbitraries], predicate) returning a boolean; fc.* stays for building arbitraries (fc.pre, fc.stringMatching, ...)"
2914
+ rawFcFix: "rewrite as it.prop(name, { of, subject, runs }, holds) returning a boolean; fc.* stays for building arbitraries (fc.pre, fc.stringMatching, ...)"
2580
2915
  };
2581
2916
  const DIFFERENTIAL_FILE_KIND = {
2582
2917
  suffix: DIFFERENTIAL_SUFFIX,
@@ -2666,7 +3001,7 @@ const createScenarioFileVisitors = (context) => ({
2666
3001
  });
2667
3002
  const kindOf = (filename) => filename.endsWith(".differential.test.ts") ? DIFFERENTIAL_FILE_KIND : PROPERTY_FILE_KIND;
2668
3003
  const propertyFilePurity = defineRule({
2669
- meta: meta$6,
3004
+ meta: meta$7,
2670
3005
  create(context) {
2671
3006
  if (!isTestFile(basenameOf(context.filename))) return {};
2672
3007
  if (context.filename.endsWith(".property.test.ts") || context.filename.endsWith(".differential.test.ts")) return createPropertyFileVisitors(context, kindOf(context.filename));
@@ -2682,12 +3017,12 @@ const UNSANCTIONED_CELL_FIX = "rename it <stem>.workflow.property.test.ts beside
2682
3017
  const MISSING_CELL_TEST_EXPECTED = "a test for every cell suffix the consumer lists in cellsRequiringTest";
2683
3018
  const MISSING_CELL_TEST_ACTUAL = "a declared cell whose own module carries no `if (import.meta.vitest)` block";
2684
3019
  const MISSING_CELL_TEST_FIX = "add an `if (import.meta.vitest)` block to this module, or drop this cell from cellsRequiringTest and cover it with a colocated test in a sanctioned test directory — the rule reads the file it is given, so a sibling test file is invisible to it and a declared cell must satisfy the requirement from its own source";
2685
- const meta$5 = {
3020
+ const meta$6 = {
2686
3021
  type: "problem",
2687
3022
  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." },
2688
3023
  schema: [Schema.toJsonSchemaDocument(Options).schema],
2689
3024
  messages: {
2690
- unsanctionedCell: MESSAGE$7,
3025
+ unsanctionedCell: MESSAGE$8,
2691
3026
  missingCellTest: ABSENCE_MESSAGE
2692
3027
  }
2693
3028
  };
@@ -2695,7 +3030,7 @@ const meta$5 = {
2695
3030
  //#region src/rules/src-property-test-cell.ts
2696
3031
  const carriesInSourceBlock = (body) => body.some((statement) => statement.type === "IfStatement" && isVitestGuard(statement.test));
2697
3032
  const srcPropertyTestCell = defineRule({
2698
- meta: meta$5,
3033
+ meta: meta$6,
2699
3034
  create(context) {
2700
3035
  const { cellsRequiringTest } = Schema.decodeUnknownSync(Options)(context.options[0] ?? {});
2701
3036
  const filename = context.filename;
@@ -2748,7 +3083,7 @@ const testFileOutsideTestsDir = defineRule({
2748
3083
  type: "problem",
2749
3084
  docs: { description: "Test files outside src/ must live under a tests/ directory; free-standing test files have no sanctioned home." },
2750
3085
  schema: [],
2751
- messages: { strayTestFile: MESSAGE$7 }
3086
+ messages: { strayTestFile: MESSAGE$8 }
2752
3087
  },
2753
3088
  create(context) {
2754
3089
  if (isUnderSrc(context.filename)) return {};
@@ -2770,24 +3105,25 @@ const testFileOutsideTestsDir = defineRule({
2770
3105
  });
2771
3106
  //#endregion
2772
3107
  //#region src/rules/test-suffix-outside-src.config.ts
2773
- const UNSANCTIONED_SUFFIX_EXPECTED = "*.integration.test.ts, *.differential.test.ts, or *.trace.test.ts";
3108
+ const UNSANCTIONED_SUFFIX_EXPECTED = "*.integration.test.ts, *.contract.test.ts, *.differential.test.ts, *.trace.test.ts, or *.conformance.test.ts";
2774
3109
  const UNSANCTIONED_SUFFIX_ACTUAL = "another test suffix outside src/";
2775
- const UNSANCTIONED_SUFFIX_FIX = "public surface -> *.integration.test.ts; parity -> *.differential.test.ts; span graph -> *.trace.test.ts; pure-cell property -> in-source; restated literal -> delete";
3110
+ const UNSANCTIONED_SUFFIX_FIX = "public surface -> *.integration.test.ts; real-oracle medium -> *.contract.test.ts (contract lane); parity -> *.differential.test.ts; span graph -> *.trace.test.ts; concurrent or stateful behaviour -> *.conformance.test.ts; pure-cell property -> in-source; restated literal -> delete";
2776
3111
  //#endregion
2777
3112
  //#region src/rules/test-suffix-outside-src.ts
2778
3113
  const testSuffixOutsideSrc = defineRule({
2779
3114
  meta: {
2780
3115
  type: "problem",
2781
- docs: { description: "Outside src/, a test file ends .integration.test.ts, .differential.test.ts, or .trace.test.ts." },
3116
+ 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." },
2782
3117
  schema: [],
2783
- messages: { unsanctionedSuffix: MESSAGE$7 }
3118
+ messages: { unsanctionedSuffix: MESSAGE$8 }
2784
3119
  },
2785
3120
  create(context) {
2786
3121
  const filename = context.filename;
2787
3122
  if (isUnderSrc(filename)) return {};
3123
+ if (isRawVitestPackage(filename)) return {};
2788
3124
  const basename = basenameOf(filename);
2789
3125
  if (!isTestFile(basename)) return {};
2790
- if (basename.endsWith(".integration.test.ts") || basename.endsWith(".differential.test.ts") || basename.endsWith(".trace.test.ts")) return {};
3126
+ if (isBehaviourBasename(basename) || basename.endsWith(".differential.test.ts") || basename.endsWith(".trace.test.ts") || basename.endsWith(".conformance.test.ts")) return {};
2791
3127
  return { Program(node) {
2792
3128
  context.report({
2793
3129
  node,
@@ -2814,7 +3150,7 @@ const testsDirHelpersInFixtures = defineRule({
2814
3150
  type: "problem",
2815
3151
  docs: { description: "Under tests/, the only non-test modules are helpers and fixtures, and they live inside tests/__fixtures__/." },
2816
3152
  schema: [],
2817
- messages: { helperOutsideFixtures: MESSAGE$7 }
3153
+ messages: { helperOutsideFixtures: MESSAGE$8 }
2818
3154
  },
2819
3155
  create(context) {
2820
3156
  const filename = context.filename;
@@ -2843,11 +3179,11 @@ const testsDirHelpersInFixtures = defineRule({
2843
3179
  const REACH_IN_EXPECTED = "a package name or subpath, or a sibling helper under the test tree";
2844
3180
  const REACH_IN_ACTUAL = "a relative import that reaches src or climbs into an internal folder";
2845
3181
  const REACH_IN_FIX = "rewrite onto the published package name when the binding is public. Delete the test when the subject is an internal";
2846
- const meta$1 = {
3182
+ const meta$2 = {
2847
3183
  type: "problem",
2848
3184
  docs: { description: "Forbid package-level tests from relative-importing src or climbing into an internal folder" },
2849
3185
  schema: [],
2850
- messages: { sourceReachIn: MESSAGE$7 }
3186
+ messages: { sourceReachIn: MESSAGE$8 }
2851
3187
  };
2852
3188
  //#endregion
2853
3189
  //#region src/rules/tests-import-public-api.ts
@@ -2865,7 +3201,7 @@ const specifierOf = (node) => {
2865
3201
  if (node.type === "Literal" && typeof node.value === "string") return node.value;
2866
3202
  };
2867
3203
  const testsImportPublicApi = defineRule({
2868
- meta: meta$1,
3204
+ meta: meta$2,
2869
3205
  create(context) {
2870
3206
  if (!isInTestsImportScope(context.filename)) return {};
2871
3207
  const reportIfForbidden = (sourceNode) => {
@@ -2923,14 +3259,14 @@ const HTTP_TERMINATION_FIX = "replace the HTTP assertion with a relation over th
2923
3259
  const RAW_EMIT_EXPECTED = "spans declared once with Span.declare({ id, name, attrs }) inside the traced cell; the spec observes the decoded graph";
2924
3260
  const RAW_EMIT_ACTUAL = "a raw span emit call inside a trace spec";
2925
3261
  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";
2926
- const meta = {
3262
+ const meta$1 = {
2927
3263
  type: "problem",
2928
3264
  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." },
2929
3265
  schema: [],
2930
3266
  messages: {
2931
- missingHarnessImport: MESSAGE$7,
2932
- httpTermination: MESSAGE$7,
2933
- rawEmitCall: MESSAGE$7
3267
+ missingHarnessImport: MESSAGE$8,
3268
+ httpTermination: MESSAGE$8,
3269
+ rawEmitCall: MESSAGE$8
2934
3270
  }
2935
3271
  };
2936
3272
  //#endregion
@@ -3097,7 +3433,7 @@ const reportHttpTermination = (context, node) => {
3097
3433
  });
3098
3434
  };
3099
3435
  const traceTestRequiresTaxonomy = defineRule({
3100
- meta,
3436
+ meta: meta$1,
3101
3437
  create(context) {
3102
3438
  if (!context.filename.endsWith(".trace.test.ts")) return {};
3103
3439
  let hasHarnessImport = false;
@@ -3126,6 +3462,66 @@ const traceTestRequiresTaxonomy = defineRule({
3126
3462
  }
3127
3463
  });
3128
3464
  //#endregion
3465
+ //#region src/rules/vitest-from-systemfsoftware-vitest.config.ts
3466
+ const MESSAGE = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
3467
+ const VIOLATION_NAME = "a value import from a foreign vitest runner";
3468
+ const VIOLATION_EXPECTED = "every test value imported from @systemfsoftware/vitest, which re-exports all of Vitest";
3469
+ const VIOLATION_ACTUAL = "a named, namespace, default, side-effect or dynamic value import from vitest or @effect/vitest";
3470
+ const VIOLATION_FIX = "import it from @systemfsoftware/vitest instead of vitest or @effect/vitest";
3471
+ const meta = {
3472
+ type: "problem",
3473
+ 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." },
3474
+ schema: [],
3475
+ messages: { vitestImport: MESSAGE }
3476
+ };
3477
+ //#endregion
3478
+ //#region src/rules/vitest-from-systemfsoftware-vitest.ts
3479
+ const isTypeOnly = (node) => {
3480
+ if (node.importKind === "type") return true;
3481
+ if (node.specifiers.length === 0) return false;
3482
+ return node.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type");
3483
+ };
3484
+ const isForeignVitestLiteral = (node) => node.type === "Literal" && typeof node.value === "string" && FOREIGN_VITEST_SOURCES[node.value] === true;
3485
+ const vitestFromSystemfsoftwareVitest = defineRule({
3486
+ meta,
3487
+ create(context) {
3488
+ if (isRawVitestPackage(context.filename)) return {};
3489
+ const report = (node) => {
3490
+ context.report({
3491
+ node,
3492
+ messageId: "vitestImport",
3493
+ data: {
3494
+ name: VIOLATION_NAME,
3495
+ expected: VIOLATION_EXPECTED,
3496
+ actual: VIOLATION_ACTUAL,
3497
+ fix: VIOLATION_FIX
3498
+ }
3499
+ });
3500
+ };
3501
+ return {
3502
+ ImportDeclaration(node) {
3503
+ if (FOREIGN_VITEST_SOURCES[node.source.value] !== true) return;
3504
+ if (isTypeOnly(node)) return;
3505
+ report(node);
3506
+ },
3507
+ ImportExpression(node) {
3508
+ if (!isForeignVitestLiteral(node.source)) return;
3509
+ report(node);
3510
+ },
3511
+ ExportAllDeclaration(node) {
3512
+ if (node.exportKind === "type") return;
3513
+ if (FOREIGN_VITEST_SOURCES[node.source.value] !== true) return;
3514
+ report(node);
3515
+ },
3516
+ ExportNamedDeclaration(node) {
3517
+ if (node.source === null || node.exportKind === "type") return;
3518
+ if (FOREIGN_VITEST_SOURCES[node.source.value] !== true) return;
3519
+ report(node);
3520
+ }
3521
+ };
3522
+ }
3523
+ });
3524
+ //#endregion
3129
3525
  //#region src/index.ts
3130
3526
  const PLUGIN_NAME = "@systemfsoftware/oxlint-plugin-test-discipline";
3131
3527
  const rule = (name) => `${PLUGIN_NAME}/${name}`;
@@ -3150,9 +3546,12 @@ const recommendedRules = {
3150
3546
  [rule("behaviour-exercises-use-case")]: "error",
3151
3547
  [rule("behaviour-one-feature-per-file")]: "error",
3152
3548
  [rule("tests-dir-helpers-in-fixtures")]: "error",
3549
+ [rule("vitest-from-systemfsoftware-vitest")]: "error",
3153
3550
  [rule("no-io-module-in-source-test")]: "error",
3154
3551
  [rule("tests-import-public-api")]: "error",
3155
3552
  [rule("differential-test-requires-harness")]: "error",
3553
+ [rule("conformance-test-requires-harness")]: "error",
3554
+ [rule("model-fixture-imports-subject")]: "error",
3156
3555
  [rule("no-pseudo-gherkin-unit-tests")]: "error",
3157
3556
  [rule("ban-raw-span-name-emit")]: "error",
3158
3557
  [rule("trace-test-requires-taxonomy")]: "error"
@@ -3181,9 +3580,12 @@ var src_default = {
3181
3580
  "behaviour-one-feature-per-file": behaviourOneFeaturePerFile,
3182
3581
  "no-pseudo-gherkin-unit-tests": noPseudoGherkinUnitTests,
3183
3582
  "tests-dir-helpers-in-fixtures": testsDirHelpersInFixtures,
3583
+ "vitest-from-systemfsoftware-vitest": vitestFromSystemfsoftwareVitest,
3184
3584
  "no-io-module-in-source-test": noIoModuleInSourceTest,
3185
3585
  "tests-import-public-api": testsImportPublicApi,
3186
3586
  "differential-test-requires-harness": differentialTestRequiresHarness,
3587
+ "conformance-test-requires-harness": conformanceTestRequiresHarness,
3588
+ "model-fixture-imports-subject": modelFixtureImportsSubject,
3187
3589
  "ban-raw-span-name-emit": banRawSpanNameEmit,
3188
3590
  "trace-test-requires-taxonomy": traceTestRequiresTaxonomy
3189
3591
  },