@systemfsoftware/oxlint-plugin-test-discipline 3.8.0 → 3.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @systemfsoftware/oxlint-plugin-test-discipline
2
2
 
3
+ ## 3.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add `expect-boolean-predicate`, which refuses an `expect` on a predicate call, comparison, negation or `&&`/`||` asserted with `toBe(true|false)` and names the `toSatisfy` or `toEqual` rewrite, and `vitest-from-effect-vitest`, which refuses any value import from `vitest` — named, namespace, default, side-effect or dynamic — and names `@effect/vitest` as the rewrite; a type-only import stays legal. `prop-arbitrary-schema-origin` also checks the `of` entries of the object form `it.prop(name, { of, subject, runs }, holds)`.
8
+
9
+ ## 3.8.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Update peer and runtime dependency on `effect` and companion packages to `4.0.0-rc.117`.
14
+
3
15
  ## 3.8.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -4,19 +4,21 @@ Oxlint rules enforcing property-based test laws, test placement, and test naming
4
4
 
5
5
  ## Rules
6
6
 
7
- | Rule | What it enforces |
8
- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
- | `no-test-file-in-src` | 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. |
10
- | `src-property-test-cell` | A property test under `src/` must be a single-segment `<stem>.workflow.property.test.ts` beside the `<stem>.workflow.ts` it covers. A source file whose suffix names a cell listed in `cellsRequiringTest` must also carry an in-source vitest block; that list is empty by default. |
11
- | `test-file-outside-tests-dir` | A test file outside `src/` must live under `tests/`. |
12
- | `test-suffix-outside-src` | Outside `src/`, a test file must end `.integration.test.ts`, `.differential.test.ts`, or `.trace.test.ts` — the three behaviour suffixes. |
13
- | `tests-dir-helpers-in-fixtures` | Under `tests/`, the only non-test modules are helpers and fixtures, and they live inside `tests/__fixtures__/`. |
14
- | `no-io-module-in-source-test` | An in-source `import.meta.vitest` test block is forbidden in a module that performs I/O — decided from the module's own syntax (a non-type import from a filesystem, process or network module, called at least once), never from its filename. Only the in-source-test idiom is judged: a module whose tests live in separate files is a no-op for this rule. |
15
- | `behaviour-test-requires-gherkin` | A `.integration.test.ts` must import `makeFeature` from `@systemfsoftware/effect-gherkin-spec` and must not import test runners directly from `vitest` or `@effect/vitest`. |
16
- | `behaviour-exercises-use-case` | A `.integration.test.ts` must reach at least one shell entry — an executor/handler/adapter/store, the package `main`/`mod`/`index`, or a non-foundation package — so it drives a use case. |
17
- | `behaviour-one-feature-per-file` | A `.integration.test.ts` must contain exactly one `Feature(...)` call; zero or two-or-more is the junk-drawer signal. |
18
- | `differential-test-requires-harness` | A `.differential.test.ts` must import `@systemfsoftware/differential-spec` and invoke its harness; direct `vitest`/`@effect/vitest` imports and raw runner calls (`it`, `test`, `describe`, `it.effect`, and aliases) are forbidden, and importing the harness without invoking it is equally non-compliant. |
19
- | `tests-import-public-api` | Every file under the package-root `tests/` or `__tests__/` trees, regardless of basename, may not relative-import `src` or climb into an `internal` folder. Import the published package name or a sibling helper. |
7
+ | Rule | What it enforces |
8
+ | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | `no-test-file-in-src` | 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. |
10
+ | `src-property-test-cell` | A property test under `src/` must be a single-segment `<stem>.workflow.property.test.ts` beside the `<stem>.workflow.ts` it covers. A source file whose suffix names a cell listed in `cellsRequiringTest` must also carry an in-source vitest block; that list is empty by default. |
11
+ | `test-file-outside-tests-dir` | A test file outside `src/` must live under `tests/`. |
12
+ | `test-suffix-outside-src` | Outside `src/`, a test file must end `.integration.test.ts`, `.differential.test.ts`, or `.trace.test.ts` — the three behaviour suffixes. |
13
+ | `tests-dir-helpers-in-fixtures` | Under `tests/`, the only non-test modules are helpers and fixtures, and they live inside `tests/__fixtures__/`. |
14
+ | `no-io-module-in-source-test` | An in-source `import.meta.vitest` test block is forbidden in a module that performs I/O — decided from the module's own syntax (a non-type import from a filesystem, process or network module, called at least once), never from its filename. Only the in-source-test idiom is judged: a module whose tests live in separate files is a no-op for this rule. |
15
+ | `behaviour-test-requires-gherkin` | A `.integration.test.ts` must import `makeFeature` from `@systemfsoftware/effect-gherkin-spec` and must not import test runners directly from `vitest` or `@effect/vitest`. |
16
+ | `behaviour-exercises-use-case` | A `.integration.test.ts` must reach at least one shell entry — an executor/handler/adapter/store, the package `main`/`mod`/`index`, or a non-foundation package — so it drives a use case. |
17
+ | `behaviour-one-feature-per-file` | A `.integration.test.ts` must contain exactly one `Feature(...)` call; zero or two-or-more is the junk-drawer signal. |
18
+ | `differential-test-requires-harness` | A `.differential.test.ts` must import `@systemfsoftware/differential-spec` and invoke its harness; direct `vitest`/`@effect/vitest` imports and raw runner calls (`it`, `test`, `describe`, `it.effect`, and aliases) are forbidden, and importing the harness without invoking it is equally non-compliant. |
19
+ | `tests-import-public-api` | Every file under the package-root `tests/` or `__tests__/` trees, regardless of basename, may not relative-import `src` or climb into an `internal` folder. Import the published package name or a sibling helper. |
20
+ | `expect-boolean-predicate` | An `expect(...)` asserted with `toBe(true)`, `toBe(false)`, `toEqual(true)` or `toEqual(false)` must wrap a boolean the code under test returned. A predicate call, comparison, negation or `&&` / `\|\|` evaluated in the test is refused, with the `toSatisfy` or `toEqual` rewrite as the fix. |
21
+ | `vitest-from-effect-vitest` | Every value a test uses from Vitest is imported from `@effect/vitest`, which re-exports all of Vitest. Any value import from `vitest` — named, namespace, default, side-effect or dynamic — is refused; a type-only import stays legal. Exempt: the packages that drive Vitest's own runner instead of the fork (`vitest` itself, `vitest-conformance`, and `effect-spec-runtime`, whose `Register` calls Vitest's option-taking collector). |
20
22
 
21
23
  | suffix | layer | doubles | location |
22
24
  | ----------------------------- | ------------ | ------------------------ | ------------------ |
@@ -27,7 +29,7 @@ Oxlint rules enforcing property-based test laws, test placement, and test naming
27
29
 
28
30
  ## Enrollment
29
31
 
30
- The rules are turned on by `@systemfsoftware/oxlint-config/base`, which spreads `@systemfsoftware/oxlint-plugin-effect-dmmf`'s recommended set; that aggregate re-exports all nine under the `@systemfsoftware/effect-dmmf/` namespace, so a package extending only `base` already enforces them. `strict` adds three TypeScript rules and nothing from this plugin. Neither preset registers this plugin standalone in `jsPlugins`, so its own `recommended` config is never the thing being loaded.
32
+ `@systemfsoftware/oxlint-config-recommended` loads this plugin in `jsPlugins` and turns on `expect-boolean-predicate` and `vitest-from-effect-vitest` at `error` in its default config. The rest of `configs.recommended` is exported from that preset as `rules`, but the default config does not enable it, so those rules fire only in a package that spreads it into its own `rules`.
31
33
 
