@systemfsoftware/oxlint-plugin-test-discipline 3.6.0 → 3.8.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 +14 -0
- package/README.md +2 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +360 -61
- package/dist/oxlint-plugin-test-discipline-untrimmed.d.ts +2 -0
- package/dist/oxlint-plugin-test-discipline.d.ts +2 -0
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @systemfsoftware/oxlint-plugin-test-discipline
|
|
2
2
|
|
|
3
|
+
## 3.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- New rule `ban-raw-span-name-emit` at error: a raw span name can no longer enter through an emit API. Calls to `startSpan`, `spanBuilder`, or `startActiveSpan` — bare or as a member call — are flagged when the first argument is a string literal or a template literal, interpolated templates included. The declared-span spelling is untouched: `Span.declare({ id, name, attrs })` and the `start(attrs)` it returns never trigger the rule, and emit calls given a dynamic name are left alone. The recommended preset enables it at error.
|
|
8
|
+
|
|
9
|
+
- New rule `trace-test-requires-taxonomy` at error: a test file named with the `trace.test` suffix is a trace spec, and a trace spec cannot quietly assert HTTP status. It requires an import binding from `@systemfsoftware/trace-spec`, forbids raw emit calls (`startSpan`, `spanBuilder`, `startActiveSpan`), and forbids terminating a case on an HTTP status or body assertion in these shapes: `expect(res.status)`, `toHaveProperty('status')` (also `statusText`, `body`, dotted paths under them, and segment arrays like `['body', 'items']`), or an object matcher (`toMatchObject`, `toEqual`, `toStrictEqual`, `expect.objectContaining`) whose object literal carries one of those keys at any depth (nested objects and array elements), also through `.resolves`, `.rejects`, and `.not`. `test-suffix-outside-src` now admits the `trace.test` suffix. The recommended preset enables the new rule at error.
|
|
10
|
+
|
|
11
|
+
## 3.7.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- Enable `no-pseudo-gherkin-unit-tests` in the recommended rule set: every `*.integration.test.ts` Feature builder must declare its collaborator environment with `.withLayer(...)` or `.withScenarioLayer(...)`. The diagnostic now names `Layer.empty` as the remedy for features whose collaborators are in memory.
|
|
16
|
+
|
|
3
17
|
## 3.6.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Oxlint rules enforcing property-based test laws, test placement, and test naming
|
|
|
9
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
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
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
|
|
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
13
|
| `tests-dir-helpers-in-fixtures` | Under `tests/`, the only non-test modules are helpers and fixtures, and they live inside `tests/__fixtures__/`. |
|
|
14
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
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`. |
|
|
@@ -23,6 +23,7 @@ Oxlint rules enforcing property-based test laws, test placement, and test naming
|
|
|
23
23
|
| `*.workflow.property.test.ts` | Property | none — pure core | ONLY under `src/` |
|
|
24
24
|
| `*.integration.test.ts` | Behaviour | permitted, at ports only | NEVER under `src/` |
|
|
25
25
|
| `*.differential.test.ts` | Differential | permitted, at ports only | NEVER under `src/` |
|
|
26
|
+
| `*.trace.test.ts` | Trace spec | none — observes graphs | NEVER under `src/` |
|
|
26
27
|
|
|
27
28
|
## Enrollment
|
|
28
29
|
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ declare const _default: {
|
|
|
28
28
|
'no-io-module-in-source-test': import("@oxlint/plugins").Rule;
|
|
29
29
|
'tests-import-public-api': import("@oxlint/plugins").Rule;
|
|
30
30
|
'differential-test-requires-harness': import("@oxlint/plugins").Rule;
|
|
31
|
+
'ban-raw-span-name-emit': import("@oxlint/plugins").Rule;
|
|
32
|
+
'trace-test-requires-taxonomy': import("@oxlint/plugins").Rule;
|
|
31
33
|
};
|
|
32
34
|
configs: {
|
|
33
35
|
recommended: {
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
import { Array as Array$1, Effect, Option, Schema } from "effect";
|
|
3
|
+
//#region src/rules/ban-raw-span-name-emit.config.ts
|
|
4
|
+
const EMIT_CALLEES = [
|
|
5
|
+
"startActiveSpan",
|
|
6
|
+
"spanBuilder",
|
|
7
|
+
"startSpan"
|
|
8
|
+
];
|
|
9
|
+
const EXPECTED$3 = "a span declared once with Span.declare({ id, name, attrs }) and started through the start(attrs) it returns";
|
|
10
|
+
const FIX$3 = "declare the span with Span.declare({ id, name, attrs }) and emit it with SomeSpan.start(attrs)";
|
|
11
|
+
const meta$25 = {
|
|
12
|
+
type: "problem",
|
|
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
|
+
schema: [],
|
|
15
|
+
messages: { banRawSpanName: "'{{name}}' is forbidden at {{callee}}. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}." }
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/rules/ban-raw-span-name-emit.ts
|
|
19
|
+
const INTERPOLATION = "${}";
|
|
20
|
+
const isEmitCallee$1 = (name) => EMIT_CALLEES.some((emit) => emit === name);
|
|
21
|
+
const memberPropertyName = (callee) => {
|
|
22
|
+
if (callee.computed) return null;
|
|
23
|
+
if (callee.property.type !== "Identifier") return null;
|
|
24
|
+
return callee.property.name;
|
|
25
|
+
};
|
|
26
|
+
const calleeName$1 = (callee) => {
|
|
27
|
+
if (callee.type === "Identifier") return callee.name;
|
|
28
|
+
if (callee.type === "MemberExpression") return memberPropertyName(callee);
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
const templateName = (node) => node.quasis.map((quasi) => quasi.value.raw).join(INTERPOLATION);
|
|
32
|
+
const inlineName = (arg) => {
|
|
33
|
+
if (arg.type === "Literal") return typeof arg.value === "string" ? arg.value : null;
|
|
34
|
+
if (arg.type === "TemplateLiteral") return templateName(arg);
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
const argName = (arg) => {
|
|
38
|
+
if (arg === void 0) return null;
|
|
39
|
+
return inlineName(arg);
|
|
40
|
+
};
|
|
41
|
+
const inspect = (node) => {
|
|
42
|
+
const callee = calleeName$1(node.callee);
|
|
43
|
+
if (callee === null || !isEmitCallee$1(callee)) return null;
|
|
44
|
+
const name = argName(node.arguments[0]);
|
|
45
|
+
if (name === null) return null;
|
|
46
|
+
return {
|
|
47
|
+
callee,
|
|
48
|
+
name
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const banRawSpanNameEmit = defineRule({
|
|
52
|
+
meta: meta$25,
|
|
53
|
+
create(context) {
|
|
54
|
+
return { CallExpression(node) {
|
|
55
|
+
const emitCall = inspect(node);
|
|
56
|
+
if (emitCall === null) return;
|
|
57
|
+
context.report({
|
|
58
|
+
node,
|
|
59
|
+
messageId: "banRawSpanName",
|
|
60
|
+
data: {
|
|
61
|
+
name: emitCall.name,
|
|
62
|
+
callee: emitCall.callee,
|
|
63
|
+
expected: EXPECTED$3,
|
|
64
|
+
actual: emitCall.name,
|
|
65
|
+
fix: FIX$3
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
} };
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
//#endregion
|
|
3
72
|
//#region src/rules/path.config.ts
|
|
4
73
|
const TEST_BASENAME = /\.(?:test|spec)\.[cm]?tsx?$/;
|
|
5
74
|
const SANCTIONED_TEST_DIRS = /* @__PURE__ */ new Set(["tests"]);
|
|
@@ -28,6 +97,7 @@ const SCHEMA_SUFFIX = ".schema.test.ts";
|
|
|
28
97
|
const WORKFLOW_TEST_BASENAME = /^[^.]+\.workflow\.property\.test\.ts$/;
|
|
29
98
|
const GHERKIN_PACKAGE = "@systemfsoftware/effect-gherkin-spec";
|
|
30
99
|
const DIFFERENTIAL_PACKAGE = "@systemfsoftware/differential-spec";
|
|
100
|
+
const TRACE_SPEC_PACKAGE = "@systemfsoftware/trace-spec";
|
|
31
101
|
const FOREIGN_RUNNERS = /* @__PURE__ */ new Set(["vitest", "@effect/vitest"]);
|
|
32
102
|
const RUNNER_NAMES = /* @__PURE__ */ new Set([
|
|
33
103
|
"it",
|
|
@@ -42,7 +112,7 @@ const NO_SUBJECT_IMPORT_NAME = "a *.integration.test.ts that reaches no package
|
|
|
42
112
|
const NO_SUBJECT_IMPORT_EXPECTED = "an import of the package code under test";
|
|
43
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";
|
|
44
114
|
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.";
|
|
45
|
-
const meta$
|
|
115
|
+
const meta$24 = {
|
|
46
116
|
type: "problem",
|
|
47
117
|
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." },
|
|
48
118
|
schema: [],
|
|
@@ -155,7 +225,7 @@ const DIST_SEGMENT = /(?:^|\/)dist\//;
|
|
|
155
225
|
* path names nothing satisfies the rule the same way a real one does.
|
|
156
226
|
*/
|
|
157
227
|
const behaviourExercisesUseCase = defineRule({
|
|
158
|
-
meta: meta$
|
|
228
|
+
meta: meta$24,
|
|
159
229
|
create(context) {
|
|
160
230
|
let reached = false;
|
|
161
231
|
return {
|
|
@@ -205,7 +275,7 @@ const TOO_MANY_FEATURES_NAME = "a *.integration.test.ts accumulating multiple Fe
|
|
|
205
275
|
const TOO_MANY_FEATURES_EXPECTED = "exactly one Feature(...) — every additional one signals a junk drawer";
|
|
206
276
|
const TOO_MANY_FEATURES_ACTUAL = "a behaviour file with two or more Feature(...) calls";
|
|
207
277
|
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.";
|
|
208
|
-
const meta$
|
|
278
|
+
const meta$23 = {
|
|
209
279
|
type: "problem",
|
|
210
280
|
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." },
|
|
211
281
|
schema: [],
|
|
@@ -250,7 +320,7 @@ const hasAnyFeatureCall = (program) => {
|
|
|
250
320
|
return false;
|
|
251
321
|
};
|
|
252
322
|
const behaviourOneFeaturePerFile = defineRule({
|
|
253
|
-
meta: meta$
|
|
323
|
+
meta: meta$23,
|
|
254
324
|
create(context) {
|
|
255
325
|
return { "Program:exit"(node) {
|
|
256
326
|
if (!isBehaviourTest$2(basenameOf(context.filename))) return;
|
|
@@ -291,7 +361,7 @@ const MISSING_MAKE_FEATURE_NAME = "a *.integration.test.ts without makeFeature";
|
|
|
291
361
|
const MISSING_MAKE_FEATURE_EXPECTED = "makeFeature imported from @systemfsoftware/effect-gherkin-spec";
|
|
292
362
|
const MISSING_MAKE_FEATURE_ACTUAL = "a behaviour file that never constructs a Gherkin feature";
|
|
293
363
|
const MISSING_MAKE_FEATURE_FIX = "import { makeFeature } from @systemfsoftware/effect-gherkin-spec and declare `const Feature = makeFeature({ it, layer })`";
|
|
294
|
-
const meta$
|
|
364
|
+
const meta$22 = {
|
|
295
365
|
type: "problem",
|
|
296
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." },
|
|
297
367
|
schema: [],
|
|
@@ -319,7 +389,7 @@ const foreignRunnerNameOf = (specifier) => {
|
|
|
319
389
|
};
|
|
320
390
|
const isBehaviourTest$1 = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
|
|
321
391
|
const behaviourTestRequiresGherkin = defineRule({
|
|
322
|
-
meta: meta$
|
|
392
|
+
meta: meta$22,
|
|
323
393
|
create(context) {
|
|
324
394
|
const basename = basenameOf(context.filename);
|
|
325
395
|
return { Program(node) {
|
|
@@ -382,7 +452,7 @@ const INVALID_BEHAVIOR_CASE_FIX = "Convert behavior to PascalCase (e.g., throwEr
|
|
|
382
452
|
const INVALID_CONDITION_CASE_FIX = "Convert condition to PascalCase (e.g., passwordInvalid → PasswordInvalid)";
|
|
383
453
|
const EMPTY_BEHAVIOR_ACTUAL = "Empty string between Should_ and _When_";
|
|
384
454
|
const EMPTY_CONDITION_ACTUAL = "Empty string after _When_";
|
|
385
|
-
const meta$
|
|
455
|
+
const meta$21 = {
|
|
386
456
|
type: "suggestion",
|
|
387
457
|
docs: { description: "Enforce DAMP (Descriptive and Meaningful Phrases) test naming format: Should_[ExpectedBehavior]_When_[Condition]" },
|
|
388
458
|
schema: [],
|
|
@@ -462,7 +532,7 @@ const isTestFunctionCall = (node) => {
|
|
|
462
532
|
return false;
|
|
463
533
|
};
|
|
464
534
|
const dampTestNaming = defineRule({
|
|
465
|
-
meta: meta$
|
|
535
|
+
meta: meta$21,
|
|
466
536
|
create(context) {
|
|
467
537
|
return { CallExpression(node) {
|
|
468
538
|
if (!isTestFunctionCall(node)) return;
|
|
@@ -485,7 +555,7 @@ const dampTestNaming = defineRule({
|
|
|
485
555
|
});
|
|
486
556
|
//#endregion
|
|
487
557
|
//#region src/rules/differential-test-requires-harness.config.ts
|
|
488
|
-
const meta$
|
|
558
|
+
const meta$20 = {
|
|
489
559
|
type: "problem",
|
|
490
560
|
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." },
|
|
491
561
|
schema: [],
|
|
@@ -498,7 +568,7 @@ const meta$19 = {
|
|
|
498
568
|
};
|
|
499
569
|
//#endregion
|
|
500
570
|
//#region src/rules/differential-test-requires-harness.ts
|
|
501
|
-
const HARNESS_PRESCRIPTION = "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)";
|
|
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)";
|
|
502
572
|
const memberObjectName = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
|
|
503
573
|
const calleeName = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName(callee);
|
|
504
574
|
const recordHarnessBindings = (node, bindings) => {
|
|
@@ -507,7 +577,7 @@ const recordHarnessBindings = (node, bindings) => {
|
|
|
507
577
|
};
|
|
508
578
|
const isForeignRunnerImport = (node) => typeof node.source.value === "string" && FOREIGN_RUNNERS.has(node.source.value);
|
|
509
579
|
const differentialTestRequiresHarness = defineRule({
|
|
510
|
-
meta: meta$
|
|
580
|
+
meta: meta$20,
|
|
511
581
|
create(context) {
|
|
512
582
|
if (!context.filename.endsWith(".differential.test.ts")) return {};
|
|
513
583
|
const harnessBindings = /* @__PURE__ */ new Set();
|
|
@@ -521,9 +591,9 @@ const differentialTestRequiresHarness = defineRule({
|
|
|
521
591
|
messageId: "runnerImport",
|
|
522
592
|
data: {
|
|
523
593
|
name: `runner import from ${String(node.source.value)} in a differential test file`,
|
|
524
|
-
expected: HARNESS_PRESCRIPTION,
|
|
594
|
+
expected: HARNESS_PRESCRIPTION$1,
|
|
525
595
|
actual: "a direct vitest / @effect/vitest runner import bypasses the differential oracle",
|
|
526
|
-
fix: `delete the runner import; ${HARNESS_PRESCRIPTION}`
|
|
596
|
+
fix: `delete the runner import; ${HARNESS_PRESCRIPTION$1}`
|
|
527
597
|
}
|
|
528
598
|
});
|
|
529
599
|
violations += 1;
|
|
@@ -539,9 +609,9 @@ const differentialTestRequiresHarness = defineRule({
|
|
|
539
609
|
messageId: "rawRunnerCall",
|
|
540
610
|
data: {
|
|
541
611
|
name: `raw runner call (${name}) in a differential test file`,
|
|
542
|
-
expected: HARNESS_PRESCRIPTION,
|
|
612
|
+
expected: HARNESS_PRESCRIPTION$1,
|
|
543
613
|
actual: `${name}(...) bypasses the differential oracle`,
|
|
544
|
-
fix: `rewrite using ${HARNESS_PRESCRIPTION}`
|
|
614
|
+
fix: `rewrite using ${HARNESS_PRESCRIPTION$1}`
|
|
545
615
|
}
|
|
546
616
|
});
|
|
547
617
|
violations += 1;
|
|
@@ -556,9 +626,9 @@ const differentialTestRequiresHarness = defineRule({
|
|
|
556
626
|
messageId: "missingHarnessImport",
|
|
557
627
|
data: {
|
|
558
628
|
name: `differential test file without ${DIFFERENTIAL_PACKAGE} import`,
|
|
559
|
-
expected: HARNESS_PRESCRIPTION,
|
|
629
|
+
expected: HARNESS_PRESCRIPTION$1,
|
|
560
630
|
actual: "no differential harness import found",
|
|
561
|
-
fix: HARNESS_PRESCRIPTION
|
|
631
|
+
fix: HARNESS_PRESCRIPTION$1
|
|
562
632
|
}
|
|
563
633
|
});
|
|
564
634
|
return;
|
|
@@ -568,9 +638,9 @@ const differentialTestRequiresHarness = defineRule({
|
|
|
568
638
|
messageId: "missingHarnessUsage",
|
|
569
639
|
data: {
|
|
570
640
|
name: `differential test file imports ${DIFFERENTIAL_PACKAGE} but never invokes it`,
|
|
571
|
-
expected: HARNESS_PRESCRIPTION,
|
|
641
|
+
expected: HARNESS_PRESCRIPTION$1,
|
|
572
642
|
actual: "the harness is imported but no Differential.compare or Metamorphic.on chain runs",
|
|
573
|
-
fix: HARNESS_PRESCRIPTION
|
|
643
|
+
fix: HARNESS_PRESCRIPTION$1
|
|
574
644
|
}
|
|
575
645
|
});
|
|
576
646
|
}
|
|
@@ -583,7 +653,7 @@ const NON_PROP_CALL_NAME = "a non-property test call inside an `import.meta.vite
|
|
|
583
653
|
const NON_PROP_CALL_EXPECTED = "only `it.prop` or `it.effect.prop` member-chain calls (standard modifiers included) with boolean predicates";
|
|
584
654
|
const NON_PROP_CALL_ACTUAL = "a bare or member-chain test call other than `it.prop`/`it.effect.prop`";
|
|
585
655
|
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";
|
|
586
|
-
const meta$
|
|
656
|
+
const meta$19 = {
|
|
587
657
|
type: "problem",
|
|
588
658
|
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." },
|
|
589
659
|
schema: [],
|
|
@@ -647,7 +717,7 @@ const rootNameOf = (node) => {
|
|
|
647
717
|
if (node.type === "CallExpression") return rootNameOf(node.callee);
|
|
648
718
|
};
|
|
649
719
|
const inSourceTestPropOnly = defineRule({
|
|
650
|
-
meta: meta$
|
|
720
|
+
meta: meta$19,
|
|
651
721
|
create(context) {
|
|
652
722
|
const filename = context.filename;
|
|
653
723
|
const basename = basenameOf(filename);
|
|
@@ -692,7 +762,7 @@ const NO_PRIVATE_TARGET_NAME = "an `import.meta.vitest` block touching no privat
|
|
|
692
762
|
const NO_PRIVATE_TARGET_EXPECTED = "an in-source test exercising a non-exported module-level binding";
|
|
693
763
|
const NO_PRIVATE_TARGET_ACTUAL = "an in-source block referencing only exported or imported names";
|
|
694
764
|
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";
|
|
695
|
-
const meta$
|
|
765
|
+
const meta$18 = {
|
|
696
766
|
type: "problem",
|
|
697
767
|
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/." },
|
|
698
768
|
schema: [],
|
|
@@ -726,7 +796,7 @@ const collectPrivateNames = (body, out) => {
|
|
|
726
796
|
}
|
|
727
797
|
};
|
|
728
798
|
const inSourceTestTargetsPrivate = defineRule({
|
|
729
|
-
meta: meta$
|
|
799
|
+
meta: meta$18,
|
|
730
800
|
create(context) {
|
|
731
801
|
const filename = context.filename;
|
|
732
802
|
const basename = basenameOf(filename);
|
|
@@ -785,7 +855,7 @@ const inSourceTestTargetsPrivate = defineRule({
|
|
|
785
855
|
//#endregion
|
|
786
856
|
//#region src/rules/no-assert-in-property.config.ts
|
|
787
857
|
const MESSAGE$6 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
788
|
-
const meta$
|
|
858
|
+
const meta$17 = {
|
|
789
859
|
type: "problem",
|
|
790
860
|
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." },
|
|
791
861
|
schema: [],
|
|
@@ -823,7 +893,7 @@ const isInsidePropPredicate = (node) => {
|
|
|
823
893
|
return isInsidePropPredicate(parent);
|
|
824
894
|
};
|
|
825
895
|
const noAssertInProperty = defineRule({
|
|
826
|
-
meta: meta$
|
|
896
|
+
meta: meta$17,
|
|
827
897
|
create(context) {
|
|
828
898
|
return { CallExpression(node) {
|
|
829
899
|
const callee = node.callee;
|
|
@@ -876,7 +946,7 @@ const SKIP_WALK_KEYS = /* @__PURE__ */ new Set([
|
|
|
876
946
|
"range",
|
|
877
947
|
"parent"
|
|
878
948
|
]);
|
|
879
|
-
const meta$
|
|
949
|
+
const meta$16 = {
|
|
880
950
|
type: "problem",
|
|
881
951
|
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." },
|
|
882
952
|
schema: [],
|
|
@@ -944,7 +1014,7 @@ const importedNames = (program) => {
|
|
|
944
1014
|
return names;
|
|
945
1015
|
};
|
|
946
1016
|
const noBehaviourlessAssertion = defineRule({
|
|
947
|
-
meta: meta$
|
|
1017
|
+
meta: meta$16,
|
|
948
1018
|
create(context) {
|
|
949
1019
|
if (!TEST_FILE.test(context.filename)) return {};
|
|
950
1020
|
const imported = importedNames(context.sourceCode.ast);
|
|
@@ -1014,7 +1084,7 @@ const IO_SOURCE_TEST_NAME = "An in-source `import.meta.vitest` test block";
|
|
|
1014
1084
|
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";
|
|
1015
1085
|
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`";
|
|
1016
1086
|
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";
|
|
1017
|
-
const meta$
|
|
1087
|
+
const meta$15 = {
|
|
1018
1088
|
type: "problem",
|
|
1019
1089
|
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." },
|
|
1020
1090
|
schema: [],
|
|
@@ -1035,7 +1105,7 @@ const bindingBase = (callee) => {
|
|
|
1035
1105
|
if (callee.type === "Identifier") return callee.name;
|
|
1036
1106
|
};
|
|
1037
1107
|
const noIoModuleInSourceTest = defineRule({
|
|
1038
|
-
meta: meta$
|
|
1108
|
+
meta: meta$15,
|
|
1039
1109
|
create(context) {
|
|
1040
1110
|
/** Local binding name -> specifier it was (non-type) imported from. */
|
|
1041
1111
|
const ioBindings = {};
|
|
@@ -1100,7 +1170,7 @@ const VIOLATION_NAME$2 = "quantification nested inside a property predicate";
|
|
|
1100
1170
|
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";
|
|
1101
1171
|
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";
|
|
1102
1172
|
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";
|
|
1103
|
-
const meta$
|
|
1173
|
+
const meta$14 = {
|
|
1104
1174
|
type: "problem",
|
|
1105
1175
|
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." },
|
|
1106
1176
|
schema: [Schema.toJsonSchemaDocument(Options$2).schema],
|
|
@@ -1251,7 +1321,7 @@ const check = (context, call, predicate) => {
|
|
|
1251
1321
|
* and excluded; DISCHARGED_BY in the suite pins the unbounded recipe shape.
|
|
1252
1322
|
*/
|
|
1253
1323
|
const noNestedQuantification = defineRule({
|
|
1254
|
-
meta: meta$
|
|
1324
|
+
meta: meta$14,
|
|
1255
1325
|
create(context) {
|
|
1256
1326
|
const options = Schema.decodeUnknownSync(Options$2)(context.options[0] ?? {});
|
|
1257
1327
|
const exempt = new Set(options.exempt);
|
|
@@ -1269,10 +1339,10 @@ const noNestedQuantification = defineRule({
|
|
|
1269
1339
|
//#endregion
|
|
1270
1340
|
//#region src/rules/no-pseudo-gherkin-unit-tests.config.ts
|
|
1271
1341
|
const NO_LAYER_IN_FEATURE_NAME = "a *.integration.test.ts feature with no environment double";
|
|
1272
|
-
const NO_LAYER_IN_FEATURE_EXPECTED = "a
|
|
1273
|
-
const NO_LAYER_IN_FEATURE_ACTUAL = "a Feature(...) call
|
|
1274
|
-
const NO_LAYER_IN_FEATURE_FIX = "an integration test under WGI-CLS1
|
|
1275
|
-
const meta$
|
|
1342
|
+
const NO_LAYER_IN_FEATURE_EXPECTED = "a Feature builder chained with .withLayer(layer) or .withScenarioLayer(layer)";
|
|
1343
|
+
const NO_LAYER_IN_FEATURE_ACTUAL = "a Feature(...) call without .withLayer or .withScenarioLayer";
|
|
1344
|
+
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 = {
|
|
1276
1346
|
type: "problem",
|
|
1277
1347
|
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." },
|
|
1278
1348
|
schema: [],
|
|
@@ -1307,7 +1377,7 @@ const findRootFeatureCall = (callNode) => {
|
|
|
1307
1377
|
return null;
|
|
1308
1378
|
};
|
|
1309
1379
|
const noPseudoGherkinUnitTests = defineRule({
|
|
1310
|
-
meta: meta$
|
|
1380
|
+
meta: meta$13,
|
|
1311
1381
|
create(context) {
|
|
1312
1382
|
if (!isBehaviourTest(basenameOf(context.filename))) return {};
|
|
1313
1383
|
return { CallExpression(node) {
|
|
@@ -1332,7 +1402,7 @@ const noPseudoGherkinUnitTests = defineRule({
|
|
|
1332
1402
|
//#endregion
|
|
1333
1403
|
//#region src/rules/no-silent-return.config.ts
|
|
1334
1404
|
const MESSAGE$4 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
1335
|
-
const meta$
|
|
1405
|
+
const meta$12 = {
|
|
1336
1406
|
type: "problem",
|
|
1337
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." },
|
|
1338
1408
|
schema: [],
|
|
@@ -1480,7 +1550,7 @@ const checkFn = (context, fn) => {
|
|
|
1480
1550
|
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");
|
|
1481
1551
|
};
|
|
1482
1552
|
const noSilentReturn = defineRule({
|
|
1483
|
-
meta: meta$
|
|
1553
|
+
meta: meta$12,
|
|
1484
1554
|
create(context) {
|
|
1485
1555
|
return { CallExpression(node) {
|
|
1486
1556
|
if (!isPropCallee(node.callee)) return;
|
|
@@ -1509,7 +1579,7 @@ const SCHEMA_TEST_DETAIL = {
|
|
|
1509
1579
|
actual: "an authored *.schema.test.ts restating generated coverage",
|
|
1510
1580
|
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"
|
|
1511
1581
|
};
|
|
1512
|
-
const meta$
|
|
1582
|
+
const meta$11 = {
|
|
1513
1583
|
type: "problem",
|
|
1514
1584
|
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." },
|
|
1515
1585
|
schema: [Schema.toJsonSchemaDocument(Options$1).schema],
|
|
@@ -1523,7 +1593,7 @@ const meta$10 = {
|
|
|
1523
1593
|
//#region src/rules/no-test-file-in-src.ts
|
|
1524
1594
|
const violationOf = (basename, isPropertyTest, dir) => basename.endsWith(".schema.test.ts") ? ["schemaTestInSrc", SCHEMA_TEST_DETAIL] : isPropertyTest ? ["propertyTestOutsideTestsDir", propertyTestLocationDetail(dir)] : ["testFileInSrc", testFileInSrcDetail(dir)];
|
|
1525
1595
|
const noTestFileInSrc = defineRule({
|
|
1526
|
-
meta: meta$
|
|
1596
|
+
meta: meta$11,
|
|
1527
1597
|
create(context) {
|
|
1528
1598
|
const { sanctionedDirs } = Schema.decodeUnknownSync(Options$1)(context.options[0] ?? {});
|
|
1529
1599
|
const basename = basenameOf(context.filename);
|
|
@@ -1576,7 +1646,7 @@ const PREDICATE_SYMBOLS = /* @__PURE__ */ new Set([
|
|
|
1576
1646
|
const NULLARY_PREDICATE_SYMBOLS = /* @__PURE__ */ new Set(["⊥"]);
|
|
1577
1647
|
const PASCAL_CASE = /^[A-Z][a-z][a-zA-Z0-9]*$/;
|
|
1578
1648
|
const DAMP_WORDS = /When|Should|Given|Then|Otherwise|After|Before/;
|
|
1579
|
-
const meta$
|
|
1649
|
+
const meta$10 = {
|
|
1580
1650
|
type: "suggestion",
|
|
1581
1651
|
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." },
|
|
1582
1652
|
schema: [],
|
|
@@ -1620,7 +1690,7 @@ const parseSegments = (name) => {
|
|
|
1620
1690
|
};
|
|
1621
1691
|
};
|
|
1622
1692
|
const pbtNaming = defineRule({
|
|
1623
|
-
meta: meta$
|
|
1693
|
+
meta: meta$10,
|
|
1624
1694
|
create(context) {
|
|
1625
1695
|
return { CallExpression(node) {
|
|
1626
1696
|
if (!isPropCall(node)) return;
|
|
@@ -1728,7 +1798,7 @@ const SCHEMA_NAMESPACE_NAMES = {
|
|
|
1728
1798
|
};
|
|
1729
1799
|
const FASTCHECK_NAMESPACE_NAMES = { FastCheck: true };
|
|
1730
1800
|
const COMBINATOR_CALLEES = { pipe: true };
|
|
1731
|
-
const meta$
|
|
1801
|
+
const meta$9 = {
|
|
1732
1802
|
type: "problem",
|
|
1733
1803
|
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." },
|
|
1734
1804
|
schema: [],
|
|
@@ -2011,7 +2081,7 @@ const checkPropCall$1 = (provenance, context, call) => {
|
|
|
2011
2081
|
}
|
|
2012
2082
|
};
|
|
2013
2083
|
const propArbitrarySchemaOrigin = defineRule({
|
|
2014
|
-
meta: meta$
|
|
2084
|
+
meta: meta$9,
|
|
2015
2085
|
create(context) {
|
|
2016
2086
|
const provenance = new Provenance(context.sourceCode.getScope);
|
|
2017
2087
|
const collect = (value, out) => {
|
|
@@ -2075,7 +2145,7 @@ const VIOLATION_NAME = "a hand-rolled recursive schema fixture inside a test blo
|
|
|
2075
2145
|
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";
|
|
2076
2146
|
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";
|
|
2077
2147
|
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";
|
|
2078
|
-
const meta$
|
|
2148
|
+
const meta$8 = {
|
|
2079
2149
|
type: "problem",
|
|
2080
2150
|
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." },
|
|
2081
2151
|
schema: [],
|
|
@@ -2253,7 +2323,7 @@ const isNamedBuilderOrigin = (union, getScope) => {
|
|
|
2253
2323
|
return false;
|
|
2254
2324
|
};
|
|
2255
2325
|
const propFixtureSchemaOrigin = defineRule({
|
|
2256
|
-
meta: meta$
|
|
2326
|
+
meta: meta$8,
|
|
2257
2327
|
create(context) {
|
|
2258
2328
|
const getScope = context.sourceCode.getScope;
|
|
2259
2329
|
return { CallExpression(node) {
|
|
@@ -2290,7 +2360,7 @@ const NO_FUNCTION_NAME = "a property that tests no function";
|
|
|
2290
2360
|
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";
|
|
2291
2361
|
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";
|
|
2292
2362
|
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";
|
|
2293
|
-
const meta$
|
|
2363
|
+
const meta$7 = {
|
|
2294
2364
|
type: "problem",
|
|
2295
2365
|
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." },
|
|
2296
2366
|
schema: [],
|
|
@@ -2436,7 +2506,7 @@ const checkPropCall = (provenance, context, call) => {
|
|
|
2436
2506
|
});
|
|
2437
2507
|
};
|
|
2438
2508
|
const propGeneratedLawDuplicate = defineRule({
|
|
2439
|
-
meta: meta$
|
|
2509
|
+
meta: meta$7,
|
|
2440
2510
|
create(context) {
|
|
2441
2511
|
const provenance = new Provenance(context.sourceCode.getScope);
|
|
2442
2512
|
const collect = (value, out) => {
|
|
@@ -2480,7 +2550,7 @@ const propGeneratedLawDuplicate = defineRule({
|
|
|
2480
2550
|
//#region src/rules/property-file-purity.config.ts
|
|
2481
2551
|
const PROPERTY_TEST_SUFFIX = ".property.test.ts";
|
|
2482
2552
|
const MESSAGE = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
2483
|
-
const meta$
|
|
2553
|
+
const meta$6 = {
|
|
2484
2554
|
type: "problem",
|
|
2485
2555
|
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." },
|
|
2486
2556
|
schema: [],
|
|
@@ -2596,7 +2666,7 @@ const createScenarioFileVisitors = (context) => ({
|
|
|
2596
2666
|
});
|
|
2597
2667
|
const kindOf = (filename) => filename.endsWith(".differential.test.ts") ? DIFFERENTIAL_FILE_KIND : PROPERTY_FILE_KIND;
|
|
2598
2668
|
const propertyFilePurity = defineRule({
|
|
2599
|
-
meta: meta$
|
|
2669
|
+
meta: meta$6,
|
|
2600
2670
|
create(context) {
|
|
2601
2671
|
if (!isTestFile(basenameOf(context.filename))) return {};
|
|
2602
2672
|
if (context.filename.endsWith(".property.test.ts") || context.filename.endsWith(".differential.test.ts")) return createPropertyFileVisitors(context, kindOf(context.filename));
|
|
@@ -2612,7 +2682,7 @@ const UNSANCTIONED_CELL_FIX = "rename it <stem>.workflow.property.test.ts beside
|
|
|
2612
2682
|
const MISSING_CELL_TEST_EXPECTED = "a test for every cell suffix the consumer lists in cellsRequiringTest";
|
|
2613
2683
|
const MISSING_CELL_TEST_ACTUAL = "a declared cell whose own module carries no `if (import.meta.vitest)` block";
|
|
2614
2684
|
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";
|
|
2615
|
-
const meta$
|
|
2685
|
+
const meta$5 = {
|
|
2616
2686
|
type: "problem",
|
|
2617
2687
|
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." },
|
|
2618
2688
|
schema: [Schema.toJsonSchemaDocument(Options).schema],
|
|
@@ -2625,7 +2695,7 @@ const meta$4 = {
|
|
|
2625
2695
|
//#region src/rules/src-property-test-cell.ts
|
|
2626
2696
|
const carriesInSourceBlock = (body) => body.some((statement) => statement.type === "IfStatement" && isVitestGuard(statement.test));
|
|
2627
2697
|
const srcPropertyTestCell = defineRule({
|
|
2628
|
-
meta: meta$
|
|
2698
|
+
meta: meta$5,
|
|
2629
2699
|
create(context) {
|
|
2630
2700
|
const { cellsRequiringTest } = Schema.decodeUnknownSync(Options)(context.options[0] ?? {});
|
|
2631
2701
|
const filename = context.filename;
|
|
@@ -2700,15 +2770,15 @@ const testFileOutsideTestsDir = defineRule({
|
|
|
2700
2770
|
});
|
|
2701
2771
|
//#endregion
|
|
2702
2772
|
//#region src/rules/test-suffix-outside-src.config.ts
|
|
2703
|
-
const UNSANCTIONED_SUFFIX_EXPECTED = "
|
|
2704
|
-
const UNSANCTIONED_SUFFIX_ACTUAL = "
|
|
2705
|
-
const UNSANCTIONED_SUFFIX_FIX = "
|
|
2773
|
+
const UNSANCTIONED_SUFFIX_EXPECTED = "*.integration.test.ts, *.differential.test.ts, or *.trace.test.ts";
|
|
2774
|
+
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";
|
|
2706
2776
|
//#endregion
|
|
2707
2777
|
//#region src/rules/test-suffix-outside-src.ts
|
|
2708
2778
|
const testSuffixOutsideSrc = defineRule({
|
|
2709
2779
|
meta: {
|
|
2710
2780
|
type: "problem",
|
|
2711
|
-
docs: { description: "Outside src/, a test file
|
|
2781
|
+
docs: { description: "Outside src/, a test file ends .integration.test.ts, .differential.test.ts, or .trace.test.ts." },
|
|
2712
2782
|
schema: [],
|
|
2713
2783
|
messages: { unsanctionedSuffix: MESSAGE$7 }
|
|
2714
2784
|
},
|
|
@@ -2717,7 +2787,7 @@ const testSuffixOutsideSrc = defineRule({
|
|
|
2717
2787
|
if (isUnderSrc(filename)) return {};
|
|
2718
2788
|
const basename = basenameOf(filename);
|
|
2719
2789
|
if (!isTestFile(basename)) return {};
|
|
2720
|
-
if (basename.endsWith(".integration.test.ts") || basename.endsWith(".differential.test.ts")) return {};
|
|
2790
|
+
if (basename.endsWith(".integration.test.ts") || basename.endsWith(".differential.test.ts") || basename.endsWith(".trace.test.ts")) return {};
|
|
2721
2791
|
return { Program(node) {
|
|
2722
2792
|
context.report({
|
|
2723
2793
|
node,
|
|
@@ -2773,7 +2843,7 @@ const testsDirHelpersInFixtures = defineRule({
|
|
|
2773
2843
|
const REACH_IN_EXPECTED = "a package name or subpath, or a sibling helper under the test tree";
|
|
2774
2844
|
const REACH_IN_ACTUAL = "a relative import that reaches src or climbs into an internal folder";
|
|
2775
2845
|
const REACH_IN_FIX = "rewrite onto the published package name when the binding is public. Delete the test when the subject is an internal";
|
|
2776
|
-
const meta = {
|
|
2846
|
+
const meta$1 = {
|
|
2777
2847
|
type: "problem",
|
|
2778
2848
|
docs: { description: "Forbid package-level tests from relative-importing src or climbing into an internal folder" },
|
|
2779
2849
|
schema: [],
|
|
@@ -2795,7 +2865,7 @@ const specifierOf = (node) => {
|
|
|
2795
2865
|
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
2796
2866
|
};
|
|
2797
2867
|
const testsImportPublicApi = defineRule({
|
|
2798
|
-
meta,
|
|
2868
|
+
meta: meta$1,
|
|
2799
2869
|
create(context) {
|
|
2800
2870
|
if (!isInTestsImportScope(context.filename)) return {};
|
|
2801
2871
|
const reportIfForbidden = (sourceNode) => {
|
|
@@ -2832,6 +2902,230 @@ const testsImportPublicApi = defineRule({
|
|
|
2832
2902
|
}
|
|
2833
2903
|
});
|
|
2834
2904
|
//#endregion
|
|
2905
|
+
//#region src/rules/trace-test-requires-taxonomy.config.ts
|
|
2906
|
+
const HARNESS_PRESCRIPTION = "import { Suite, Case, Rel, Graph, Observe, Stimulus } from @systemfsoftware/trace-spec and terminate every case on a relation hold — .holds(Rel.all(Rel.exists(PlaceOrder), Rel.child(PlaceOrder, PaymentCapture)))";
|
|
2907
|
+
const MISSING_HARNESS_ACTUAL = "no trace-spec import found in a file named as a trace spec";
|
|
2908
|
+
const HTTP_MEMBERS = {
|
|
2909
|
+
body: true,
|
|
2910
|
+
status: true,
|
|
2911
|
+
statusText: true
|
|
2912
|
+
};
|
|
2913
|
+
const HTTP_PROPERTY_MATCHER = "toHaveProperty";
|
|
2914
|
+
const HTTP_OBJECT_MATCHERS = {
|
|
2915
|
+
objectContaining: true,
|
|
2916
|
+
toEqual: true,
|
|
2917
|
+
toMatchObject: true,
|
|
2918
|
+
toStrictEqual: true
|
|
2919
|
+
};
|
|
2920
|
+
const HTTP_TERMINATION_EXPECTED = "a case terminated on a relation hold — .holds(Rel.all(...)) over the declared span graph";
|
|
2921
|
+
const HTTP_TERMINATION_ACTUAL = "an HTTP response assertion terminating the case";
|
|
2922
|
+
const HTTP_TERMINATION_FIX = "replace the HTTP assertion with a relation over the observed spans, e.g. .holds(Rel.all(Rel.exists(PlaceOrder), Rel.child(PlaceOrder, PaymentCapture)))";
|
|
2923
|
+
const RAW_EMIT_EXPECTED = "spans declared once with Span.declare({ id, name, attrs }) inside the traced cell; the spec observes the decoded graph";
|
|
2924
|
+
const RAW_EMIT_ACTUAL = "a raw span emit call inside a trace spec";
|
|
2925
|
+
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 = {
|
|
2927
|
+
type: "problem",
|
|
2928
|
+
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
|
+
schema: [],
|
|
2930
|
+
messages: {
|
|
2931
|
+
missingHarnessImport: MESSAGE$7,
|
|
2932
|
+
httpTermination: MESSAGE$7,
|
|
2933
|
+
rawEmitCall: MESSAGE$7
|
|
2934
|
+
}
|
|
2935
|
+
};
|
|
2936
|
+
//#endregion
|
|
2937
|
+
//#region src/rules/trace-test-requires-taxonomy.ts
|
|
2938
|
+
const asIdentifierName = (expression) => {
|
|
2939
|
+
if (expression === void 0 || expression.type !== "Identifier") return null;
|
|
2940
|
+
return expression.name;
|
|
2941
|
+
};
|
|
2942
|
+
const isEmitCallee = (name) => EMIT_CALLEES.some((emit) => emit === name);
|
|
2943
|
+
const isExpectCall = (node) => node?.type === "CallExpression" && asIdentifierName(node.callee) === "expect";
|
|
2944
|
+
const expectCallUnder = (node) => {
|
|
2945
|
+
const callee = node.callee;
|
|
2946
|
+
if (callee.type !== "MemberExpression" || !isExpectCall(callee.object)) return null;
|
|
2947
|
+
return callee.object;
|
|
2948
|
+
};
|
|
2949
|
+
const memberReadName = (argument) => {
|
|
2950
|
+
if (argument === void 0 || argument.type !== "MemberExpression") return null;
|
|
2951
|
+
return asIdentifierName(argument.property);
|
|
2952
|
+
};
|
|
2953
|
+
const httpReadName = (node) => {
|
|
2954
|
+
const read = memberReadName(expectCallUnder(node)?.arguments[0]);
|
|
2955
|
+
if (read === null || HTTP_MEMBERS[read] !== true) return null;
|
|
2956
|
+
return read;
|
|
2957
|
+
};
|
|
2958
|
+
const memberCallName = (callee) => {
|
|
2959
|
+
if (callee.type !== "MemberExpression") return null;
|
|
2960
|
+
return callee.computed ? null : asIdentifierName(callee.property);
|
|
2961
|
+
};
|
|
2962
|
+
const emitCalleeName = (node) => {
|
|
2963
|
+
return asIdentifierName(node.callee) ?? memberCallName(node.callee);
|
|
2964
|
+
};
|
|
2965
|
+
const rawEmitName = (node) => {
|
|
2966
|
+
const name = emitCalleeName(node);
|
|
2967
|
+
if (name === null || !isEmitCallee(name)) return null;
|
|
2968
|
+
return name;
|
|
2969
|
+
};
|
|
2970
|
+
const hasHarnessBinding = (node) => node.source.value === "@systemfsoftware/trace-spec" && node.specifiers.length > 0;
|
|
2971
|
+
const literalValueOf = (node) => {
|
|
2972
|
+
if (node === void 0 || node.type !== "Literal") return null;
|
|
2973
|
+
return node.value;
|
|
2974
|
+
};
|
|
2975
|
+
const stringLiteralValue = (node) => {
|
|
2976
|
+
const value = literalValueOf(node);
|
|
2977
|
+
return typeof value === "string" ? value : null;
|
|
2978
|
+
};
|
|
2979
|
+
const stringElementValue = (element) => stringLiteralValue(element ?? void 0);
|
|
2980
|
+
const httpMemberName = (name) => HTTP_MEMBERS[name] === true ? name : null;
|
|
2981
|
+
const httpMemberHead = (path) => {
|
|
2982
|
+
const head = path.split(".")[0];
|
|
2983
|
+
return head === void 0 ? null : httpMemberName(head);
|
|
2984
|
+
};
|
|
2985
|
+
const propertyPathText = (node) => node?.type === "ArrayExpression" ? stringElementValue(node.elements[0]) : stringLiteralValue(node);
|
|
2986
|
+
const toHavePropertyMember = (node) => {
|
|
2987
|
+
const path = propertyPathText(node.arguments[0]);
|
|
2988
|
+
return path === null ? null : httpMemberHead(path);
|
|
2989
|
+
};
|
|
2990
|
+
const propertyMatchTermination = (node, matcher) => {
|
|
2991
|
+
const member = toHavePropertyMember(node);
|
|
2992
|
+
return matcher === "toHaveProperty" ? member : null;
|
|
2993
|
+
};
|
|
2994
|
+
const identifierKeyName = (key) => {
|
|
2995
|
+
if (key.type !== "Identifier") return null;
|
|
2996
|
+
return key.name;
|
|
2997
|
+
};
|
|
2998
|
+
const keyLiteralValue = (key) => {
|
|
2999
|
+
if (key.type !== "Literal") return null;
|
|
3000
|
+
return key.value;
|
|
3001
|
+
};
|
|
3002
|
+
const stringKeyName = (key) => {
|
|
3003
|
+
const value = keyLiteralValue(key);
|
|
3004
|
+
return typeof value === "string" ? value : null;
|
|
3005
|
+
};
|
|
3006
|
+
const objectKeyName = (key) => {
|
|
3007
|
+
const identifier = identifierKeyName(key);
|
|
3008
|
+
return identifier === null ? stringKeyName(key) : identifier;
|
|
3009
|
+
};
|
|
3010
|
+
const objectPropertyKey = (property) => {
|
|
3011
|
+
if (property.type !== "Property" || property.computed) return null;
|
|
3012
|
+
return property.key;
|
|
3013
|
+
};
|
|
3014
|
+
const propertyKeyName = (property) => {
|
|
3015
|
+
const key = objectPropertyKey(property);
|
|
3016
|
+
return key === null ? null : objectKeyName(key);
|
|
3017
|
+
};
|
|
3018
|
+
const httpMemberOr = (name) => name === null ? null : httpMemberName(name);
|
|
3019
|
+
const propertyValueOf = (property) => property.type === "Property" ? property.value : null;
|
|
3020
|
+
const nestedPropertyHttpKey = (property) => httpKeyOfValue(propertyValueOf(property));
|
|
3021
|
+
const httpKeyOfProperty = (property) => {
|
|
3022
|
+
const named = httpMemberOr(propertyKeyName(property));
|
|
3023
|
+
return named === null ? nestedPropertyHttpKey(property) : named;
|
|
3024
|
+
};
|
|
3025
|
+
const elementsHttpKey = (elements) => {
|
|
3026
|
+
const hit = elements.find((element) => httpKeyOfValue(element) !== null);
|
|
3027
|
+
return hit === void 0 ? null : httpKeyOfValue(hit);
|
|
3028
|
+
};
|
|
3029
|
+
const httpKeyOfNonObject = (value) => value?.type === "ArrayExpression" ? elementsHttpKey(value.elements) : null;
|
|
3030
|
+
const httpKeyOfValue = (value) => value?.type === "ObjectExpression" ? propertiesHttpKey(value.properties) : httpKeyOfNonObject(value);
|
|
3031
|
+
const propertiesHttpKey = (properties) => {
|
|
3032
|
+
const hit = properties.find((property) => httpKeyOfProperty(property) !== null);
|
|
3033
|
+
return hit === void 0 ? null : httpKeyOfProperty(hit);
|
|
3034
|
+
};
|
|
3035
|
+
const isObjectExpression = (node) => node.type === "ObjectExpression";
|
|
3036
|
+
const objectArgumentHttpKey = (node) => {
|
|
3037
|
+
const argument = node.arguments.find(isObjectExpression);
|
|
3038
|
+
return argument === void 0 ? null : propertiesHttpKey(argument.properties);
|
|
3039
|
+
};
|
|
3040
|
+
const isObjectShapeMatcher = (matcher) => HTTP_OBJECT_MATCHERS[matcher] === true;
|
|
3041
|
+
const objectMatchTermination = (node, matcher) => {
|
|
3042
|
+
const key = objectArgumentHttpKey(node);
|
|
3043
|
+
return isObjectShapeMatcher(matcher) ? key : null;
|
|
3044
|
+
};
|
|
3045
|
+
const isExpectIdentifier = (node) => node.type === "Identifier" && node.name === "expect";
|
|
3046
|
+
const isExpectReference = (node) => isExpectCall(node) || isExpectIdentifier(node);
|
|
3047
|
+
const chainRootsAtExpect = (node) => node.type === "MemberExpression" ? chainRootsAtExpect(node.object) : isExpectReference(node);
|
|
3048
|
+
const calleeRootsAtExpect = (callee) => callee.type === "MemberExpression" ? chainRootsAtExpect(callee.object) : false;
|
|
3049
|
+
const matcherCallName = (node) => {
|
|
3050
|
+
if (!calleeRootsAtExpect(node.callee)) return null;
|
|
3051
|
+
return memberCallName(node.callee);
|
|
3052
|
+
};
|
|
3053
|
+
const propertyMatchName = (member) => `expect(...).${HTTP_PROPERTY_MATCHER}('${member}')`;
|
|
3054
|
+
const objectMatchName = (matcher, key) => `expect(...).${matcher}({ ${key}: ... })`;
|
|
3055
|
+
const objectMatchedName = (node, matcher) => {
|
|
3056
|
+
const key = objectMatchTermination(node, matcher);
|
|
3057
|
+
return key === null ? null : objectMatchName(matcher, key);
|
|
3058
|
+
};
|
|
3059
|
+
const matchedShapeName = (node, matcher) => {
|
|
3060
|
+
const member = propertyMatchTermination(node, matcher);
|
|
3061
|
+
return member === null ? objectMatchedName(node, matcher) : propertyMatchName(member);
|
|
3062
|
+
};
|
|
3063
|
+
const shapeTerminationName = (node) => {
|
|
3064
|
+
const matcher = matcherCallName(node);
|
|
3065
|
+
return matcher === null ? null : matchedShapeName(node, matcher);
|
|
3066
|
+
};
|
|
3067
|
+
const httpTerminationName = (node) => {
|
|
3068
|
+
const direct = httpReadName(node);
|
|
3069
|
+
return direct === null ? shapeTerminationName(node) : `expect(...${direct})`;
|
|
3070
|
+
};
|
|
3071
|
+
const reportRawEmit = (context, node) => {
|
|
3072
|
+
const name = rawEmitName(node);
|
|
3073
|
+
if (name === null) return;
|
|
3074
|
+
context.report({
|
|
3075
|
+
node,
|
|
3076
|
+
messageId: "rawEmitCall",
|
|
3077
|
+
data: {
|
|
3078
|
+
name: `${name}(...) inside a trace spec`,
|
|
3079
|
+
expected: RAW_EMIT_EXPECTED,
|
|
3080
|
+
actual: RAW_EMIT_ACTUAL,
|
|
3081
|
+
fix: RAW_EMIT_FIX
|
|
3082
|
+
}
|
|
3083
|
+
});
|
|
3084
|
+
};
|
|
3085
|
+
const reportHttpTermination = (context, node) => {
|
|
3086
|
+
const name = httpTerminationName(node);
|
|
3087
|
+
if (name === null) return;
|
|
3088
|
+
context.report({
|
|
3089
|
+
node,
|
|
3090
|
+
messageId: "httpTermination",
|
|
3091
|
+
data: {
|
|
3092
|
+
name: `${name} inside a trace spec`,
|
|
3093
|
+
expected: HTTP_TERMINATION_EXPECTED,
|
|
3094
|
+
actual: HTTP_TERMINATION_ACTUAL,
|
|
3095
|
+
fix: HTTP_TERMINATION_FIX
|
|
3096
|
+
}
|
|
3097
|
+
});
|
|
3098
|
+
};
|
|
3099
|
+
const traceTestRequiresTaxonomy = defineRule({
|
|
3100
|
+
meta,
|
|
3101
|
+
create(context) {
|
|
3102
|
+
if (!context.filename.endsWith(".trace.test.ts")) return {};
|
|
3103
|
+
let hasHarnessImport = false;
|
|
3104
|
+
return {
|
|
3105
|
+
ImportDeclaration(node) {
|
|
3106
|
+
if (hasHarnessBinding(node)) hasHarnessImport = true;
|
|
3107
|
+
},
|
|
3108
|
+
CallExpression(node) {
|
|
3109
|
+
reportRawEmit(context, node);
|
|
3110
|
+
reportHttpTermination(context, node);
|
|
3111
|
+
},
|
|
3112
|
+
"Program:exit"(node) {
|
|
3113
|
+
if (hasHarnessImport) return;
|
|
3114
|
+
context.report({
|
|
3115
|
+
node,
|
|
3116
|
+
messageId: "missingHarnessImport",
|
|
3117
|
+
data: {
|
|
3118
|
+
name: `a *.trace.test.ts without ${TRACE_SPEC_PACKAGE}`,
|
|
3119
|
+
expected: HARNESS_PRESCRIPTION,
|
|
3120
|
+
actual: MISSING_HARNESS_ACTUAL,
|
|
3121
|
+
fix: HARNESS_PRESCRIPTION
|
|
3122
|
+
}
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
});
|
|
3128
|
+
//#endregion
|
|
2835
3129
|
//#region src/index.ts
|
|
2836
3130
|
const PLUGIN_NAME = "@systemfsoftware/oxlint-plugin-test-discipline";
|
|
2837
3131
|
const rule = (name) => `${PLUGIN_NAME}/${name}`;
|
|
@@ -2858,7 +3152,10 @@ const recommendedRules = {
|
|
|
2858
3152
|
[rule("tests-dir-helpers-in-fixtures")]: "error",
|
|
2859
3153
|
[rule("no-io-module-in-source-test")]: "error",
|
|
2860
3154
|
[rule("tests-import-public-api")]: "error",
|
|
2861
|
-
[rule("differential-test-requires-harness")]: "error"
|
|
3155
|
+
[rule("differential-test-requires-harness")]: "error",
|
|
3156
|
+
[rule("no-pseudo-gherkin-unit-tests")]: "error",
|
|
3157
|
+
[rule("ban-raw-span-name-emit")]: "error",
|
|
3158
|
+
[rule("trace-test-requires-taxonomy")]: "error"
|
|
2862
3159
|
};
|
|
2863
3160
|
var src_default = {
|
|
2864
3161
|
meta: { name: PLUGIN_NAME },
|
|
@@ -2886,7 +3183,9 @@ var src_default = {
|
|
|
2886
3183
|
"tests-dir-helpers-in-fixtures": testsDirHelpersInFixtures,
|
|
2887
3184
|
"no-io-module-in-source-test": noIoModuleInSourceTest,
|
|
2888
3185
|
"tests-import-public-api": testsImportPublicApi,
|
|
2889
|
-
"differential-test-requires-harness": differentialTestRequiresHarness
|
|
3186
|
+
"differential-test-requires-harness": differentialTestRequiresHarness,
|
|
3187
|
+
"ban-raw-span-name-emit": banRawSpanNameEmit,
|
|
3188
|
+
"trace-test-requires-taxonomy": traceTestRequiresTaxonomy
|
|
2890
3189
|
},
|
|
2891
3190
|
configs: { recommended: { rules: recommendedRules } }
|
|
2892
3191
|
};
|
|
@@ -29,6 +29,8 @@ declare const _default: {
|
|
|
29
29
|
'no-io-module-in-source-test': Rule;
|
|
30
30
|
'tests-import-public-api': Rule;
|
|
31
31
|
'differential-test-requires-harness': Rule;
|
|
32
|
+
'ban-raw-span-name-emit': Rule;
|
|
33
|
+
'trace-test-requires-taxonomy': Rule;
|
|
32
34
|
};
|
|
33
35
|
configs: {
|
|
34
36
|
recommended: {
|
|
@@ -29,6 +29,8 @@ declare const _default: {
|
|
|
29
29
|
'no-io-module-in-source-test': Rule;
|
|
30
30
|
'tests-import-public-api': Rule;
|
|
31
31
|
'differential-test-requires-harness': Rule;
|
|
32
|
+
'ban-raw-span-name-emit': Rule;
|
|
33
|
+
'trace-test-requires-taxonomy': Rule;
|
|
32
34
|
};
|
|
33
35
|
configs: {
|
|
34
36
|
recommended: {
|
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.
|
|
4
|
+
"version": "3.8.0",
|
|
5
5
|
"author": "Ryan Lee <drdgvhbh@gmail.com>",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@microsoft/api-extractor": "^7.59.1",
|
|
46
46
|
"@systemfsoftware/arethetypeswrong-cli": "^4.2.0",
|
|
47
|
-
"@systemfsoftware/stryker-ignorer-effect-schema-declarations": "^0.1.
|
|
48
|
-
"@systemfsoftware/stryker-ignorer-in-source-vitest-block": "^0.1.
|
|
49
|
-
"@systemfsoftware/stryker-js": "^10.
|
|
50
|
-
"@systemfsoftware/stryker-js-typescript-checker": "^7.0.
|
|
51
|
-
"@systemfsoftware/stryker-js-vitest-runner": "^7.
|
|
47
|
+
"@systemfsoftware/stryker-ignorer-effect-schema-declarations": "^0.1.1",
|
|
48
|
+
"@systemfsoftware/stryker-ignorer-in-source-vitest-block": "^0.1.1",
|
|
49
|
+
"@systemfsoftware/stryker-js": "^10.1.1",
|
|
50
|
+
"@systemfsoftware/stryker-js-typescript-checker": "^7.0.5",
|
|
51
|
+
"@systemfsoftware/stryker-js-vitest-runner": "^7.1.1",
|
|
52
52
|
"@systemfsoftware/stryker-test-contribution": "^3.0.3",
|
|
53
53
|
"@types/node": "^26",
|
|
54
54
|
"@vitest/coverage-v8": "^5",
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
"tsdown": "^0.23.0",
|
|
60
60
|
"typescript": "^7",
|
|
61
61
|
"vitest": "^5",
|
|
62
|
-
"@systemfsoftware/
|
|
63
|
-
"@systemfsoftware/tsdown-config": "^0.1.0",
|
|
62
|
+
"@systemfsoftware/stryker-config": "^0.1.0",
|
|
64
63
|
"@systemfsoftware/vitest-config": "^0.1.0",
|
|
65
|
-
"@systemfsoftware/
|
|
64
|
+
"@systemfsoftware/tsconfig": "^1.3.6",
|
|
65
|
+
"@systemfsoftware/tsdown-config": "^0.1.0"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"effect": "4.0.0-rc.116",
|