@avi2dg/checks 0.32.0 → 0.33.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 +15 -0
- package/README.md +3 -3
- package/dist/readability/index.js +18 -9
- package/docs/configs/typescript-rules.md +1 -1
- package/docs/design.md +6 -2
- package/docs/gates/checks-docs.md +9 -3
- package/docs/gates/checks-mutation.md +2 -1
- package/docs/gates/checks-vendor.md +3 -3
- package/package.json +11 -5
- package/src/core/git.ts +1 -1
- package/src/core/lint.ts +1 -1
- package/src/delivery/github.ts +1 -1
- package/src/delivery/release-pr.ts +3 -2
- package/src/delivery/release.ts +1 -1
- package/src/dependencies/pinned-binary.ts +5 -4
- package/src/docs/doc-agents.ts +22 -20
- package/src/docs/doc-names.ts +1 -1
- package/src/docs/doc-references.ts +14 -7
- package/src/docs/docs.ts +15 -2
- package/src/docs/prose-matchers.ts +1 -1
- package/src/docs/sentence-length.ts +106 -0
- package/src/testing/flake.ts +1 -1
- package/src/testing/mutation.ts +1 -1
- package/src/testing/test.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.33.0
|
|
6
|
+
|
|
7
|
+
Released 2026-10-03.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **docs:** report sentences over trial length caps as advisory [#120](https://github.com/avi2d/checks/pull/120)
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
|
|
15
|
+
- move effect to stable 4.0.0 [#122](https://github.com/avi2d/checks/pull/122)
|
|
16
|
+
- **complexity:** accept signed numbers and earlier props names in thin-astro defaults [#115](https://github.com/avi2d/checks/pull/115)
|
|
17
|
+
- **docs:** judge agent file entries and headings from a CommonMark parse [#116](https://github.com/avi2d/checks/pull/116)
|
|
18
|
+
- **docs:** fail link anchors the checks-docs reference rule could not check [#114](https://github.com/avi2d/checks/pull/114)
|
|
19
|
+
|
|
5
20
|
## 0.32.0
|
|
6
21
|
|
|
7
22
|
Released 2026-09-29.
|
package/README.md
CHANGED
|
@@ -9,12 +9,12 @@ Each repository owns its workflows and native tool configs, as [Native settings]
|
|
|
9
9
|
<!-- generated prerequisites: bun run build writes it from package.json, .bun-version and scripts/doc-blocks.ts -->
|
|
10
10
|
|
|
11
11
|
- A git repository, whose history the range gates read.
|
|
12
|
-
- Bun 1.
|
|
12
|
+
- Bun 1.4.2, which runs every bin.
|
|
13
13
|
- The peer dependencies, at the exact versions the kit pins:
|
|
14
14
|
- `@effect/tsgo` 0.45.0
|
|
15
15
|
- `@swc/core` 1.16.2
|
|
16
16
|
- `dependency-cruiser` 18.4.0
|
|
17
|
-
- `effect` 4.0.0
|
|
17
|
+
- `effect` 4.0.0
|
|
18
18
|
- `jscpd` 5.3.2
|
|
19
19
|
- `oxlint` 1.83.0
|
|
20
20
|
- `oxlint-tsgolint` 7.0.2002
|
|
@@ -31,7 +31,7 @@ To consume the kit from a repository:
|
|
|
31
31
|
<!-- generated install: bun run build writes it from package.json and scripts/doc-blocks.ts -->
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
|
-
bun add -d @avi2dg/checks @effect/tsgo@0.45.0 @swc/core@1.16.2 dependency-cruiser@18.4.0 effect@4.0.0
|
|
34
|
+
bun add -d @avi2dg/checks @effect/tsgo@0.45.0 @swc/core@1.16.2 dependency-cruiser@18.4.0 effect@4.0.0 jscpd@5.3.2 oxlint@1.83.0 oxlint-tsgolint@7.0.2002 typescript@7.0.2
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
<!-- end generated install -->
|
|
@@ -400,11 +400,11 @@ var rule = {
|
|
|
400
400
|
create(context) {
|
|
401
401
|
const max = maxOf(context.options);
|
|
402
402
|
const check = (node) => {
|
|
403
|
-
const
|
|
404
|
-
if (
|
|
403
|
+
const score = node.type === "StaticBlock" ? cognitiveComplexity(node, NO_NAMES) : cognitiveComplexity(node, selfNames(node));
|
|
404
|
+
if (score <= max)
|
|
405
405
|
return;
|
|
406
406
|
const name = node.type === "StaticBlock" ? "static block" : `function \`${displayName(node)}\``;
|
|
407
|
-
context.report({ node, message: `${name} has a cognitive complexity of ${
|
|
407
|
+
context.report({ node, message: `${name} has a cognitive complexity of ${score}. Maximum allowed is ${max}.` });
|
|
408
408
|
};
|
|
409
409
|
return {
|
|
410
410
|
FunctionDeclaration: check,
|
|
@@ -430,6 +430,9 @@ function isAstroProps(value) {
|
|
|
430
430
|
const { object, property } = value;
|
|
431
431
|
return object.type === "Identifier" && object.name === "Astro" && property.type === "Identifier" && property.name === "props";
|
|
432
432
|
}
|
|
433
|
+
function isSignedNumber(value) {
|
|
434
|
+
return value.type === "UnaryExpression" && (value.operator === "-" || value.operator === "+") && value.argument.type === "Literal" && typeof value.argument.value === "number";
|
|
435
|
+
}
|
|
433
436
|
function isPlainValue(value, bound) {
|
|
434
437
|
return value.type === "Literal" || value.type === "Identifier" && bound.has(value.name);
|
|
435
438
|
}
|
|
@@ -442,17 +445,23 @@ function readsProps(expression, bound) {
|
|
|
442
445
|
return isAstroProps(value) || readsProps(value.object, bound);
|
|
443
446
|
}
|
|
444
447
|
function isPlainPattern(pattern, bound) {
|
|
448
|
+
return isPlainInOrder(pattern, bound, new Set(bound));
|
|
449
|
+
}
|
|
450
|
+
function isPlainInOrder(pattern, bound, seen) {
|
|
445
451
|
if (pattern === null)
|
|
446
452
|
return true;
|
|
447
453
|
if (pattern.type === "RestElement")
|
|
448
|
-
return
|
|
449
|
-
if (pattern.type === "AssignmentPattern")
|
|
450
|
-
return isPlainValue(pattern.right,
|
|
454
|
+
return isPlainInOrder(pattern.argument, bound, seen);
|
|
455
|
+
if (pattern.type === "AssignmentPattern") {
|
|
456
|
+
return (isPlainValue(pattern.right, seen) || isSignedNumber(pattern.right)) && isPlainInOrder(pattern.left, bound, seen);
|
|
457
|
+
}
|
|
451
458
|
if (pattern.type === "ArrayPattern")
|
|
452
|
-
return pattern.elements.every((element) =>
|
|
453
|
-
if (pattern.type === "Identifier")
|
|
459
|
+
return pattern.elements.every((element) => isPlainInOrder(element, bound, seen));
|
|
460
|
+
if (pattern.type === "Identifier") {
|
|
461
|
+
seen.add(pattern.name);
|
|
454
462
|
return true;
|
|
455
|
-
|
|
463
|
+
}
|
|
464
|
+
return pattern.properties.every((property) => property.type === "RestElement" ? isPlainInOrder(property.argument, bound, seen) : (!property.computed || isPlainValue(property.key, bound)) && isPlainInOrder(property.value, bound, seen));
|
|
456
465
|
}
|
|
457
466
|
function isPropsRead(declarator, bound) {
|
|
458
467
|
return declarator.init !== null && readsProps(declarator.init, bound) && isPlainPattern(declarator.id, bound);
|
|
@@ -48,7 +48,7 @@ The base loads the kit's `data-shape` plugin from `dist/` with one rule for ever
|
|
|
48
48
|
An override in `oxlintrc.json` turns on one rule of the kit's `readability` plugin in each `.astro` file:
|
|
49
49
|
|
|
50
50
|
- `readability/thin-astro` refuses a statement in the frontmatter or a script block that is neither an import, a re-export from another module, a type or interface declaration, nor a variable read from `Astro.props`.
|
|
51
|
-
- A default inside an `Astro.props` destructuring passes only when it is a literal or a name read from `Astro.props` earlier, so `const { title = "Home" } = Astro.props;` passes and a call or `await` in a default is refused.
|
|
51
|
+
- A default inside an `Astro.props` destructuring passes only when it is a literal, a `-` or `+` on a numeric literal, or a name read from `Astro.props` earlier, in the same pattern or a statement before it, so `const { title = "Home", heading = title } = Astro.props;` passes and a call or `await` in a default is refused.
|
|
52
52
|
- Move a refused statement into a `.ts` file and import it, so the `.astro` file holds only imports, props and markup.
|
|
53
53
|
- A script block loads client code with a side-effect import, as in `<script>import "../client.ts";</script>`, and the override turns off `import/no-unassigned-import` so that import passes.
|
|
54
54
|
- A dynamic route re-exports `getStaticPaths` from a `.ts` file, as in `export { getStaticPaths } from "../lib/paths.ts";`.
|
package/docs/design.md
CHANGED
|
@@ -67,7 +67,7 @@ So the fragment lists the rules in `files`, and in `include` beside every file u
|
|
|
67
67
|
|
|
68
68
|
The source sits under `src/<vector>/`, one directory for each thing the kit judges a repository on: complexity, quality, testing, docs, delivery and dependencies.
|
|
69
69
|
`src/core/` holds what every vector runs on.
|
|
70
|
-
`scripts/` holds only the kit's own build, and nothing in it ships.
|
|
70
|
+
`scripts/` holds only the kit's own build and CI tooling, and nothing in it ships.
|
|
71
71
|
Sorting files by what loads them would put both oxlint plugins at the root and every bin in one flat directory.
|
|
72
72
|
Nothing would then say which gate a helper serves.
|
|
73
73
|
A mutation runner's default scope covers `src/`, so the kit's own Stryker run mutates its source with no `mutate` list.
|
|
@@ -100,7 +100,7 @@ The `.ts` bins keep a `bun` shebang and need no build step, unlike the oxlint pl
|
|
|
100
100
|
The bins are written in Effect.
|
|
101
101
|
So `effect` is a peer dependency, and `@effect/platform-bun`, which only the bins use, is a dependency.
|
|
102
102
|
`@effect/platform-node-shared` is a direct dependency only to pin its version.
|
|
103
|
-
`@effect/platform-bun` asks for it with a `^` range,
|
|
103
|
+
`@effect/platform-bun` asks for it with a `^` range, so a newer minor of it could resolve and peer on a newer `effect` than the exact version consumers install.
|
|
104
104
|
So the three packages move together at one exact version.
|
|
105
105
|
|
|
106
106
|
## checks-lint runs each gate as its own bin
|
|
@@ -211,6 +211,10 @@ A score cannot fail a change without failing correct prose, and a suggestion tha
|
|
|
211
211
|
`src/docs/prose-matchers.ts` imports nothing, so the gate and a write-time hook run one matcher and refuse in the same words.
|
|
212
212
|
A hook bundle ships without `node_modules`, so a matcher that needed Vale or another package could not refuse at write time.
|
|
213
213
|
|
|
214
|
+
The entry rule and the rule against a `## Maintaining this file` section take their list items and headings from `commonmark`, the CommonMark reference parser.
|
|
215
|
+
A reader sees the blocks a renderer builds, and a line rule that guesses at blockquotes, HTML blocks and indented code misjudges each corner its guess misses.
|
|
216
|
+
Only the gate runs these rules, so the parser costs no hook anything, and the line scan reads each entry's code spans and links from that entry's line alone.
|
|
217
|
+
|
|
214
218
|
A path, link or command on a line the range leaves alone still fails when the range broke it, for example by deleting the file it names.
|
|
215
219
|
A reference goes stale far more often because the code it names moves than because its own line is edited.
|
|
216
220
|
So a gate on edited lines alone would miss the usual break.
|
|
@@ -110,6 +110,8 @@ A line holds one sentence, so a changed line is a changed sentence.
|
|
|
110
110
|
A bold label that opens a line, as in `**Status.**`, heads the sentence after it and is not a sentence of its own.
|
|
111
111
|
No rule reads fenced code, inline code, link destinations, URLs, HTML comments or front matter.
|
|
112
112
|
Readability scores and word choice, such as easy, are not checked.
|
|
113
|
+
A sentence over the trial length cap is listed as advisory and never fails the run.
|
|
114
|
+
The cap is 20 words in an ordered list item and 25 words elsewhere.
|
|
113
115
|
|
|
114
116
|
`src/docs/prose-matchers.ts` holds the rules and a synchronous `proseRefused()`, and imports nothing.
|
|
115
117
|
The package exports it as `@avi2dg/checks/scripts/prose-matchers.ts`.
|
|
@@ -121,7 +123,8 @@ Each reference a living doc or an agent file names has to resolve at the head co
|
|
|
121
123
|
|
|
122
124
|
- A path in inline code that ends in a file extension, such as `src/core/lint.ts`, names a file from the root or from the doc's directory.
|
|
123
125
|
- A relative Markdown link names a file or a directory.
|
|
124
|
-
Its anchor names a heading in that file, as GitHub derives the anchor, or an explicit `id`.
|
|
126
|
+
Its anchor names a heading in that Markdown or MDX file, as GitHub derives the anchor, or an explicit `id`.
|
|
127
|
+
An anchor into a directory or any other file fails, because it names no heading the gate can check.
|
|
125
128
|
- A `bun run` command in code names a script in the nearest `package.json`, a bin in `node_modules/.bin`, or a file that exists.
|
|
126
129
|
- A code span fails when some tracked file outside the docs held that exact text at the base, and none holds it at the head.
|
|
127
130
|
A span the path check already fails on is not reported again.
|
|
@@ -150,7 +153,8 @@ audience: consumers
|
|
|
150
153
|
An agent file holds the router its template sketches, and the rules below hold its shape whatever the range touches.
|
|
151
154
|
A file over 3,000 characters fails.
|
|
152
155
|
Move each part's notes into the people doc that covers that part, and delete what a check or the code already holds.
|
|
153
|
-
A file that holds a `## Maintaining this file` section fails, because this gate holds the shape the section asked for.
|
|
156
|
+
A file that holds a `## Maintaining this file` section a reader sees fails, because this gate holds the shape the section asked for.
|
|
157
|
+
A heading inside an HTML comment, an HTML block or a code block is not such a section.
|
|
154
158
|
Each entry names at least one of these, or it fails:
|
|
155
159
|
|
|
156
160
|
- A path in inline code that git tracks at the head commit, a file or a directory, such as `package.json`, `LICENSE` or `.gitignore`.
|
|
@@ -159,7 +163,7 @@ Each entry names at least one of these, or it fails:
|
|
|
159
163
|
- A `bun run` command.
|
|
160
164
|
|
|
161
165
|
Whether the link or the command resolves is the reference rule's call, as [Paths, links and commands](#paths-links-and-commands) says, and it fails when the range adds or breaks one.
|
|
162
|
-
An entry is any list item a reader sees, the items above the first section included.
|
|
166
|
+
An entry is any list item a reader sees as CommonMark renders the file, the items above the first section included.
|
|
163
167
|
A list item inside an HTML comment, an HTML block or an indented code block is not an entry.
|
|
164
168
|
A fresh file passes the ceiling, and its entries pass once each names the file that holds its detail.
|
|
165
169
|
|
|
@@ -205,6 +209,8 @@ docs: 6 violation(s):
|
|
|
205
209
|
docs/parts.md:9: names `gates.lint`, which the range removed from every file outside the docs. Say what holds now, or drop the line
|
|
206
210
|
docs: advisory, 1 doc file(s) the range leaves alone do not hold to their templates yet:
|
|
207
211
|
docs/adr/0001-quality-gates.md: 5 violation(s)
|
|
212
|
+
docs: advisory, 1 sentence(s) over the trial length caps:
|
|
213
|
+
README.md:16: carries a 27-word descriptive sentence, over the 25-word cap (procedural means an ordered list item, capped at 20 words)
|
|
208
214
|
docs: advisory, 1 path(s), link(s) or command(s) the living docs or agent files name were broken before the range:
|
|
209
215
|
docs/parts.md:9: links to `suppliers.md#prices`, and `docs/suppliers.md` has no heading with that anchor
|
|
210
216
|
```
|
|
@@ -59,8 +59,9 @@ An `--incremental` run with no report stops before instrumenting with the missin
|
|
|
59
59
|
## When it runs
|
|
60
60
|
|
|
61
61
|
A repository runs full baselines from the mutation workflow on `workflow_dispatch`.
|
|
62
|
+
A repository that picks a pull request scope from a baseline also runs that workflow on a nightly schedule.
|
|
62
63
|
Run scoped checks locally during development.
|
|
63
|
-
A scheduled run
|
|
64
|
+
A scheduled run costs a full Stryker run.
|
|
64
65
|
|
|
65
66
|
## Running it in CI
|
|
66
67
|
|
|
@@ -60,19 +60,19 @@ With no arguments it pins nothing.
|
|
|
60
60
|
## Sample output
|
|
61
61
|
|
|
62
62
|
```
|
|
63
|
-
checks-vendor: cloned effect@4.0.0
|
|
63
|
+
checks-vendor: cloned effect@4.0.0 from https://github.com/Effect-TS/effect.git and linked repos/effect
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
A fresh fetch reports the tag it cloned and the link it made.
|
|
67
67
|
|
|
68
68
|
```
|
|
69
|
-
checks-vendor: repos/effect still holds effect@4.0.0
|
|
69
|
+
checks-vendor: repos/effect still holds effect@4.0.0, verified against its recorded commit
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
A later run reports the link it kept.
|
|
73
73
|
|
|
74
74
|
```
|
|
75
|
-
checks-vendor: found 1 path writable by its owner, starting with /home/runner/.cache/avi2dg-checks/repos/github.com/Effect-TS/effect/effect@4.0.0
|
|
75
|
+
checks-vendor: found 1 path writable by its owner, starting with /home/runner/.cache/avi2dg-checks/repos/github.com/Effect-TS/effect/effect@4.0.0, and froze the tree again, so repos/effect still holds effect@4.0.0, verified against its recorded commit
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
A run that found an owner write bit reports how many paths carried one and the first of them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"src/complexity/exports.ts",
|
|
61
61
|
"src/complexity/knip.ts",
|
|
62
62
|
"src/testing/quarantine-clock.ts",
|
|
63
|
+
"src/docs/sentence-length.ts",
|
|
63
64
|
"src/docs/docs.ts",
|
|
64
65
|
"src/dependencies/vendor.ts",
|
|
65
66
|
"src/dependencies/vendor-args.ts",
|
|
@@ -136,7 +137,7 @@
|
|
|
136
137
|
"@swc/core": "1.16.2",
|
|
137
138
|
"dependency-cruiser": "18.4.0",
|
|
138
139
|
"@effect/tsgo": "0.45.0",
|
|
139
|
-
"effect": "4.0.0
|
|
140
|
+
"effect": "4.0.0",
|
|
140
141
|
"jscpd": "5.3.2",
|
|
141
142
|
"oxlint": "1.83.0",
|
|
142
143
|
"oxlint-tsgolint": "7.0.2002",
|
|
@@ -149,8 +150,9 @@
|
|
|
149
150
|
"@stryker-mutator/core": "10.0.0",
|
|
150
151
|
"@swc/core": "1.16.2",
|
|
151
152
|
"@types/bun": "^1.3.0",
|
|
153
|
+
"@types/commonmark": "0.27.10",
|
|
152
154
|
"dependency-cruiser": "18.4.0",
|
|
153
|
-
"effect": "4.0.0
|
|
155
|
+
"effect": "4.0.0",
|
|
154
156
|
"jscpd": "5.3.2",
|
|
155
157
|
"oxlint": "1.83.0",
|
|
156
158
|
"oxlint-tsgolint": "7.0.2002",
|
|
@@ -159,13 +161,17 @@
|
|
|
159
161
|
"dependencies": {
|
|
160
162
|
"@commitlint/cli": "21.2.3",
|
|
161
163
|
"@commitlint/config-conventional": "21.2.3",
|
|
162
|
-
"@effect/platform-bun": "4.0.0
|
|
163
|
-
"@effect/platform-node-shared": "4.0.0
|
|
164
|
+
"@effect/platform-bun": "4.0.0",
|
|
165
|
+
"@effect/platform-node-shared": "4.0.0",
|
|
164
166
|
"@total-typescript/ts-reset": "0.6.1",
|
|
167
|
+
"commonmark": "0.31.2",
|
|
165
168
|
"knip": "6.38.0"
|
|
166
169
|
},
|
|
167
170
|
"overrides": {
|
|
168
171
|
"qs": "6.16.0",
|
|
169
172
|
"smol-toml": "1.9.0"
|
|
173
|
+
},
|
|
174
|
+
"patchedDependencies": {
|
|
175
|
+
"@hughescr/stryker-bun-runner@1.4.0": "patches/@hughescr%2Fstryker-bun-runner@1.4.0.patch"
|
|
170
176
|
}
|
|
171
177
|
}
|
package/src/core/git.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Config, Effect, FileSystem, Path, Schema, Stream } from "effect";
|
|
2
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
2
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
3
3
|
import { DEFAULT_BRANCH } from "./gates.ts";
|
|
4
4
|
import { Usage } from "./main.ts";
|
|
5
5
|
|
package/src/core/lint.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Config, Console, Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
3
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
3
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
4
4
|
import { EVERY_REPOSITORY, KIT_GATES, type KitGate } from "./gates.ts";
|
|
5
5
|
import { defaultBranch, git } from "./git.ts";
|
|
6
6
|
import { runMain, Usage } from "./main.ts";
|
package/src/delivery/github.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import { ChildProcessSpawner } from "effect/
|
|
2
|
+
import { ChildProcessSpawner } from "effect/process";
|
|
3
3
|
import { collect } from "../core/git.ts";
|
|
4
4
|
|
|
5
5
|
export class GitHubFailure extends Schema.TaggedError<GitHubFailure>()("GitHubFailure", {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { Console, Effect,
|
|
2
|
+
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
+
import * as Base64 from "effect/encoding/Base64";
|
|
3
4
|
import { dispatch, gitHubJson, REPOSITORY } from "./github.ts";
|
|
4
5
|
import { nextVersion, type Pending, readPending, releaseTitle, runBuild, withVersion } from "./release.ts";
|
|
5
6
|
import { git } from "../core/git.ts";
|
|
@@ -105,7 +106,7 @@ const readStaged = Effect.fn("readStaged")(function* (root: string, staged: Stag
|
|
|
105
106
|
if (staged.kind === "deleted") return staged;
|
|
106
107
|
if (!REGULAR_FILE_MODES.has(staged.mode)) return yield* refused(`the build wrote ${staged.path} with mode ${staged.mode}, which is no regular file`);
|
|
107
108
|
const bytes = yield* (yield* FileSystem.FileSystem).readFile((yield* Path.Path).join(root, staged.path));
|
|
108
|
-
return { ...staged, content:
|
|
109
|
+
return { ...staged, content: Base64.encode(bytes) };
|
|
109
110
|
});
|
|
110
111
|
|
|
111
112
|
// The build writes into the working tree, so the tree it starts from has to hold nothing a release would sweep in.
|
package/src/delivery/release.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
2
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
3
3
|
import { groupOf } from "./changelog.ts";
|
|
4
4
|
import { git } from "../core/git.ts";
|
|
5
5
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Crypto, Effect,
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { Crypto, Effect, FileSystem, Path, Schema } from "effect";
|
|
2
|
+
import * as Hex from "effect/encoding/Hex";
|
|
3
|
+
import { HttpClient, HttpClientResponse } from "effect/http";
|
|
4
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
4
5
|
import { collect } from "../core/git.ts";
|
|
5
6
|
|
|
6
7
|
const EXECUTABLE_MODE = 0o755;
|
|
@@ -19,7 +20,7 @@ function installedSha256(asset: Asset): string {
|
|
|
19
20
|
|
|
20
21
|
const sha256Of = Effect.fn("sha256Of")(function* (bytes: Uint8Array) {
|
|
21
22
|
const crypto = yield* Crypto.Crypto;
|
|
22
|
-
return
|
|
23
|
+
return Hex.encode(yield* crypto.digest("SHA-256", bytes));
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
const verified = Effect.fn("verified")(function* (binary: string, sha256: string) {
|
package/src/docs/doc-agents.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
+
import { Parser, type NodeType } from "commonmark";
|
|
1
2
|
import { commandNames, type Snapshot } from "./doc-references.ts";
|
|
2
3
|
import { AGENT_NAMES, scanMarkdown, type MarkdownLine } from "./prose-matchers.ts";
|
|
3
4
|
|
|
4
5
|
export const AGENT_CEILING = 3000;
|
|
5
6
|
|
|
6
|
-
const LIST_ITEM = /^(?:\s*>)*\s*(?:[-*+]|\d{1,9}[.)])(?:\s|$)/;
|
|
7
|
-
const INDENT = /^[ \t]*/;
|
|
8
|
-
const CODE_INDENT = 4;
|
|
9
7
|
const INLINE_LINK =
|
|
10
8
|
/(?<![!\\])\[(?:[^[\]\\]|\\.|\[[^\]]*\])*\]\(\s*(?:<[^<>\n]+>|[^\s()<>]+(?:\([^\s()]*\)[^\s()<>]*)*)(?:\s+(?:"[^"]*"|'[^']*'))?\s*\)/g;
|
|
11
9
|
const MAINTAINING = /^\s{0,3}#{1,6}\s+Maintaining this file(?:\s+#+)?\s*$/;
|
|
@@ -27,8 +25,24 @@ export function ceilingFinding(text: string): AgentFinding | undefined {
|
|
|
27
25
|
};
|
|
28
26
|
}
|
|
29
27
|
|
|
28
|
+
function blockStarts(lines: readonly MarkdownLine[], type: NodeType): ReadonlySet<number> {
|
|
29
|
+
const read = lines.map(({ kind, raw }) => (kind === "front-matter" ? "" : raw)).join("\n");
|
|
30
|
+
const walker = new Parser().parse(read).walker();
|
|
31
|
+
const starts = new Set<number>();
|
|
32
|
+
for (let step = walker.next(); step !== null; step = walker.next()) {
|
|
33
|
+
if (step.entering && step.node.type === type) starts.add(step.node.sourcepos[0][0]);
|
|
34
|
+
}
|
|
35
|
+
return starts;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function linesOpening(text: string, type: NodeType): readonly MarkdownLine[] {
|
|
39
|
+
const lines = scanMarkdown(text);
|
|
40
|
+
const starts = blockStarts(lines, type);
|
|
41
|
+
return lines.filter(({ line }) => starts.has(line)).flatMap(({ line, raw }) => scanMarkdown(raw).map((alone) => ({ ...alone, line })));
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
export function maintainingFinding(text: string): AgentFinding | undefined {
|
|
31
|
-
const heading =
|
|
45
|
+
const heading = linesOpening(text, "heading").find(({ raw }) => MAINTAINING.test(raw));
|
|
32
46
|
if (heading === undefined) return undefined;
|
|
33
47
|
return {
|
|
34
48
|
line: heading.line,
|
|
@@ -36,27 +50,14 @@ export function maintainingFinding(text: string): AgentFinding | undefined {
|
|
|
36
50
|
};
|
|
37
51
|
}
|
|
38
52
|
|
|
39
|
-
function indentOf(raw: string): number {
|
|
40
|
-
return (INDENT.exec(raw)?.[0] ?? "").replaceAll("\t", " ").length;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
53
|
export function entries(text: string): readonly MarkdownLine[] {
|
|
44
|
-
|
|
45
|
-
let afterBlank = true;
|
|
46
|
-
return scanMarkdown(text).filter((line) => {
|
|
47
|
-
const blank = line.raw.trim() === "";
|
|
48
|
-
const indented = !blank && indentOf(line.raw) >= CODE_INDENT && !line.raw.trimStart().startsWith(">");
|
|
49
|
-
const listItem = line.kind === "prose" && LIST_ITEM.test(line.prose) && (inList || !indented);
|
|
50
|
-
if (listItem) inList = true;
|
|
51
|
-
else if (!blank && !indented && (afterBlank || line.kind !== "prose")) inList = false;
|
|
52
|
-
afterBlank = blank;
|
|
53
|
-
return listItem;
|
|
54
|
-
});
|
|
54
|
+
return linesOpening(text, "item");
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
type Tracked = Pick<Snapshot, "files" | "directories">;
|
|
58
58
|
|
|
59
59
|
const STAYS = new Set(["", "."]);
|
|
60
|
+
const NAMES_A_SEGMENT = /[^/]/;
|
|
60
61
|
|
|
61
62
|
function joined(directory: string, segment: string): string {
|
|
62
63
|
return directory === "" ? segment : `${directory}/${segment}`;
|
|
@@ -76,10 +77,11 @@ function resolvesFrom(directory: string, span: string, tracked: Tracked): boolea
|
|
|
76
77
|
if (walked === undefined) return false;
|
|
77
78
|
if (!STAYS.has(last) && last !== ".." && tracked.files.has(joined(walked, last))) return true;
|
|
78
79
|
const end = enter(walked, last, tracked);
|
|
79
|
-
return end !== undefined && end !== ""
|
|
80
|
+
return end !== undefined && end !== "";
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
function namesTrackedPath(agentFile: string, span: string, tracked: Tracked): boolean {
|
|
84
|
+
if (!NAMES_A_SEGMENT.test(span)) return false;
|
|
83
85
|
const directory = agentFile.includes("/") ? agentFile.slice(0, agentFile.lastIndexOf("/")) : "";
|
|
84
86
|
const fromFile = resolvesFrom(directory, span, tracked);
|
|
85
87
|
return fromFile || (!span.startsWith("./") && !span.startsWith("../") && resolvesFrom("", span, tracked));
|
package/src/docs/doc-names.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
2
|
-
import { ChildProcessSpawner } from "effect/
|
|
2
|
+
import { ChildProcessSpawner } from "effect/process";
|
|
3
3
|
import { collect, git, pathsAt } from "../core/git.ts";
|
|
4
4
|
import type { Unresolved } from "./doc-references.ts";
|
|
5
5
|
import { scanMarkdown } from "./prose-matchers.ts";
|
|
@@ -78,20 +78,28 @@ function unresolvedPath(doc: string, line: number, span: string, snapshot: Snaps
|
|
|
78
78
|
const SCHEME = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
79
79
|
const ASCII_ESCAPE = /%([0-7][0-9a-f])/gi;
|
|
80
80
|
|
|
81
|
+
function linkPath(doc: string, target: string, hash: number): string | undefined {
|
|
82
|
+
const written = (hash < 0 ? target : target.slice(0, hash)).split("?", 1)[0] ?? "";
|
|
83
|
+
const decoded = written.replace(ASCII_ESCAPE, (_, code: string) => String.fromCharCode(Number.parseInt(code, 16)));
|
|
84
|
+
return decoded === "" ? doc : decoded.startsWith("/") ? normalize(decoded) : within(directoryOf(doc), decoded);
|
|
85
|
+
}
|
|
86
|
+
|
|
81
87
|
function unresolvedLink(doc: string, line: number, target: string, snapshot: Snapshot): Unresolved | undefined {
|
|
82
88
|
if (target === "" || SCHEME.test(target)) return undefined;
|
|
83
89
|
const hash = target.indexOf("#");
|
|
84
|
-
const
|
|
85
|
-
const decoded = written.replace(ASCII_ESCAPE, (_, code: string) => String.fromCharCode(Number.parseInt(code, 16)));
|
|
86
|
-
const path = decoded === "" ? doc : decoded.startsWith("/") ? normalize(decoded) : within(directoryOf(doc), decoded);
|
|
90
|
+
const path = linkPath(doc, target, hash);
|
|
87
91
|
if (path === undefined) return undefined;
|
|
88
92
|
const named = target;
|
|
89
93
|
if (!exists(snapshot, path)) {
|
|
90
94
|
return { kind: "link", line, named, message: `links to \`${named}\`, which is not in the repository`, missing: { type: "file", path } };
|
|
91
95
|
}
|
|
92
96
|
const anchor = hash < 0 ? "" : target.slice(hash + 1);
|
|
97
|
+
if (anchor === "") return undefined;
|
|
93
98
|
const anchors = snapshot.anchors.get(path);
|
|
94
|
-
if (
|
|
99
|
+
if (anchors === undefined) {
|
|
100
|
+
return { kind: "link", line, named, message: `links to \`${named}\`, and \`${path}\` has no headings to check`, missing: { type: "anchor" } };
|
|
101
|
+
}
|
|
102
|
+
if (anchors.has(anchor) || anchors.has(anchor.toLowerCase())) return undefined;
|
|
95
103
|
return { kind: "link", line, named, message: `links to \`${named}\`, and \`${path}\` has no heading with that anchor`, missing: { type: "anchor" } };
|
|
96
104
|
}
|
|
97
105
|
|
|
@@ -152,9 +160,8 @@ export function anchoredTargets(doc: string, text: string): readonly string[] {
|
|
|
152
160
|
links.flatMap((target) => {
|
|
153
161
|
const hash = target.indexOf("#");
|
|
154
162
|
if (hash < 0 || SCHEME.test(target)) return [];
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
return path?.endsWith(".md") === true ? [path] : [];
|
|
163
|
+
const path = linkPath(doc, target, hash);
|
|
164
|
+
return path !== undefined && (path.endsWith(".md") || path.endsWith(".mdx")) ? [path] : [];
|
|
158
165
|
}),
|
|
159
166
|
);
|
|
160
167
|
}
|
package/src/docs/docs.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { readTexts, snapshotAt, stillMissing } from "./doc-snapshot.ts";
|
|
|
8
8
|
import { changedLines, changedPaths, git, pathsAt, rangeEnds, refArgs } from "../core/git.ts";
|
|
9
9
|
import { runMain } from "../core/main.ts";
|
|
10
10
|
import { proseFindings, readerOf } from "./prose-matchers.ts";
|
|
11
|
+
import { longSentences } from "./sentence-length.ts";
|
|
11
12
|
|
|
12
13
|
type Finding = {
|
|
13
14
|
readonly path: string;
|
|
@@ -22,6 +23,7 @@ type Judged = {
|
|
|
22
23
|
readonly agents: number;
|
|
23
24
|
readonly findings: readonly Finding[];
|
|
24
25
|
readonly advisory: ReadonlyMap<string, number>;
|
|
26
|
+
readonly sentenceAdvisory: readonly Finding[];
|
|
25
27
|
readonly brokenBefore: readonly Finding[];
|
|
26
28
|
};
|
|
27
29
|
|
|
@@ -105,6 +107,9 @@ const runDocs = Effect.fn("runDocs")(function* (root: string, base: string, head
|
|
|
105
107
|
const prose = edited.flatMap(({ path, reader }) =>
|
|
106
108
|
proseFindings(text(path), reader, changed.get(path)).map(({ line, message }) => ({ path, line, message })),
|
|
107
109
|
);
|
|
110
|
+
const sentenceAdvisory = edited
|
|
111
|
+
.flatMap(({ path }) => longSentences(text(path), changed.get(path)).map(({ line, message }) => ({ path, line, message })))
|
|
112
|
+
.toSorted(inPathOrder);
|
|
108
113
|
const judging = (path: string): Judging => ({ commands: !speaksToConsumers(text(path)) });
|
|
109
114
|
// A directory the range deletes still belongs to this repository, so a path under it is stale rather than another repository's.
|
|
110
115
|
const roots = rootsOf(yield* pathsAt(base, [], root));
|
|
@@ -133,6 +138,7 @@ const runDocs = Effect.fn("runDocs")(function* (root: string, base: string, head
|
|
|
133
138
|
...entries,
|
|
134
139
|
].toSorted(inPathOrder),
|
|
135
140
|
advisory,
|
|
141
|
+
sentenceAdvisory,
|
|
136
142
|
brokenBefore: references.brokenBefore.toSorted(inPathOrder),
|
|
137
143
|
} satisfies Judged;
|
|
138
144
|
});
|
|
@@ -141,7 +147,7 @@ function describe({ path, line, message }: Finding): string {
|
|
|
141
147
|
return ` ${path}${line === undefined ? "" : `:${line}`}: ${message}`;
|
|
142
148
|
}
|
|
143
149
|
|
|
144
|
-
export function report({ held, edited, named, agents, findings, advisory, brokenBefore }: Judged): string {
|
|
150
|
+
export function report({ held, edited, named, agents, findings, advisory, sentenceAdvisory, brokenBefore }: Judged): string {
|
|
145
151
|
const verdict =
|
|
146
152
|
findings.length === 0
|
|
147
153
|
? [
|
|
@@ -158,6 +164,13 @@ export function report({ held, edited, named, agents, findings, advisory, broken
|
|
|
158
164
|
`${NAME}: advisory, ${advisory.size} doc file(s) the range leaves alone do not hold to their templates yet:`,
|
|
159
165
|
...[...advisory].map(([path, count]) => ` ${path}: ${count} violation(s)`),
|
|
160
166
|
];
|
|
167
|
+
const sentences =
|
|
168
|
+
sentenceAdvisory.length === 0
|
|
169
|
+
? []
|
|
170
|
+
: [
|
|
171
|
+
`${NAME}: advisory, ${sentenceAdvisory.length} sentence(s) over the trial length caps:`,
|
|
172
|
+
...sentenceAdvisory.map(describe),
|
|
173
|
+
];
|
|
161
174
|
const broken =
|
|
162
175
|
brokenBefore.length === 0
|
|
163
176
|
? []
|
|
@@ -165,7 +178,7 @@ export function report({ held, edited, named, agents, findings, advisory, broken
|
|
|
165
178
|
`${NAME}: advisory, ${brokenBefore.length} path(s), link(s) or command(s) the living docs or agent files name were broken before the range:`,
|
|
166
179
|
...brokenBefore.map(describe),
|
|
167
180
|
];
|
|
168
|
-
return [...verdict, ...unconformed, ...broken].join("\n");
|
|
181
|
+
return [...verdict, ...unconformed, ...sentences, ...broken].join("\n");
|
|
169
182
|
}
|
|
170
183
|
|
|
171
184
|
const docs = Effect.gen(function* () {
|
|
@@ -243,7 +243,7 @@ export function scanMarkdown(text: string): readonly MarkdownLine[] {
|
|
|
243
243
|
|
|
244
244
|
const LEADING_MARKERS = /^(?:\s*>)*\s*(?:#{1,6}\s+|(?:[-*+]|\d{1,9}[.)])\s+(?:\[[ xX]\]\s+)?)?/;
|
|
245
245
|
|
|
246
|
-
function bodyOf({ prose }: MarkdownLine): string {
|
|
246
|
+
export function bodyOf({ prose }: MarkdownLine): string {
|
|
247
247
|
const markers = LEADING_MARKERS.exec(prose)?.[0] ?? "";
|
|
248
248
|
return BLANK.repeat(markers.length) + prose.slice(markers.length);
|
|
249
249
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { bodyOf, scanMarkdown } from "./prose-matchers.ts";
|
|
2
|
+
|
|
3
|
+
export const PROCEDURAL_WORD_CAP = 20;
|
|
4
|
+
export const DESCRIPTIVE_WORD_CAP = 25;
|
|
5
|
+
|
|
6
|
+
export type SentenceKind = "procedural" | "descriptive";
|
|
7
|
+
|
|
8
|
+
export type SentenceLength = {
|
|
9
|
+
readonly line: number;
|
|
10
|
+
readonly words: number;
|
|
11
|
+
readonly kind: SentenceKind;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type LongSentence = SentenceLength & {
|
|
15
|
+
readonly message: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function graphemesOf(body: string): readonly string[] {
|
|
19
|
+
return [...new Intl.Segmenter().segment(body)].map(({ segment }) => segment);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function dropPrefix(raw: string): string {
|
|
23
|
+
const chars = graphemesOf(raw);
|
|
24
|
+
for (const [at, char] of chars.entries()) {
|
|
25
|
+
if (char !== " " && char !== "\t" && char !== ">") return chars.slice(at).join("");
|
|
26
|
+
}
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function orderedAt(raw: string): boolean {
|
|
31
|
+
const rest = dropPrefix(raw);
|
|
32
|
+
const digits = /^\d+/.exec(rest)?.[0] ?? "";
|
|
33
|
+
if (digits === "") return false;
|
|
34
|
+
const marker = rest.charAt(digits.length);
|
|
35
|
+
if (marker !== "." && marker !== ")") return false;
|
|
36
|
+
const after = rest.charAt(digits.length + 1);
|
|
37
|
+
return after === " " || after === "\t" || after === "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isWordChar(char: string): boolean {
|
|
41
|
+
return (char >= "0" && char <= "9") || (char >= "A" && char <= "Z") || (char >= "a" && char <= "z");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function wordsIn(sentence: string): number {
|
|
45
|
+
let words = 0;
|
|
46
|
+
let inToken = false;
|
|
47
|
+
let holdsWordChar = false;
|
|
48
|
+
for (const char of sentence) {
|
|
49
|
+
if (char === " " || char === "\t") {
|
|
50
|
+
if (inToken && holdsWordChar) words += 1;
|
|
51
|
+
inToken = false;
|
|
52
|
+
holdsWordChar = false;
|
|
53
|
+
} else {
|
|
54
|
+
inToken = true;
|
|
55
|
+
if (isWordChar(char)) holdsWordChar = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return inToken && holdsWordChar ? words + 1 : words;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isCloser(char: string): boolean {
|
|
62
|
+
return char === '"' || char === "'" || char === ")" || char === "]" || char === "*" || char === "_" || char === "”" || char === "’";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sentencesIn(body: string): readonly string[] {
|
|
66
|
+
const chars = graphemesOf(body);
|
|
67
|
+
const sentences: string[] = [];
|
|
68
|
+
let start = 0;
|
|
69
|
+
let cut = -1;
|
|
70
|
+
for (const [at, char] of chars.entries()) {
|
|
71
|
+
if (char === "." || char === "!" || char === "?") cut = at + 1;
|
|
72
|
+
else if (cut === at && isCloser(char)) cut = at + 1;
|
|
73
|
+
else if (cut > start && (char === " " || char === "\t")) {
|
|
74
|
+
sentences.push(chars.slice(start, cut).join(""));
|
|
75
|
+
start = cut;
|
|
76
|
+
cut = -1;
|
|
77
|
+
} else if (cut > start) cut = -1;
|
|
78
|
+
}
|
|
79
|
+
if (cut === chars.length && cut > start) {
|
|
80
|
+
sentences.push(chars.slice(start, cut).join(""));
|
|
81
|
+
start = cut;
|
|
82
|
+
}
|
|
83
|
+
const tail = chars.slice(start).join("");
|
|
84
|
+
if (tail.trim() !== "") sentences.push(tail);
|
|
85
|
+
return sentences;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function sentenceLengths(text: string, within?: ReadonlySet<number>): readonly SentenceLength[] {
|
|
89
|
+
return scanMarkdown(text).flatMap((line) => {
|
|
90
|
+
if (line.kind !== "prose" || (within !== undefined && !within.has(line.line))) return [];
|
|
91
|
+
const kind: SentenceKind = orderedAt(line.raw) ? "procedural" : "descriptive";
|
|
92
|
+
return sentencesIn(bodyOf(line)).map((sentence) => ({ line: line.line, words: wordsIn(sentence), kind }));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function longSentences(text: string, within?: ReadonlySet<number>): readonly LongSentence[] {
|
|
97
|
+
return sentenceLengths(text, within).flatMap(({ line, words, kind }) => {
|
|
98
|
+
const cap = kind === "procedural" ? PROCEDURAL_WORD_CAP : DESCRIPTIVE_WORD_CAP;
|
|
99
|
+
if (words <= cap) return [];
|
|
100
|
+
const rule =
|
|
101
|
+
kind === "procedural"
|
|
102
|
+
? `procedural means an ordered list item; descriptive caps at ${DESCRIPTIVE_WORD_CAP} words`
|
|
103
|
+
: `procedural means an ordered list item, capped at ${PROCEDURAL_WORD_CAP} words`;
|
|
104
|
+
return [{ line, words, kind, message: `carries a ${words}-word ${kind} sentence, over the ${cap}-word cap (${rule})` }];
|
|
105
|
+
});
|
|
106
|
+
}
|
package/src/testing/flake.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Config, Console, Effect, FileSystem, Option, Path, Random, Schema } from "effect";
|
|
3
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
3
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
4
4
|
import { runMain, Usage } from "../core/main.ts";
|
|
5
5
|
import { NAME_SEPARATOR, parseReport, ReportError, reporterArgs, type TestResult } from "./test-report.ts";
|
|
6
6
|
|
package/src/testing/mutation.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Config, Effect, Schema } from "effect";
|
|
3
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
3
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
4
4
|
import { runMain } from "../core/main.ts";
|
|
5
5
|
import { fullRunRefusal } from "./mutation-scope.js";
|
|
6
6
|
|
package/src/testing/test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Config, Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
3
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
4
4
|
import { TEST_ENTRY_POINT } from "../core/gates.ts";
|
|
5
5
|
import { runMain, Usage } from "../core/main.ts";
|
|
6
6
|
import { readSkipDeclarations, type Environment, type SkipDeclaration, type TestTier } from "./test-skips.ts";
|