32
34
  ## Testing
33
35
 
package/dist/index.d.ts CHANGED
@@ -25,6 +25,8 @@ declare const _default: {
25
25
  'behaviour-one-feature-per-file': import("@oxlint/plugins").Rule;
26
26
  'no-pseudo-gherkin-unit-tests': import("@oxlint/plugins").Rule;
27
27
  'tests-dir-helpers-in-fixtures': import("@oxlint/plugins").Rule;
28
+ 'expect-boolean-predicate': import("@oxlint/plugins").Rule;
29
+ 'vitest-from-effect-vitest': import("@oxlint/plugins").Rule;
28
30
  'no-io-module-in-source-test': import("@oxlint/plugins").Rule;
29
31
  'tests-import-public-api': import("@oxlint/plugins").Rule;
30
32
  'differential-test-requires-harness': import("@oxlint/plugins").Rule;
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$27 = {
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: [],
@@ -49,7 +49,7 @@ const inspect = (node) => {
49
49
  };
50
50
  };
51
51
  const banRawSpanNameEmit = defineRule({
52
- meta: meta$25,
52
+ meta: meta$27,
53
53
  create(context) {
54
54
  return { CallExpression(node) {
55
55
  const emitCall = inspect(node);
@@ -99,12 +99,17 @@ const GHERKIN_PACKAGE = "@systemfsoftware/effect-gherkin-spec";
99
99
  const DIFFERENTIAL_PACKAGE = "@systemfsoftware/differential-spec";
100
100
  const TRACE_SPEC_PACKAGE = "@systemfsoftware/trace-spec";
101
101
  const FOREIGN_RUNNERS = /* @__PURE__ */ new Set(["vitest", "@effect/vitest"]);
102
+ const RAW_VITEST_PACKAGES = {
103
+ "effect-spec-runtime": true,
104
+ "vitest": true,
105
+ "vitest-conformance": true
106
+ };
102
107
  const RUNNER_NAMES = /* @__PURE__ */ new Set([
103
108
  "it",
104
109
  "test",
105
110
  "describe"
106
111
  ]);
107
- const MESSAGE$7 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
112
+ const MESSAGE$9 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
108
113
  const ABSENCE_MESSAGE = "{{name}} is untested. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
109
114
  //#endregion
110
115
  //#region src/rules/behaviour-exercises-use-case.config.ts
@@ -112,11 +117,11 @@ const NO_SUBJECT_IMPORT_NAME = "a *.integration.test.ts that reaches no package
112
117
  const NO_SUBJECT_IMPORT_EXPECTED = "an import of the package code under test";
113
118
  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";
114
119
  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 = {
120
+ const meta$26 = {
116
121
  type: "problem",
117
122
  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
123
  schema: [],
119
- messages: { noSubjectImport: MESSAGE$7 }
124
+ messages: { noSubjectImport: MESSAGE$9 }
120
125
  };
121
126
  //#endregion
122
127
  //#region src/rules/path.ts
@@ -126,6 +131,7 @@ const basenameOf = (filename) => Array$1.lastNonEmpty(segmentsOf(filename));
126
131
  /** Directory segments only — the basename never counts as a directory. */
127
132
  const directoriesOf = (filename) => Array$1.initNonEmpty(segmentsOf(filename));
128
133
  const isUnderSrc = (filename) => directoriesOf(filename).includes("src");
134
+ const isRawVitestPackage = (filename) => directoriesOf(filename).some((segment) => RAW_VITEST_PACKAGES[segment] === true);
129
135
  const isInSanctionedTestDir = (filename) => directoriesOf(filename).some((segment) => SANCTIONED_TEST_DIRS.has(segment));
130
136
  const isInTestsImportScope = (filename) => {
131
137
  const segments = segmentsOf(filename);
@@ -225,7 +231,7 @@ const DIST_SEGMENT = /(?:^|\/)dist\//;
225
231
  * path names nothing satisfies the rule the same way a real one does.
226
232
  */
227
233
  const behaviourExercisesUseCase = defineRule({
228
- meta: meta$24,
234
+ meta: meta$26,
229
235
  create(context) {
230
236
  let reached = false;
231
237
  return {
@@ -275,13 +281,13 @@ const TOO_MANY_FEATURES_NAME = "a *.integration.test.ts accumulating multiple Fe
275
281
  const TOO_MANY_FEATURES_EXPECTED = "exactly one Feature(...) — every additional one signals a junk drawer";
276
282
  const TOO_MANY_FEATURES_ACTUAL = "a behaviour file with two or more Feature(...) calls";
277
283
  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 = {
284
+ const meta$25 = {
279
285
  type: "problem",
280
286
  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
287
  schema: [],
282
288
  messages: {
283
- tooFewFeatures: MESSAGE$7,
284
- tooManyFeatures: MESSAGE$7
289
+ tooFewFeatures: MESSAGE$9,
290
+ tooManyFeatures: MESSAGE$9
285
291
  }
286
292
  };
287
293
  //#endregion
@@ -320,7 +326,7 @@ const hasAnyFeatureCall = (program) => {
320
326
  return false;
321
327
  };
322
328
  const behaviourOneFeaturePerFile = defineRule({
323
- meta: meta$23,
329
+ meta: meta$25,
324
330
  create(context) {
325
331
  return { "Program:exit"(node) {
326
332
  if (!isBehaviourTest$2(basenameOf(context.filename))) return;
@@ -361,13 +367,13 @@ const MISSING_MAKE_FEATURE_NAME = "a *.integration.test.ts without makeFeature";
361
367
  const MISSING_MAKE_FEATURE_EXPECTED = "makeFeature imported from @systemfsoftware/effect-gherkin-spec";
362
368
  const MISSING_MAKE_FEATURE_ACTUAL = "a behaviour file that never constructs a Gherkin feature";
363
369
  const MISSING_MAKE_FEATURE_FIX = "import { makeFeature } from @systemfsoftware/effect-gherkin-spec and declare `const Feature = makeFeature({ it, layer })`";
364
- const meta$22 = {
370
+ const meta$24 = {
365
371
  type: "problem",
366
372
  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." },
367
373
  schema: [],
368
374
  messages: {
369
- foreignRunner: MESSAGE$7,
370
- missingMakeFeature: MESSAGE$7
375
+ foreignRunner: MESSAGE$9,
376
+ missingMakeFeature: MESSAGE$9
371
377
  }
372
378
  };
373
379
  //#endregion
@@ -389,7 +395,7 @@ const foreignRunnerNameOf = (specifier) => {
389
395
  };
390
396
  const isBehaviourTest$1 = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
391
397
  const behaviourTestRequiresGherkin = defineRule({
392
- meta: meta$22,
398
+ meta: meta$24,
393
399
  create(context) {
394
400
  const basename = basenameOf(context.filename);
395
401
  return { Program(node) {
@@ -452,7 +458,7 @@ const INVALID_BEHAVIOR_CASE_FIX = "Convert behavior to PascalCase (e.g., throwEr
452
458
  const INVALID_CONDITION_CASE_FIX = "Convert condition to PascalCase (e.g., passwordInvalid → PasswordInvalid)";
453
459
  const EMPTY_BEHAVIOR_ACTUAL = "Empty string between Should_ and _When_";
454
460
  const EMPTY_CONDITION_ACTUAL = "Empty string after _When_";
455
- const meta$21 = {
461
+ const meta$23 = {
456
462
  type: "suggestion",
457
463
  docs: { description: "Enforce DAMP (Descriptive and Meaningful Phrases) test naming format: Should_[ExpectedBehavior]_When_[Condition]" },
458
464
  schema: [],
@@ -532,7 +538,7 @@ const isTestFunctionCall = (node) => {
532
538
  return false;
533
539
  };
534
540
  const dampTestNaming = defineRule({
535
- meta: meta$21,
541
+ meta: meta$23,
536
542
  create(context) {
537
543
  return { CallExpression(node) {
538
544
  if (!isTestFunctionCall(node)) return;
@@ -555,7 +561,7 @@ const dampTestNaming = defineRule({
555
561
  });
556
562
  //#endregion
557
563
  //#region src/rules/differential-test-requires-harness.config.ts
558
- const meta$20 = {
564
+ const meta$22 = {
559
565
  type: "problem",
560
566
  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
567
  schema: [],
@@ -577,7 +583,7 @@ const recordHarnessBindings = (node, bindings) => {
577
583
  };
578
584
  const isForeignRunnerImport = (node) => typeof node.source.value === "string" && FOREIGN_RUNNERS.has(node.source.value);
579
585
  const differentialTestRequiresHarness = defineRule({
580
- meta: meta$20,
586
+ meta: meta$22,
581
587
  create(context) {
582
588
  if (!context.filename.endsWith(".differential.test.ts")) return {};
583
589
  const harnessBindings = /* @__PURE__ */ new Set();
@@ -648,16 +654,97 @@ const differentialTestRequiresHarness = defineRule({
648
654
  }
649
655
  });
650
656
  //#endregion
657
+ //#region src/rules/expect-boolean-predicate.config.ts
658
+ const MESSAGE$8 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
659
+ const BOOLEAN_MATCHERS = {
660
+ toBe: true,
661
+ toEqual: true
662
+ };
663
+ const COMPARISON_OPERATORS = {
664
+ "===": true,
665
+ "!==": true,
666
+ "==": true,
667
+ "!=": true,
668
+ "<": true,
669
+ "<=": true,
670
+ ">": true,
671
+ ">=": true,
672
+ instanceof: true,
673
+ in: true
674
+ };
675
+ const VIOLATION_NAME$4 = "a predicate evaluated in the test asserted against true or false";
676
+ const VIOLATION_EXPECTED$1 = "expect(...) around a boolean the code under test returned";
677
+ const VIOLATION_ACTUAL$1 = "expect(...) wraps a call, a comparison, a `!` or an `&&` / `||` evaluated in the test, so a failure reports `expected false to be true` instead of the values that disagreed";
678
+ const VIOLATION_FIX$1 = "rewrite a predicate call as expect(x).toSatisfy(P) — e.g. expect(s.reading).toSatisfy(Result.isFailure) — and a comparison as expect(a).toEqual(b)";
679
+ const meta$21 = {
680
+ type: "problem",
681
+ docs: { description: "An expect(...) asserted with toBe(true|false) or toEqual(true|false) must wrap a boolean the code under test returned. A predicate call, comparison, negation or && / || evaluated in the test is refused, with the toSatisfy or toEqual rewrite as the fix." },
682
+ schema: [],
683
+ messages: { booleanPredicate: MESSAGE$8 }
684
+ };
685
+ //#endregion
686
+ //#region src/rules/expect-call.ts
687
+ const EXPECT = "expect";
688
+ const isExpectCallee = (callee) => callee.type === "Identifier" && callee.name === EXPECT;
689
+ const expectCallOf = (node, acceptsMember) => {
690
+ if (node.callee.type !== "MemberExpression") return void 0;
691
+ let target = node.callee.object;
692
+ while (target.type === "MemberExpression") {
693
+ if (acceptsMember !== void 0 && acceptsMember(target) === false) return void 0;
694
+ target = target.object;
695
+ }
696
+ if (target.type !== "CallExpression" || isExpectCallee(target.callee) === false) return void 0;
697
+ return target;
698
+ };
699
+ //#endregion
700
+ //#region src/rules/expect-boolean-predicate.ts
701
+ const isNotMember = (member) => member.property.type === "Identifier" && member.property.name === "not";
702
+ const isBooleanLiteral = (node) => node !== void 0 && node.type === "Literal" && typeof node.value === "boolean";
703
+ /** The ARG positions `expect` exposes that no subject can fill by accident. */
704
+ const isEvaluatedInTest = (arg) => {
705
+ switch (arg.type) {
706
+ case "CallExpression": return true;
707
+ case "UnaryExpression": return arg.operator === "!";
708
+ case "LogicalExpression": return arg.operator === "&&" || arg.operator === "||";
709
+ case "BinaryExpression": return COMPARISON_OPERATORS[arg.operator] === true;
710
+ default: return false;
711
+ }
712
+ };
713
+ const expectBooleanPredicate = defineRule({
714
+ meta: meta$21,
715
+ create(context) {
716
+ return { CallExpression(node) {
717
+ if (node.callee.type !== "MemberExpression" || node.callee.property.type !== "Identifier") return;
718
+ if (BOOLEAN_MATCHERS[node.callee.property.name] !== true) return;
719
+ if (!isBooleanLiteral(node.arguments[0])) return;
720
+ const receiver = expectCallOf(node, isNotMember);
721
+ if (receiver === void 0) return;
722
+ const arg = receiver.arguments[0];
723
+ if (arg === void 0 || !isEvaluatedInTest(arg)) return;
724
+ context.report({
725
+ node,
726
+ messageId: "booleanPredicate",
727
+ data: {
728
+ name: VIOLATION_NAME$4,
729
+ expected: VIOLATION_EXPECTED$1,
730
+ actual: VIOLATION_ACTUAL$1,
731
+ fix: VIOLATION_FIX$1
732
+ }
733
+ });
734
+ } };
735
+ }
736
+ });
737
+ //#endregion
651
738
  //#region src/rules/in-source-test-prop-only.config.ts
652
739
  const NON_PROP_CALL_NAME = "a non-property test call inside an `import.meta.vitest` block";
653
740
  const NON_PROP_CALL_EXPECTED = "only `it.prop` or `it.effect.prop` member-chain calls (standard modifiers included) with boolean predicates";
654
741
  const NON_PROP_CALL_ACTUAL = "a bare or member-chain test call other than `it.prop`/`it.effect.prop`";
655
742
  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 = {
743
+ const meta$20 = {
657
744
  type: "problem",
658
745
  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
746
  schema: [],
660
- messages: { nonPropCall: MESSAGE$7 }
747
+ messages: { nonPropCall: MESSAGE$9 }
661
748
  };
662
749
  //#endregion
663
750
  //#region src/rules/vitest-guard.ts
@@ -717,7 +804,7 @@ const rootNameOf = (node) => {
717
804
  if (node.type === "CallExpression") return rootNameOf(node.callee);
718
805
  };
719
806
  const inSourceTestPropOnly = defineRule({
720
- meta: meta$19,
807
+ meta: meta$20,
721
808
  create(context) {
722
809
  const filename = context.filename;
723
810
  const basename = basenameOf(filename);
@@ -762,13 +849,13 @@ const NO_PRIVATE_TARGET_NAME = "an `import.meta.vitest` block touching no privat
762
849
  const NO_PRIVATE_TARGET_EXPECTED = "an in-source test exercising a non-exported module-level binding";
763
850
  const NO_PRIVATE_TARGET_ACTUAL = "an in-source block referencing only exported or imported names";
764
851
  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 = {
852
+ const meta$19 = {
766
853
  type: "problem",
767
854
  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
855
  schema: [],
769
856
  messages: {
770
- notModuleLevel: MESSAGE$7,
771
- noPrivateTarget: MESSAGE$7
857
+ notModuleLevel: MESSAGE$9,
858
+ noPrivateTarget: MESSAGE$9
772
859
  }
773
860
  };
774
861
  //#endregion
@@ -796,7 +883,7 @@ const collectPrivateNames = (body, out) => {
796
883
  }
797
884
  };
798
885
  const inSourceTestTargetsPrivate = defineRule({
799
- meta: meta$18,
886
+ meta: meta$19,
800
887
  create(context) {
801
888
  const filename = context.filename;
802
889
  const basename = basenameOf(filename);
@@ -854,15 +941,15 @@ const inSourceTestTargetsPrivate = defineRule({
854
941
  });
855
942
  //#endregion
856
943
  //#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 = {
944
+ const MESSAGE$7 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
945
+ const meta$18 = {
859
946
  type: "problem",
860
947
  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
948
  schema: [],
862
949
  messages: {
863
- expectCall: MESSAGE$6,
864
- assertCall: MESSAGE$6,
865
- rawFcRun: MESSAGE$6
950
+ expectCall: MESSAGE$7,
951
+ assertCall: MESSAGE$7,
952
+ rawFcRun: MESSAGE$7
866
953
  }
867
954
  };
868
955
  //#endregion
@@ -893,7 +980,7 @@ const isInsidePropPredicate = (node) => {
893
980
  return isInsidePropPredicate(parent);
894
981
  };
895
982
  const noAssertInProperty = defineRule({
896
- meta: meta$17,
983
+ meta: meta$18,
897
984
  create(context) {
898
985
  return { CallExpression(node) {
899
986
  const callee = node.callee;
@@ -946,7 +1033,7 @@ const SKIP_WALK_KEYS = /* @__PURE__ */ new Set([
946
1033
  "range",
947
1034
  "parent"
948
1035
  ]);
949
- const meta$16 = {
1036
+ const meta$17 = {
950
1037
  type: "problem",
951
1038
  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
1039
  schema: [],
@@ -996,15 +1083,6 @@ const dependsOnBehaviour = (node, imported) => {
996
1083
  }
997
1084
  return false;
998
1085
  };
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
1086
  const importedNames = (program) => {
1009
1087
  const names = /* @__PURE__ */ new Set();
1010
1088
  for (const statement of program.body) {
@@ -1014,7 +1092,7 @@ const importedNames = (program) => {
1014
1092
  return names;
1015
1093
  };
1016
1094
  const noBehaviourlessAssertion = defineRule({
1017
- meta: meta$16,
1095
+ meta: meta$17,
1018
1096
  create(context) {
1019
1097
  if (!TEST_FILE.test(context.filename)) return {};
1020
1098
  const imported = importedNames(context.sourceCode.ast);
@@ -1084,7 +1162,7 @@ const IO_SOURCE_TEST_NAME = "An in-source `import.meta.vitest` test block";
1084
1162
  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
1163
  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
1164
  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 = {
1165
+ const meta$16 = {
1088
1166
  type: "problem",
1089
1167
  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
1168
  schema: [],
@@ -1105,7 +1183,7 @@ const bindingBase = (callee) => {
1105
1183
  if (callee.type === "Identifier") return callee.name;
1106
1184
  };
1107
1185
  const noIoModuleInSourceTest = defineRule({
1108
- meta: meta$15,
1186
+ meta: meta$16,
1109
1187
  create(context) {
1110
1188
  /** Local binding name -> specifier it was (non-type) imported from. */
1111
1189
  const ioBindings = {};
@@ -1147,7 +1225,7 @@ const noIoModuleInSourceTest = defineRule({
1147
1225
  });
1148
1226
  //#endregion
1149
1227
  //#region src/rules/no-nested-quantification.config.ts
1150
- const MESSAGE$5 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1228
+ const MESSAGE$6 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1151
1229
  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
1230
  const ITERATOR_METHODS = /* @__PURE__ */ new Set([
1153
1231
  "every",
@@ -1166,15 +1244,15 @@ const ITERATOR_METHODS = /* @__PURE__ */ new Set([
1166
1244
  ]);
1167
1245
  const CONSTANT_POOL_ARBITRARIES = /* @__PURE__ */ new Set(["constant", "constantFrom"]);
1168
1246
  const FASTCHECK_NAMESPACES = /* @__PURE__ */ new Set(["fc"]);
1169
- const VIOLATION_NAME$2 = "quantification nested inside a property predicate";
1247
+ const VIOLATION_NAME$3 = "quantification nested inside a property predicate";
1170
1248
  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
1249
  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
1250
  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 = {
1251
+ const meta$15 = {
1174
1252
  type: "problem",
1175
1253
  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
1254
  schema: [Schema.toJsonSchemaDocument(Options$2).schema],
1177
- messages: { nestedQuantification: MESSAGE$5 }
1255
+ messages: { nestedQuantification: MESSAGE$6 }
1178
1256
  };
1179
1257
  //#endregion
1180
1258
  //#region src/rules/no-nested-quantification.ts
@@ -1229,11 +1307,17 @@ const isConstantPool = (element) => {
1229
1307
  };
1230
1308
  const boundedPoolIndices = (call) => {
1231
1309
  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
- });
1310
+ const options = call.arguments[1];
1311
+ if (options === void 0 || options.type !== "ObjectExpression") return bounded;
1312
+ for (const property of options.properties) {
1313
+ if (property.type !== "Property" || property.computed) continue;
1314
+ if (property.key.type !== "Identifier" || property.key.name !== "of") continue;
1315
+ if (property.value.type !== "ArrayExpression") return bounded;
1316
+ property.value.elements.forEach((element, index) => {
1317
+ if (isConstantPool(element)) bounded.add(index);
1318
+ });
1319
+ return bounded;
1320
+ }
1237
1321
  return bounded;
1238
1322
  };
1239
1323
  const drawnNamesIn = (predicate, bounded) => {
@@ -1241,12 +1325,12 @@ const drawnNamesIn = (predicate, bounded) => {
1241
1325
  const addFrom = (value) => {
1242
1326
  for (const name of identifiersIn(value)) drawn.add(name);
1243
1327
  };
1244
- const [first, ...rest] = predicate.params;
1245
- if (isNode$3(first) && first.type === "ArrayPattern") first.elements.forEach((element, index) => {
1328
+ const [, values, ...rest] = predicate.params;
1329
+ if (isNode$3(values) && values.type === "ArrayPattern") values.elements.forEach((element, index) => {
1246
1330
  if (bounded.has(index)) return;
1247
1331
  addFrom(element);
1248
1332
  });
1249
- else addFrom(first);
1333
+ else addFrom(values);
1250
1334
  addFrom(rest);
1251
1335
  visit(predicate.body, (node) => {
1252
1336
  if (node.type !== "VariableDeclarator") return;
@@ -1306,7 +1390,7 @@ const check = (context, call, predicate) => {
1306
1390
  node: iteration.node,
1307
1391
  messageId: "nestedQuantification",
1308
1392
  data: {
1309
- name: VIOLATION_NAME$2,
1393
+ name: VIOLATION_NAME$3,
1310
1394
  expected: EXPECTED$2,
1311
1395
  actual: ACTUAL$2,
1312
1396
  fix: FIX$2
@@ -1321,7 +1405,7 @@ const check = (context, call, predicate) => {
1321
1405
  * and excluded; DISCHARGED_BY in the suite pins the unbounded recipe shape.
1322
1406
  */
1323
1407
  const noNestedQuantification = defineRule({
1324
- meta: meta$14,
1408
+ meta: meta$15,
1325
1409
  create(context) {
1326
1410
  const options = Schema.decodeUnknownSync(Options$2)(context.options[0] ?? {});
1327
1411
  const exempt = new Set(options.exempt);
@@ -1342,11 +1426,11 @@ const NO_LAYER_IN_FEATURE_NAME = "a *.integration.test.ts feature with no enviro
1342
1426
  const NO_LAYER_IN_FEATURE_EXPECTED = "a Feature builder chained with .withLayer(layer) or .withScenarioLayer(layer)";
1343
1427
  const NO_LAYER_IN_FEATURE_ACTUAL = "a Feature(...) call without .withLayer or .withScenarioLayer";
1344
1428
  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 = {
1429
+ const meta$14 = {
1346
1430
  type: "problem",
1347
1431
  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
1432
  schema: [],
1349
- messages: { noLayerInFeature: MESSAGE$7 }
1433
+ messages: { noLayerInFeature: MESSAGE$9 }
1350
1434
  };
1351
1435
  //#endregion
1352
1436
  //#region src/rules/no-pseudo-gherkin-unit-tests.ts
@@ -1377,7 +1461,7 @@ const findRootFeatureCall = (callNode) => {
1377
1461
  return null;
1378
1462
  };
1379
1463
  const noPseudoGherkinUnitTests = defineRule({
1380
- meta: meta$13,
1464
+ meta: meta$14,
1381
1465
  create(context) {
1382
1466
  if (!isBehaviourTest(basenameOf(context.filename))) return {};
1383
1467
  return { CallExpression(node) {
@@ -1401,16 +1485,16 @@ const noPseudoGherkinUnitTests = defineRule({
1401
1485
  });
1402
1486
  //#endregion
1403
1487
  //#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 = {
1488
+ const MESSAGE$5 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1489
+ const meta$13 = {
1406
1490
  type: "problem",
1407
1491
  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." },
1408
1492
  schema: [],
1409
1493
  messages: {
1410
- bareReturn: MESSAGE$4,
1411
- nonBooleanReturn: MESSAGE$4,
1412
- missingReturn: MESSAGE$4,
1413
- nonBooleanBody: MESSAGE$4
1494
+ bareReturn: MESSAGE$5,
1495
+ nonBooleanReturn: MESSAGE$5,
1496
+ missingReturn: MESSAGE$5,
1497
+ nonBooleanBody: MESSAGE$5
1414
1498
  }
1415
1499
  };
1416
1500
  //#endregion
@@ -1550,7 +1634,7 @@ const checkFn = (context, fn) => {
1550
1634
  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
1635
  };
1552
1636
  const noSilentReturn = defineRule({
1553
- meta: meta$12,
1637
+ meta: meta$13,
1554
1638
  create(context) {
1555
1639
  return { CallExpression(node) {
1556
1640
  if (!isPropCallee(node.callee)) return;
@@ -1579,21 +1663,21 @@ const SCHEMA_TEST_DETAIL = {
1579
1663
  actual: "an authored *.schema.test.ts restating generated coverage",
1580
1664
  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
1665
  };
1582
- const meta$11 = {
1666
+ const meta$12 = {
1583
1667
  type: "problem",
1584
1668
  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
1669
  schema: [Schema.toJsonSchemaDocument(Options$1).schema],
1586
1670
  messages: {
1587
- testFileInSrc: MESSAGE$7,
1588
- schemaTestInSrc: MESSAGE$7,
1589
- propertyTestOutsideTestsDir: MESSAGE$7
1671
+ testFileInSrc: MESSAGE$9,
1672
+ schemaTestInSrc: MESSAGE$9,
1673
+ propertyTestOutsideTestsDir: MESSAGE$9
1590
1674
  }
1591
1675
  };
1592
1676
  //#endregion
1593
1677
  //#region src/rules/no-test-file-in-src.ts
1594
1678
  const violationOf = (basename, isPropertyTest, dir) => basename.endsWith(".schema.test.ts") ? ["schemaTestInSrc", SCHEMA_TEST_DETAIL] : isPropertyTest ? ["propertyTestOutsideTestsDir", propertyTestLocationDetail(dir)] : ["testFileInSrc", testFileInSrcDetail(dir)];
1595
1679
  const noTestFileInSrc = defineRule({
1596
- meta: meta$11,
1680
+ meta: meta$12,
1597
1681
  create(context) {
1598
1682
  const { sanctionedDirs } = Schema.decodeUnknownSync(Options$1)(context.options[0] ?? {});
1599
1683
  const basename = basenameOf(context.filename);
@@ -1646,7 +1730,7 @@ const PREDICATE_SYMBOLS = /* @__PURE__ */ new Set([
1646
1730
  const NULLARY_PREDICATE_SYMBOLS = /* @__PURE__ */ new Set(["⊥"]);
1647
1731
  const PASCAL_CASE = /^[A-Z][a-z][a-zA-Z0-9]*$/;
1648
1732
  const DAMP_WORDS = /When|Should|Given|Then|Otherwise|After|Before/;
1649
- const meta$10 = {
1733
+ const meta$11 = {
1650
1734
  type: "suggestion",
1651
1735
  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
1736
  schema: [],
@@ -1690,7 +1774,7 @@ const parseSegments = (name) => {
1690
1774
  };
1691
1775
  };
1692
1776
  const pbtNaming = defineRule({
1693
- meta: meta$10,
1777
+ meta: meta$11,
1694
1778
  create(context) {
1695
1779
  return { CallExpression(node) {
1696
1780
  if (!isPropCall(node)) return;
@@ -1782,8 +1866,8 @@ const pbtNaming = defineRule({
1782
1866
  });
1783
1867
  //#endregion
1784
1868
  //#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";
1869
+ const MESSAGE$4 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
1870
+ const VIOLATION_NAME$2 = "a hand-built fast-check arbitrary with no schema underneath";
1787
1871
  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
1872
  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
1873
  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 +1882,13 @@ const SCHEMA_NAMESPACE_NAMES = {
1798
1882
  };
1799
1883
  const FASTCHECK_NAMESPACE_NAMES = { FastCheck: true };
1800
1884
  const COMBINATOR_CALLEES = { pipe: true };
1801
- const meta$9 = {
1885
+ const meta$10 = {
1802
1886
  type: "problem",
1803
1887
  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
1888
  schema: [],
1805
1889
  messages: {
1806
- handBuiltArbitrary: MESSAGE$3,
1807
- stockDerivedArbitrary: MESSAGE$3
1890
+ handBuiltArbitrary: MESSAGE$4,
1891
+ stockDerivedArbitrary: MESSAGE$4
1808
1892
  }
1809
1893
  };
1810
1894
  //#endregion
@@ -2062,26 +2146,49 @@ const checkArbitraryFactory = (provenance, context, call) => {
2062
2146
  }
2063
2147
  });
2064
2148
  };
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) {
2149
+ const reportHandBuilt = (provenance, context, node) => {
2150
+ if (provenance.verdictOf(node, 0) !== "handBuilt") return;
2151
+ context.report({
2152
+ node,
2153
+ messageId: "handBuiltArbitrary",
2154
+ data: {
2155
+ name: VIOLATION_NAME$2,
2156
+ expected: EXPECTED$1,
2157
+ actual: ACTUAL$1,
2158
+ fix: FIX$1
2159
+ }
2160
+ });
2161
+ };
2162
+ const checkArrayOf = (provenance, context, of) => {
2163
+ for (const element of of.elements) {
2069
2164
  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
- });
2165
+ reportHandBuilt(provenance, context, element);
2166
+ }
2167
+ };
2168
+ const checkRecordOf = (provenance, context, of) => {
2169
+ for (const member of of.properties) {
2170
+ if (member.type !== "Property" || member.computed) continue;
2171
+ reportHandBuilt(provenance, context, member.value);
2172
+ }
2173
+ };
2174
+ const checkPropCall$1 = (provenance, context, call) => {
2175
+ const options = call.arguments[1];
2176
+ if (options === void 0 || options.type !== "ObjectExpression") return;
2177
+ for (const property of options.properties) {
2178
+ if (property.type !== "Property" || property.computed) continue;
2179
+ if (property.key.type !== "Identifier" || property.key.name !== "of") continue;
2180
+ if (property.value.type === "ArrayExpression") {
2181
+ checkArrayOf(provenance, context, property.value);
2182
+ return;
2183
+ }
2184
+ if (property.value.type === "ObjectExpression") {
2185
+ checkRecordOf(provenance, context, property.value);
2186
+ return;
2187
+ }
2081
2188
  }
2082
2189
  };
2083
2190
  const propArbitrarySchemaOrigin = defineRule({
2084
- meta: meta$9,
2191
+ meta: meta$10,
2085
2192
  create(context) {
2086
2193
  const provenance = new Provenance(context.sourceCode.getScope);
2087
2194
  const collect = (value, out) => {
@@ -2140,16 +2247,16 @@ const propArbitrarySchemaOrigin = defineRule({
2140
2247
  });
2141
2248
  //#endregion
2142
2249
  //#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";
2250
+ const MESSAGE$3 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2251
+ const VIOLATION_NAME$1 = "a hand-rolled recursive schema fixture inside a test block";
2145
2252
  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
2253
  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
2254
  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 = {
2255
+ const meta$9 = {
2149
2256
  type: "problem",
2150
2257
  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
2258
  schema: [],
2152
- messages: { handRolledRecursiveFixture: MESSAGE$2 }
2259
+ messages: { handRolledRecursiveFixture: MESSAGE$3 }
2153
2260
  };
2154
2261
  //#endregion
2155
2262
  //#region src/rules/prop-fixture-schema-origin.ts
@@ -2323,7 +2430,7 @@ const isNamedBuilderOrigin = (union, getScope) => {
2323
2430
  return false;
2324
2431
  };
2325
2432
  const propFixtureSchemaOrigin = defineRule({
2326
- meta: meta$8,
2433
+ meta: meta$9,
2327
2434
  create(context) {
2328
2435
  const getScope = context.sourceCode.getScope;
2329
2436
  return { CallExpression(node) {
@@ -2340,7 +2447,7 @@ const propFixtureSchemaOrigin = defineRule({
2340
2447
  node,
2341
2448
  messageId: "handRolledRecursiveFixture",
2342
2449
  data: {
2343
- name: VIOLATION_NAME,
2450
+ name: VIOLATION_NAME$1,
2344
2451
  expected: EXPECTED,
2345
2452
  actual: ACTUAL,
2346
2453
  fix: FIX
@@ -2351,7 +2458,7 @@ const propFixtureSchemaOrigin = defineRule({
2351
2458
  });
2352
2459
  //#endregion
2353
2460
  //#region src/rules/prop-generated-law-duplicate.config.ts
2354
- const MESSAGE$1 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2461
+ const MESSAGE$2 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2355
2462
  const COMPILER_NAME = "a runtime probe of a compile-time guarantee";
2356
2463
  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
2464
  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 +2467,13 @@ const NO_FUNCTION_NAME = "a property that tests no function";
2360
2467
  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
2468
  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
2469
  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 = {
2470
+ const meta$8 = {
2364
2471
  type: "problem",
2365
2472
  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
2473
  schema: [],
2367
2474
  messages: {
2368
- compilerDuplicate: MESSAGE$1,
2369
- noDomainFunction: MESSAGE$1
2475
+ compilerDuplicate: MESSAGE$2,
2476
+ noDomainFunction: MESSAGE$2
2370
2477
  }
2371
2478
  };
2372
2479
  //#endregion
@@ -2506,7 +2613,7 @@ const checkPropCall = (provenance, context, call) => {
2506
2613
  });
2507
2614
  };
2508
2615
  const propGeneratedLawDuplicate = defineRule({
2509
- meta: meta$7,
2616
+ meta: meta$8,
2510
2617
  create(context) {
2511
2618
  const provenance = new Provenance(context.sourceCode.getScope);
2512
2619
  const collect = (value, out) => {
@@ -2549,17 +2656,17 @@ const propGeneratedLawDuplicate = defineRule({
2549
2656
  //#endregion
2550
2657
  //#region src/rules/property-file-purity.config.ts
2551
2658
  const PROPERTY_TEST_SUFFIX = ".property.test.ts";
2552
- const MESSAGE = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2553
- const meta$6 = {
2659
+ const MESSAGE$1 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
2660
+ const meta$7 = {
2554
2661
  type: "problem",
2555
2662
  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
2663
  schema: [],
2557
2664
  messages: {
2558
- plainIt: MESSAGE,
2559
- plainEffectIt: MESSAGE,
2560
- rawFastCheck: MESSAGE,
2561
- fastCheckImport: MESSAGE,
2562
- propCall: MESSAGE
2665
+ plainIt: MESSAGE$1,
2666
+ plainEffectIt: MESSAGE$1,
2667
+ rawFastCheck: MESSAGE$1,
2668
+ fastCheckImport: MESSAGE$1,
2669
+ propCall: MESSAGE$1
2563
2670
  }
2564
2671
  };
2565
2672
  //#endregion
@@ -2576,7 +2683,7 @@ const PROPERTY_FILE_KIND = {
2576
2683
  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
2684
  rawFcExpected: "it.prop(...) or it.effect.prop(...) from @effect/vitest",
2578
2685
  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, ...)"
2686
+ rawFcFix: "rewrite as it.prop(name, { of, subject, runs }, holds) returning a boolean; fc.* stays for building arbitraries (fc.pre, fc.stringMatching, ...)"
2580
2687
  };
2581
2688
  const DIFFERENTIAL_FILE_KIND = {
2582
2689
  suffix: DIFFERENTIAL_SUFFIX,
@@ -2666,7 +2773,7 @@ const createScenarioFileVisitors = (context) => ({
2666
2773
  });
2667
2774
  const kindOf = (filename) => filename.endsWith(".differential.test.ts") ? DIFFERENTIAL_FILE_KIND : PROPERTY_FILE_KIND;
2668
2775
  const propertyFilePurity = defineRule({
2669
- meta: meta$6,
2776
+ meta: meta$7,
2670
2777
  create(context) {
2671
2778
  if (!isTestFile(basenameOf(context.filename))) return {};
2672
2779
  if (context.filename.endsWith(".property.test.ts") || context.filename.endsWith(".differential.test.ts")) return createPropertyFileVisitors(context, kindOf(context.filename));
@@ -2682,12 +2789,12 @@ const UNSANCTIONED_CELL_FIX = "rename it <stem>.workflow.property.test.ts beside
2682
2789
  const MISSING_CELL_TEST_EXPECTED = "a test for every cell suffix the consumer lists in cellsRequiringTest";
2683
2790
  const MISSING_CELL_TEST_ACTUAL = "a declared cell whose own module carries no `if (import.meta.vitest)` block";
2684
2791
  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 = {
2792
+ const meta$6 = {
2686
2793
  type: "problem",
2687
2794
  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
2795
  schema: [Schema.toJsonSchemaDocument(Options).schema],
2689
2796
  messages: {
2690
- unsanctionedCell: MESSAGE$7,
2797
+ unsanctionedCell: MESSAGE$9,
2691
2798
  missingCellTest: ABSENCE_MESSAGE
2692
2799
  }
2693
2800
  };
@@ -2695,7 +2802,7 @@ const meta$5 = {
2695
2802
  //#region src/rules/src-property-test-cell.ts
2696
2803
  const carriesInSourceBlock = (body) => body.some((statement) => statement.type === "IfStatement" && isVitestGuard(statement.test));
2697
2804
  const srcPropertyTestCell = defineRule({
2698
- meta: meta$5,
2805
+ meta: meta$6,
2699
2806
  create(context) {
2700
2807
  const { cellsRequiringTest } = Schema.decodeUnknownSync(Options)(context.options[0] ?? {});
2701
2808
  const filename = context.filename;
@@ -2748,7 +2855,7 @@ const testFileOutsideTestsDir = defineRule({
2748
2855
  type: "problem",
2749
2856
  docs: { description: "Test files outside src/ must live under a tests/ directory; free-standing test files have no sanctioned home." },
2750
2857
  schema: [],
2751
- messages: { strayTestFile: MESSAGE$7 }
2858
+ messages: { strayTestFile: MESSAGE$9 }
2752
2859
  },
2753
2860
  create(context) {
2754
2861
  if (isUnderSrc(context.filename)) return {};
@@ -2780,7 +2887,7 @@ const testSuffixOutsideSrc = defineRule({
2780
2887
  type: "problem",
2781
2888
  docs: { description: "Outside src/, a test file ends .integration.test.ts, .differential.test.ts, or .trace.test.ts." },
2782
2889
  schema: [],
2783
- messages: { unsanctionedSuffix: MESSAGE$7 }
2890
+ messages: { unsanctionedSuffix: MESSAGE$9 }
2784
2891
  },
2785
2892
  create(context) {
2786
2893
  const filename = context.filename;
@@ -2814,7 +2921,7 @@ const testsDirHelpersInFixtures = defineRule({
2814
2921
  type: "problem",
2815
2922
  docs: { description: "Under tests/, the only non-test modules are helpers and fixtures, and they live inside tests/__fixtures__/." },
2816
2923
  schema: [],
2817
- messages: { helperOutsideFixtures: MESSAGE$7 }
2924
+ messages: { helperOutsideFixtures: MESSAGE$9 }
2818
2925
  },
2819
2926
  create(context) {
2820
2927
  const filename = context.filename;
@@ -2843,11 +2950,11 @@ const testsDirHelpersInFixtures = defineRule({
2843
2950
  const REACH_IN_EXPECTED = "a package name or subpath, or a sibling helper under the test tree";
2844
2951
  const REACH_IN_ACTUAL = "a relative import that reaches src or climbs into an internal folder";
2845
2952
  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 = {
2953
+ const meta$2 = {
2847
2954
  type: "problem",
2848
2955
  docs: { description: "Forbid package-level tests from relative-importing src or climbing into an internal folder" },
2849
2956
  schema: [],
2850
- messages: { sourceReachIn: MESSAGE$7 }
2957
+ messages: { sourceReachIn: MESSAGE$9 }
2851
2958
  };
2852
2959
  //#endregion
2853
2960
  //#region src/rules/tests-import-public-api.ts
@@ -2865,7 +2972,7 @@ const specifierOf = (node) => {
2865
2972
  if (node.type === "Literal" && typeof node.value === "string") return node.value;
2866
2973
  };
2867
2974
  const testsImportPublicApi = defineRule({
2868
- meta: meta$1,
2975
+ meta: meta$2,
2869
2976
  create(context) {
2870
2977
  if (!isInTestsImportScope(context.filename)) return {};
2871
2978
  const reportIfForbidden = (sourceNode) => {
@@ -2923,14 +3030,14 @@ const HTTP_TERMINATION_FIX = "replace the HTTP assertion with a relation over th
2923
3030
  const RAW_EMIT_EXPECTED = "spans declared once with Span.declare({ id, name, attrs }) inside the traced cell; the spec observes the decoded graph";
2924
3031
  const RAW_EMIT_ACTUAL = "a raw span emit call inside a trace spec";
2925
3032
  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 = {
3033
+ const meta$1 = {
2927
3034
  type: "problem",
2928
3035
  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
3036
  schema: [],
2930
3037
  messages: {
2931
- missingHarnessImport: MESSAGE$7,
2932
- httpTermination: MESSAGE$7,
2933
- rawEmitCall: MESSAGE$7
3038
+ missingHarnessImport: MESSAGE$9,
3039
+ httpTermination: MESSAGE$9,
3040
+ rawEmitCall: MESSAGE$9
2934
3041
  }
2935
3042
  };
2936
3043
  //#endregion
@@ -3097,7 +3204,7 @@ const reportHttpTermination = (context, node) => {
3097
3204
  });
3098
3205
  };
3099
3206
  const traceTestRequiresTaxonomy = defineRule({
3100
- meta,
3207
+ meta: meta$1,
3101
3208
  create(context) {
3102
3209
  if (!context.filename.endsWith(".trace.test.ts")) return {};
3103
3210
  let hasHarnessImport = false;
@@ -3126,6 +3233,56 @@ const traceTestRequiresTaxonomy = defineRule({
3126
3233
  }
3127
3234
  });
3128
3235
  //#endregion
3236
+ //#region src/rules/vitest-from-effect-vitest.config.ts
3237
+ const MESSAGE = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
3238
+ const VIOLATION_NAME = "a value import from vitest";
3239
+ const VIOLATION_EXPECTED = "every test value imported from @effect/vitest, which re-exports all of Vitest";
3240
+ const VIOLATION_ACTUAL = "a named, namespace, default, side-effect or dynamic value import from vitest";
3241
+ const VIOLATION_FIX = "import it from @effect/vitest instead of vitest";
3242
+ const meta = {
3243
+ type: "problem",
3244
+ docs: { description: "Every value a test uses from Vitest is imported from @effect/vitest, which re-exports all of Vitest. Any value import from vitest — named, namespace, default, side-effect or dynamic — is refused; a type-only import stays legal." },
3245
+ schema: [],
3246
+ messages: { vitestImport: MESSAGE }
3247
+ };
3248
+ //#endregion
3249
+ //#region src/rules/vitest-from-effect-vitest.ts
3250
+ const isTypeOnly = (node) => {
3251
+ if (node.importKind === "type") return true;
3252
+ if (node.specifiers.length === 0) return false;
3253
+ return node.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type");
3254
+ };
3255
+ const isVitestLiteral = (node) => node.type === "Literal" && typeof node.value === "string" && node.value === "vitest";
3256
+ const vitestFromEffectVitest = defineRule({
3257
+ meta,
3258
+ create(context) {
3259
+ if (isRawVitestPackage(context.filename)) return {};
3260
+ const report = (node) => {
3261
+ context.report({
3262
+ node,
3263
+ messageId: "vitestImport",
3264
+ data: {
3265
+ name: VIOLATION_NAME,
3266
+ expected: VIOLATION_EXPECTED,
3267
+ actual: VIOLATION_ACTUAL,
3268
+ fix: VIOLATION_FIX
3269
+ }
3270
+ });
3271
+ };
3272
+ return {
3273
+ ImportDeclaration(node) {
3274
+ if (node.source.value !== "vitest") return;
3275
+ if (isTypeOnly(node)) return;
3276
+ report(node);
3277
+ },
3278
+ ImportExpression(node) {
3279
+ if (!isVitestLiteral(node.source)) return;
3280
+ report(node);
3281
+ }
3282
+ };
3283
+ }
3284
+ });
3285
+ //#endregion
3129
3286
  //#region src/index.ts
3130
3287
  const PLUGIN_NAME = "@systemfsoftware/oxlint-plugin-test-discipline";
3131
3288
  const rule = (name) => `${PLUGIN_NAME}/${name}`;
@@ -3150,6 +3307,8 @@ const recommendedRules = {
3150
3307
  [rule("behaviour-exercises-use-case")]: "error",
3151
3308
  [rule("behaviour-one-feature-per-file")]: "error",
3152
3309
  [rule("tests-dir-helpers-in-fixtures")]: "error",
3310
+ [rule("expect-boolean-predicate")]: "error",
3311
+ [rule("vitest-from-effect-vitest")]: "error",
3153
3312
  [rule("no-io-module-in-source-test")]: "error",
3154
3313
  [rule("tests-import-public-api")]: "error",
3155
3314
  [rule("differential-test-requires-harness")]: "error",
@@ -3181,6 +3340,8 @@ var src_default = {
3181
3340
  "behaviour-one-feature-per-file": behaviourOneFeaturePerFile,
3182
3341
  "no-pseudo-gherkin-unit-tests": noPseudoGherkinUnitTests,
3183
3342
  "tests-dir-helpers-in-fixtures": testsDirHelpersInFixtures,
3343
+ "expect-boolean-predicate": expectBooleanPredicate,
3344
+ "vitest-from-effect-vitest": vitestFromEffectVitest,
3184
3345
  "no-io-module-in-source-test": noIoModuleInSourceTest,
3185
3346
  "tests-import-public-api": testsImportPublicApi,
3186
3347
  "differential-test-requires-harness": differentialTestRequiresHarness,
@@ -26,6 +26,8 @@ declare const _default: {
26
26
  'behaviour-one-feature-per-file': Rule;
27
27
  'no-pseudo-gherkin-unit-tests': Rule;
28
28
  'tests-dir-helpers-in-fixtures': Rule;
29
+ 'expect-boolean-predicate': Rule;
30
+ 'vitest-from-effect-vitest': Rule;
29
31
  'no-io-module-in-source-test': Rule;
30
32
  'tests-import-public-api': Rule;
31
33
  'differential-test-requires-harness': Rule;
@@ -26,6 +26,8 @@ declare const _default: {
26
26
  'behaviour-one-feature-per-file': Rule;
27
27
  'no-pseudo-gherkin-unit-tests': Rule;
28
28
  'tests-dir-helpers-in-fixtures': Rule;
29
+ 'expect-boolean-predicate': Rule;
30
+ 'vitest-from-effect-vitest': Rule;
29
31
  'no-io-module-in-source-test': Rule;
30
32
  'tests-import-public-api': Rule;
31
33
  'differential-test-requires-harness': Rule;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@systemfsoftware/oxlint-plugin-test-discipline",
3
3
  "license": "Apache-2.0",
4
- "version": "3.8.0",
4
+ "version": "3.9.0",
5
5
  "author": "Ryan Lee <drdgvhbh@gmail.com>",
6
6
  "repository": {
7
7
  "type": "git",
@@ -44,28 +44,28 @@
44
44
  "devDependencies": {
45
45
  "@microsoft/api-extractor": "^7.59.1",
46
46
  "@systemfsoftware/arethetypeswrong-cli": "^4.2.0",
47
+ "@systemfsoftware/stryker-config": "^0.1.0",
47
48
  "@systemfsoftware/stryker-ignorer-effect-schema-declarations": "^0.1.1",
48
49
  "@systemfsoftware/stryker-ignorer-in-source-vitest-block": "^0.1.1",
49
50
  "@systemfsoftware/stryker-js": "^10.1.1",
50
51
  "@systemfsoftware/stryker-js-typescript-checker": "^7.0.5",
51
52
  "@systemfsoftware/stryker-js-vitest-runner": "^7.1.1",
52
53
  "@systemfsoftware/stryker-test-contribution": "^3.0.3",
54
+ "@systemfsoftware/tsconfig": "^2.0.1",
55
+ "@systemfsoftware/tsdown-config": "^0.1.0",
56
+ "@systemfsoftware/vitest-config": "^0.1.0",
53
57
  "@types/node": "^26",
54
58
  "@vitest/coverage-v8": "^5",
55
- "effect": "4.0.0-rc.116",
59
+ "effect": "4.0.0-rc.117",
56
60
  "fast-check": "^4",
57
61
  "oxlint": "~1.82.0",
58
62
  "rimraf": "^6.1.3",
59
63
  "tsdown": "^0.23.0",
60
64
  "typescript": "^7",
61
- "vitest": "^5",
62
- "@systemfsoftware/stryker-config": "^0.1.0",
63
- "@systemfsoftware/vitest-config": "^0.1.0",
64
- "@systemfsoftware/tsconfig": "^1.3.6",
65
- "@systemfsoftware/tsdown-config": "^0.1.0"
65
+ "vitest": "^5"
66
66
  },
67
67
  "peerDependencies": {
68
- "effect": "4.0.0-rc.116",
68
+ "effect": "4.0.0-rc.117",
69
69
  "typescript": ">=5.0.0"
70
70
  },
71
71
  "publishConfig": {
@@ -74,7 +74,7 @@
74
74
  "scripts": {
75
75
  "build": "tsdown -l warn && pnpm api:check",
76
76
  "clean": "rimraf dist",
77
- "typecheck": "tsc --noEmit --incremental",
77
+ "typecheck": "tsc -b",
78
78
  "test": "vitest run",
79
79
  "mutation": "stryker run",
80
80
  "mutation:full": "stryker run --force",