carveout 0.1.1 → 0.2.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/AGENTS.md +21 -3
- package/NOTICE.md +2 -1
- package/README.md +248 -473
- package/dist/src/account.d.ts +37 -0
- package/dist/src/account.js +275 -0
- package/dist/src/api.d.ts +162 -0
- package/dist/src/api.js +134 -0
- package/dist/src/carve.d.ts +788 -0
- package/dist/src/carve.js +1721 -125
- package/dist/src/cli.d.ts +10 -0
- package/dist/src/cli.js +674 -47
- package/dist/src/command.d.ts +10 -0
- package/dist/src/core.d.ts +11 -0
- package/dist/src/cut.d.ts +78 -0
- package/dist/src/cut.js +25 -7
- package/dist/src/extract/compiler-config.d.ts +49 -0
- package/dist/src/extract/fixture.d.ts +103 -0
- package/dist/src/extract/hash.d.ts +6 -0
- package/dist/src/extract/package-registry.d.ts +47 -0
- package/dist/src/extract/package-types.d.ts +52 -0
- package/dist/src/extract/surface.d.ts +44 -0
- package/dist/src/extract/typescript.d.ts +180 -0
- package/dist/src/gate.d.ts +55 -0
- package/dist/src/gate.js +1 -0
- package/dist/src/graph.d.ts +142 -0
- package/dist/src/graph.js +4 -3
- package/dist/src/impact.d.ts +113 -0
- package/dist/src/imports.d.ts +67 -0
- package/dist/src/mergeback.d.ts +182 -0
- package/dist/src/mergeback.js +580 -35
- package/dist/src/platform.d.ts +69 -0
- package/dist/src/platform.js +184 -0
- package/dist/src/policy.d.ts +181 -0
- package/dist/src/policy.js +716 -230
- package/dist/src/project/build.d.ts +23 -0
- package/dist/src/project/catalog.d.ts +25 -0
- package/dist/src/project/project.d.ts +287 -0
- package/dist/src/project/project.js +479 -52
- package/dist/src/project/provenance.d.ts +73 -0
- package/dist/src/project/provenance.js +12 -0
- package/dist/src/project/runner.d.ts +19 -0
- package/dist/src/project/secrets.d.ts +64 -0
- package/dist/src/project/vitest-config.d.ts +70 -0
- package/dist/src/project/vitest-config.js +291 -0
- package/dist/src/pseudonyms/aliases.d.ts +107 -0
- package/dist/src/pseudonyms/aliases.js +394 -0
- package/dist/src/pseudonyms/draft.d.ts +64 -0
- package/dist/src/pseudonyms/draft.js +284 -0
- package/dist/src/pseudonyms/folder.d.ts +172 -0
- package/dist/src/pseudonyms/folder.js +527 -0
- package/dist/src/pseudonyms/index.d.ts +11 -0
- package/dist/src/pseudonyms/index.js +11 -0
- package/dist/src/pseudonyms/vocabulary.d.ts +113 -0
- package/dist/src/pseudonyms/vocabulary.js +522 -0
- package/dist/src/readiness.d.ts +71 -0
- package/dist/src/readiness.js +131 -0
- package/dist/src/receipt.d.ts +154 -0
- package/dist/src/receipt.js +296 -0
- package/dist/src/sandbox.d.ts +53 -0
- package/dist/src/sandbox.js +154 -0
- package/dist/src/slice/analysis.d.ts +75 -0
- package/dist/src/slice/externals.d.ts +51 -0
- package/dist/src/slice/graph.d.ts +69 -0
- package/dist/src/slice/manifest.d.ts +262 -0
- package/dist/src/slice/manifest.js +4 -3
- package/dist/src/slice/reach.d.ts +204 -0
- package/dist/src/slice/reach.js +11 -5
- package/dist/src/slice/report.d.ts +49 -0
- package/dist/src/slice/report.js +5 -2
- package/dist/src/specifiers.d.ts +48 -0
- package/dist/src/status.d.ts +46 -0
- package/dist/src/status.js +3 -2
- package/dist/src/test-integrity.d.ts +54 -0
- package/dist/src/test-integrity.js +812 -0
- package/package.json +24 -5
- package/skills/carveout/SKILL.md +518 -0
- package/skills/carveout-gate/SKILL.md +98 -0
- package/SKILL.md +0 -423
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bounded walk that proposes a slice.
|
|
3
|
+
*
|
|
4
|
+
* Starting from the entry points, the walk follows what a file's own code
|
|
5
|
+
* imports, breadth first, until the budget is spent: `maxDepth` imports
|
|
6
|
+
* from the nearest entry point, `maxFiles` files in all. Everything reached
|
|
7
|
+
* inside the budget is _included_; every link that leaves the included set
|
|
8
|
+
* is a _cut_, outbound where the slice needs something outside and inbound
|
|
9
|
+
* where outside code needs the slice.
|
|
10
|
+
*
|
|
11
|
+
* It follows names, not whole files. An import of `a` from a barrel
|
|
12
|
+
* includes the barrel and, through its re-exports, only the module that
|
|
13
|
+
* declares `a`, at the barrel's depth: a re-export is not an import of the
|
|
14
|
+
* barrel's own code. An import of types only (`import type`, or names the
|
|
15
|
+
* target declares as interfaces or type aliases) is erased at runtime, so
|
|
16
|
+
* its module is cut and stubbed, never copied. A module named to a test
|
|
17
|
+
* runner (`vi.mock`) is never followed either; a file read by path is.
|
|
18
|
+
*
|
|
19
|
+
* Two options move the cut from the hop count to the package: with
|
|
20
|
+
* `wholePackage`, every file of the entry points' package the walk reaches
|
|
21
|
+
* comes along at any depth, so a sibling module is copied, never stubbed;
|
|
22
|
+
* and a test brought along by `tests` brings its own helpers (the fakes and
|
|
23
|
+
* builders it imports from its package, or from a test folder anywhere),
|
|
24
|
+
* since a stub in a fake's place fails the test before any change is made
|
|
25
|
+
* (E6c: 44% of the tests failing on a fresh cut reached such a stub). A
|
|
26
|
+
* file the owner marks `private` is never entered, wherever it is reached:
|
|
27
|
+
* it is cut and stubbed like a module past the budget.
|
|
28
|
+
*
|
|
29
|
+
* Pure and deterministic: states are visited in path order within a level,
|
|
30
|
+
* so the same graph, entry points and budget give the same slice however
|
|
31
|
+
* the graph ordered its arrays. The budget bounds the proposal; it does not
|
|
32
|
+
* prove the slice closed. Imports the compiler could not resolve and
|
|
33
|
+
* dynamic imports are reported as dependencies for the caller to turn into
|
|
34
|
+
* blockers, never dropped.
|
|
35
|
+
*/
|
|
36
|
+
import type { SliceBudget } from "./analysis.js";
|
|
37
|
+
import type { SliceGraph, SliceRelation } from "./graph.js";
|
|
38
|
+
/** One link between two repository files. */
|
|
39
|
+
export interface CutEdge {
|
|
40
|
+
readonly from: string;
|
|
41
|
+
readonly to: string;
|
|
42
|
+
readonly relation: SliceRelation;
|
|
43
|
+
readonly specifier: string | null;
|
|
44
|
+
readonly location: string | null;
|
|
45
|
+
/** Erased at runtime: the stub alone serves it. */
|
|
46
|
+
readonly typeOnly: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** An import from an included file that resolves to no repository file. */
|
|
49
|
+
export interface SliceDependency {
|
|
50
|
+
readonly file: string;
|
|
51
|
+
/** The import text; for a path read, the path from the repository's
|
|
52
|
+
* root; null for a dynamic import of a computed name. */
|
|
53
|
+
readonly specifier: string | null;
|
|
54
|
+
/** `path`: a literal path read at runtime that names no file a slice
|
|
55
|
+
* can copy (a folder, a file of another kind). */
|
|
56
|
+
readonly status: "external" | "unresolved" | "dynamic" | "path";
|
|
57
|
+
readonly location: string | null;
|
|
58
|
+
}
|
|
59
|
+
export interface ReachResult {
|
|
60
|
+
/** Entry points resolved to files, sorted. */
|
|
61
|
+
readonly entries: readonly string[];
|
|
62
|
+
/** Entry points naming nothing in the graph, in the order given. */
|
|
63
|
+
readonly unknownEntryPoints: readonly string[];
|
|
64
|
+
/** Files inside the budget, sorted by path. */
|
|
65
|
+
readonly included: readonly string[];
|
|
66
|
+
/** Imports from the nearest entry point, per included file. */
|
|
67
|
+
readonly depthOf: Readonly<Record<string, number>>;
|
|
68
|
+
/**
|
|
69
|
+
* Files the walk reached within the depth that did not fit the file
|
|
70
|
+
* budget, sorted. They are cut like any module past the budget.
|
|
71
|
+
*/
|
|
72
|
+
readonly dropped: readonly string[];
|
|
73
|
+
readonly cuts: {
|
|
74
|
+
readonly outbound: readonly CutEdge[];
|
|
75
|
+
/** Imports of the slice from outside it; never a `references` or
|
|
76
|
+
* `reads` link. */
|
|
77
|
+
readonly inbound: readonly CutEdge[];
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Links between included files that carry the specifier the source
|
|
81
|
+
* wrote, sorted. A generated project resolves its imports with these and
|
|
82
|
+
* the outbound cuts, so a path alias or a moved file can be rewritten
|
|
83
|
+
* without the source's compiler configuration.
|
|
84
|
+
*/
|
|
85
|
+
readonly internal: readonly CutEdge[];
|
|
86
|
+
readonly dependencies: readonly SliceDependency[];
|
|
87
|
+
/** Of the included files, the tests brought along, sorted. */
|
|
88
|
+
readonly tests: readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* Of the included files, the helpers the tests brought along: what a
|
|
91
|
+
* test imports at runtime from its own package, or from a test folder,
|
|
92
|
+
* that the slice did not already hold. Sorted.
|
|
93
|
+
*/
|
|
94
|
+
readonly helpers: readonly string[];
|
|
95
|
+
/** Files the walk reached and kept out as private, sorted; each is a cut. */
|
|
96
|
+
readonly privateFiles: readonly string[];
|
|
97
|
+
/** Of the included files, the pure code copied rather than mocked, sorted. */
|
|
98
|
+
readonly pure: readonly string[];
|
|
99
|
+
/**
|
|
100
|
+
* Tests of the slice left behind, sorted: they run on neither `node:test`
|
|
101
|
+
* nor Vitest, or make an import no slice can carry (a computed dynamic
|
|
102
|
+
* import, one that resolves to nothing, a path read of no file).
|
|
103
|
+
*/
|
|
104
|
+
readonly untestable: readonly string[];
|
|
105
|
+
/**
|
|
106
|
+
* Every test that runs the slice's code, directly or through a chain of
|
|
107
|
+
* barrels, that the folder does not hold, with the reason; sorted. A
|
|
108
|
+
* change these tests would catch passes in the folder unseen.
|
|
109
|
+
*/
|
|
110
|
+
readonly testsLeft: readonly SliceTestLeft[];
|
|
111
|
+
/** Of the tests, those on Vitest's globals, which import no runner. */
|
|
112
|
+
readonly globalsTests: readonly string[];
|
|
113
|
+
/** Barrels outside the slice that forward names from it, sorted. */
|
|
114
|
+
readonly barrels: readonly string[];
|
|
115
|
+
/** Why each included file is in the slice. */
|
|
116
|
+
readonly originOf: Readonly<Record<string, SliceOrigin>>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Why a file is in the slice: an entry point, reached within the depth,
|
|
120
|
+
* brought by `wholePackage`, a test, a barrel a test imports through, a
|
|
121
|
+
* test's helper, code of the package the tests run (`wholePackage` after
|
|
122
|
+
* the tests), or pure code copied rather than mocked.
|
|
123
|
+
*/
|
|
124
|
+
export type SliceOrigin = "entry" | "walk" | "package" | "test" | "barrel" | "helper" | "test-code" | "pure";
|
|
125
|
+
/**
|
|
126
|
+
* A test that runs the slice's code and is not in the folder: it was not
|
|
127
|
+
* asked for (`tests` off), it tests another package, it runs on no runner
|
|
128
|
+
* a slice runs (Jest, say), it reads what no slice carries, or the budget
|
|
129
|
+
* ran out.
|
|
130
|
+
*/
|
|
131
|
+
export interface SliceTestLeft {
|
|
132
|
+
readonly file: string;
|
|
133
|
+
readonly reason: "not-asked" | "other-package" | "runner" | "unreadable" | "budget";
|
|
134
|
+
/**
|
|
135
|
+
* The slice's files it imports itself, which it tests: named so a
|
|
136
|
+
* contributor knows what a change only it would catch is in. Empty when
|
|
137
|
+
* it reaches the slice only through a barrel outside it.
|
|
138
|
+
*/
|
|
139
|
+
readonly imports?: readonly string[];
|
|
140
|
+
}
|
|
141
|
+
export interface ReachOptions {
|
|
142
|
+
/**
|
|
143
|
+
* Files walked from as the entry points are, which are no entry point:
|
|
144
|
+
* the setup files a package's Vitest tests run before them. They come
|
|
145
|
+
* along with what they import, as the tests' helpers; the entry points,
|
|
146
|
+
* their package and their demands are the caller's alone.
|
|
147
|
+
*/
|
|
148
|
+
readonly roots?: readonly string[];
|
|
149
|
+
/**
|
|
150
|
+
* Also include each test file outside the slice that imports it, directly
|
|
151
|
+
* or through a barrel that re-exports from it, and runs on `node:test` or
|
|
152
|
+
* Vitest. Such a barrel comes along too, its other re-exports cut. Tests
|
|
153
|
+
* and barrels count against the file budget.
|
|
154
|
+
*/
|
|
155
|
+
readonly tests?: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* The package folder a file belongs to (`""` for the root), for
|
|
158
|
+
* `wholePackage` and for a test's helpers. Every file is in one package
|
|
159
|
+
* without it.
|
|
160
|
+
*/
|
|
161
|
+
readonly packageOf?: (file: string) => string;
|
|
162
|
+
/**
|
|
163
|
+
* Follow the entry points' package past the depth: every file of it the
|
|
164
|
+
* included files import at runtime (or read by path) comes along, tests
|
|
165
|
+
* apart, while the file budget lasts. Imports into other packages still
|
|
166
|
+
* stop at the depth.
|
|
167
|
+
*/
|
|
168
|
+
readonly wholePackage?: boolean;
|
|
169
|
+
/** Files never entered: cut and stubbed wherever the walk reaches them. */
|
|
170
|
+
readonly isPrivate?: (file: string) => boolean;
|
|
171
|
+
/**
|
|
172
|
+
* The runner of a test file, from its path and the bare specifiers it
|
|
173
|
+
* imports; `testRunnerOf` without it, which knows only a runner imported.
|
|
174
|
+
*/
|
|
175
|
+
readonly runnerOf?: (file: string, specifiers: readonly string[]) => TestRunner | null;
|
|
176
|
+
/**
|
|
177
|
+
* Copy pure code rather than mock it: a module the slice imports at
|
|
178
|
+
* runtime from outside comes along, past the depth, when every module
|
|
179
|
+
* its runtime imports reach outside the slice is pure. A mock of a
|
|
180
|
+
* validator, an error class or a formatter answers with more mocks, and
|
|
181
|
+
* the slice's tests fail on it before any change (a red baseline); the
|
|
182
|
+
* real code runs the same in the folder as in the repository.
|
|
183
|
+
*/
|
|
184
|
+
readonly pure?: {
|
|
185
|
+
/** A package a pure module may import: no service, no I/O. */
|
|
186
|
+
readonly package: (specifier: string) => boolean;
|
|
187
|
+
/** Whether the file's own code reads the environment, the disk or the network. */
|
|
188
|
+
readonly sideEffects: (file: string) => boolean;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** `x.test.ts`, `x.spec.tsx`, or anything under `__tests__/`. */
|
|
192
|
+
export declare const TEST_PATH_PATTERN: RegExp;
|
|
193
|
+
export declare function isTestPath(path: string): boolean;
|
|
194
|
+
/** A file in a folder of test support: fakes, fixtures, builders. */
|
|
195
|
+
export declare const TEST_SUPPORT_PATH_PATTERN: RegExp;
|
|
196
|
+
export declare function isTestSupportPath(path: string): boolean;
|
|
197
|
+
/**
|
|
198
|
+
* A runner a slice runs tests on: `node:test`, Vitest imported, or Vitest
|
|
199
|
+
* with its globals (`describe`, `it`, `expect` unimported).
|
|
200
|
+
*/
|
|
201
|
+
export type TestRunner = "node" | "vitest" | "vitest-globals";
|
|
202
|
+
/** The test runner a test file imports, when carveout can run it. */
|
|
203
|
+
export declare function testRunnerOf(specifiers: readonly string[]): "node" | "vitest" | null;
|
|
204
|
+
export declare function reach(graph: SliceGraph, entryPoints: readonly string[], budget: SliceBudget, options?: ReachOptions): ReachResult;
|
package/dist/src/slice/reach.js
CHANGED
|
@@ -206,6 +206,7 @@ export function reach(graph, entryPoints, budget, options = {}) {
|
|
|
206
206
|
for (const file of resolved)
|
|
207
207
|
entries.add(file);
|
|
208
208
|
}
|
|
209
|
+
const roots = new Set((options.roots ?? []).flatMap((root) => resolveEntry(root, byId, files)).filter((file) => !entries.has(file)));
|
|
209
210
|
const maxFiles = Math.max(0, Math.floor(budget.maxFiles));
|
|
210
211
|
const maxDepth = Math.max(0, Math.floor(budget.maxDepth));
|
|
211
212
|
const packageOf = options.packageOf ?? (() => "");
|
|
@@ -226,7 +227,7 @@ export function reach(graph, entryPoints, budget, options = {}) {
|
|
|
226
227
|
const dropped = new Set();
|
|
227
228
|
const seen = new Set();
|
|
228
229
|
const byState = (a, b) => compare(a[0], b[0]) || compare(a[1], b[1]);
|
|
229
|
-
let level = [...entries].map((file) => [file, EVERYTHING]);
|
|
230
|
+
let level = [...entries, ...roots].map((file) => [file, EVERYTHING]);
|
|
230
231
|
let depth = 0;
|
|
231
232
|
while (level.length > 0 && depth <= maxDepth && dropped.size === 0) {
|
|
232
233
|
const work = level.sort(byState);
|
|
@@ -242,7 +243,7 @@ export function reach(graph, entryPoints, budget, options = {}) {
|
|
|
242
243
|
return false;
|
|
243
244
|
}
|
|
244
245
|
depthOf.set(file, depth);
|
|
245
|
-
originOf.set(file, entries.has(file) ? "entry" : "walk");
|
|
246
|
+
originOf.set(file, entries.has(file) ? "entry" : roots.has(file) ? "helper" : "walk");
|
|
246
247
|
return true;
|
|
247
248
|
};
|
|
248
249
|
// A re-export forwards a name at the barrel's depth; an import of the
|
|
@@ -366,7 +367,7 @@ export function reach(graph, entryPoints, budget, options = {}) {
|
|
|
366
367
|
demand.set(file, set);
|
|
367
368
|
return grew;
|
|
368
369
|
};
|
|
369
|
-
for (const file of entries)
|
|
370
|
+
for (const file of [...entries, ...roots])
|
|
370
371
|
if (depthOf.has(file))
|
|
371
372
|
add(file, EVERYTHING);
|
|
372
373
|
for (let changed = true; changed;) {
|
|
@@ -568,10 +569,11 @@ export function reach(graph, entryPoints, budget, options = {}) {
|
|
|
568
569
|
if (link.relation !== "imports" || link.typeOnly)
|
|
569
570
|
continue;
|
|
570
571
|
const through = barrels.get(edge.to);
|
|
571
|
-
const entry = exercising.get(file) ?? { through: new Set(), own: false };
|
|
572
|
+
const entry = exercising.get(file) ?? { through: new Set(), own: false, imports: new Set() };
|
|
572
573
|
if (code.has(edge.to)) {
|
|
573
574
|
if (packageOf(edge.to) === mine)
|
|
574
575
|
entry.own = true;
|
|
576
|
+
entry.imports.add(edge.to);
|
|
575
577
|
exercising.set(file, entry);
|
|
576
578
|
}
|
|
577
579
|
else if (through !== undefined &&
|
|
@@ -583,6 +585,8 @@ export function reach(graph, entryPoints, budget, options = {}) {
|
|
|
583
585
|
entry.own = true;
|
|
584
586
|
entry.through.add(edge.to);
|
|
585
587
|
}
|
|
588
|
+
// Named only if the slice holds the barrel itself.
|
|
589
|
+
entry.imports.add(edge.to);
|
|
586
590
|
exercising.set(file, entry);
|
|
587
591
|
}
|
|
588
592
|
}
|
|
@@ -720,11 +724,13 @@ export function reach(graph, entryPoints, budget, options = {}) {
|
|
|
720
724
|
}
|
|
721
725
|
/** The tests of the slice's code the folder does not carry, and why. */
|
|
722
726
|
const testsLeft = [];
|
|
723
|
-
for (const [file, { own }] of [...exercising].sort(([a], [b]) => compare(a, b))) {
|
|
727
|
+
for (const [file, { own, imports }] of [...exercising].sort(([a], [b]) => compare(a, b))) {
|
|
724
728
|
if (depthOf.has(file))
|
|
725
729
|
continue;
|
|
730
|
+
const named = [...imports].filter((path) => depthOf.has(path)).sort(compare);
|
|
726
731
|
testsLeft.push({
|
|
727
732
|
file,
|
|
733
|
+
...(named.length === 0 ? {} : { imports: named }),
|
|
728
734
|
reason: !own
|
|
729
735
|
? "other-package"
|
|
730
736
|
: options.tests !== true
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a slice shows and what it tests, in words a maintainer and a
|
|
3
|
+
* contributor read before trusting a pass in the folder.
|
|
4
|
+
*
|
|
5
|
+
* Exposure counts the files and lines a cut copies, by why each is there,
|
|
6
|
+
* so the cost of `--whole-package`, `--with-tests` and `--copy-pure` in code
|
|
7
|
+
* shown is a number before anything is handed out. The tests account names
|
|
8
|
+
* every test that runs the slice's code and stays in the repository: a
|
|
9
|
+
* change one of them would catch passes in the folder unseen.
|
|
10
|
+
*/
|
|
11
|
+
import { type ReachResult, type SliceOrigin, type SliceTestLeft } from "./reach.js";
|
|
12
|
+
export interface SliceExposure {
|
|
13
|
+
readonly files: number;
|
|
14
|
+
readonly lines: number;
|
|
15
|
+
/** By origin, in `ORIGIN_ORDER`, origins with no file left out. */
|
|
16
|
+
readonly byOrigin: readonly {
|
|
17
|
+
readonly origin: SliceOrigin;
|
|
18
|
+
readonly files: number;
|
|
19
|
+
readonly lines: number;
|
|
20
|
+
}[];
|
|
21
|
+
/**
|
|
22
|
+
* The source the folder copies from outside the entry points, tests left
|
|
23
|
+
* out, by folder, most lines first: what the contributor reads beyond
|
|
24
|
+
* the part handed to them, and what `--private` would keep out. Absent
|
|
25
|
+
* from a record written before it was counted.
|
|
26
|
+
*/
|
|
27
|
+
readonly outside?: readonly {
|
|
28
|
+
readonly folder: string;
|
|
29
|
+
readonly files: number;
|
|
30
|
+
readonly lines: number;
|
|
31
|
+
}[];
|
|
32
|
+
}
|
|
33
|
+
/** Lines and files the walk's included files hold, by origin. */
|
|
34
|
+
export declare function exposureOf(walk: Pick<ReachResult, "included" | "originOf">, read: (path: string) => string | null): SliceExposure;
|
|
35
|
+
/** One line: `24 files, 3,410 lines: 1 entry point (180), ...`. */
|
|
36
|
+
export declare function renderExposure(exposure: SliceExposure): string;
|
|
37
|
+
/**
|
|
38
|
+
* The source copied from outside the entry points, by folder, as lines for
|
|
39
|
+
* `plan` and `cut`; nothing when there is none.
|
|
40
|
+
*/
|
|
41
|
+
export declare function renderOutside(exposure: SliceExposure): string[];
|
|
42
|
+
export declare const TEST_LEFT_REASON: Readonly<Record<SliceTestLeft["reason"], string>>;
|
|
43
|
+
/** What a pass in the folder says, given the tests it holds and leaves. */
|
|
44
|
+
export declare function testsVerdict(walk: Pick<ReachResult, "tests" | "testsLeft">): string;
|
|
45
|
+
/**
|
|
46
|
+
* The tests section of BOUNDARY.md. `name` writes a path outside the slice
|
|
47
|
+
* as the folder may show it (a public cut names only its package).
|
|
48
|
+
*/
|
|
49
|
+
export declare function renderTestsMarkdown(walk: Pick<ReachResult, "tests" | "testsLeft">, name: (path: string) => string): string[];
|
package/dist/src/slice/report.js
CHANGED
|
@@ -120,8 +120,11 @@ export function renderTestsMarkdown(walk, name) {
|
|
|
120
120
|
if (walk.testsLeft.length > 0) {
|
|
121
121
|
lines.push("Left in the repository, which runs them on the change after the merge:", "");
|
|
122
122
|
const grouped = new Map();
|
|
123
|
-
for (const { file, reason } of walk.testsLeft) {
|
|
124
|
-
|
|
123
|
+
for (const { file, reason, imports } of walk.testsLeft) {
|
|
124
|
+
// What it tests, by the slice's files it imports: a change to one of
|
|
125
|
+
// those is what only it would catch.
|
|
126
|
+
const tests = imports === undefined || imports.length === 0 ? "" : `; it imports ${imports.map((path) => `\`${path}\``).join(", ")}`;
|
|
127
|
+
const key = `\`${name(file)}\`: ${TEST_LEFT_REASON[reason]}${tests}`;
|
|
125
128
|
grouped.set(key, (grouped.get(key) ?? 0) + 1);
|
|
126
129
|
}
|
|
127
130
|
for (const [key, count] of grouped)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The module specifiers a source writes, read from its syntax tree rather
|
|
3
|
+
* than by pattern: imports and re-exports, `require` and `import()`,
|
|
4
|
+
* `import("x").T` in a type, `import x = require("y")`, and the calls that
|
|
5
|
+
* name a module to a test runner or a resolver (`vi.mock`,
|
|
6
|
+
* `jest.requireActual`, `require.resolve`, `import.meta.resolve`).
|
|
7
|
+
*
|
|
8
|
+
* A regex over the text (the former `IMPORT_LITERAL`) misread an import
|
|
9
|
+
* quoted in a comment, a string that said `from`, or a clause spanning
|
|
10
|
+
* lines, and each misreading was a false `import_unrewritable` blocker or
|
|
11
|
+
* a specifier left unrewritten (E6c: 23 such blockers, 3 green templates
|
|
12
|
+
* marked not ready). The tree has none of those: a specifier is a string
|
|
13
|
+
* literal in one of the positions below, and nothing else is one.
|
|
14
|
+
*/
|
|
15
|
+
import ts from "typescript";
|
|
16
|
+
/** Where a specifier sits in the source. */
|
|
17
|
+
export interface SpecifierLiteral {
|
|
18
|
+
/** The text between the quotes. */
|
|
19
|
+
readonly specifier: string;
|
|
20
|
+
/** Offsets of that text in the source. */
|
|
21
|
+
readonly start: number;
|
|
22
|
+
readonly end: number;
|
|
23
|
+
/**
|
|
24
|
+
* `import`: an import, re-export or `import()`; `require`: a `require`
|
|
25
|
+
* call; `type`: `import("x")` in a type; `reference`: a call that names a
|
|
26
|
+
* module without loading it (`vi.mock`, `require.resolve`).
|
|
27
|
+
*/
|
|
28
|
+
readonly kind: "import" | "require" | "type" | "reference";
|
|
29
|
+
/** Whether only types are imported (`import type`, `export type`). */
|
|
30
|
+
readonly typeOnly: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Calls that name a module to a test runner or a resolver. */
|
|
33
|
+
export declare const MODULE_REFERENCE_CALLS: readonly string[];
|
|
34
|
+
/** The parse of `text` as the file at `path` would be parsed. */
|
|
35
|
+
export declare function parseSource(path: string, text: string): ts.SourceFile;
|
|
36
|
+
/**
|
|
37
|
+
* Every module specifier in `text`, in source order. The same literal is
|
|
38
|
+
* never reported twice.
|
|
39
|
+
*/
|
|
40
|
+
export declare function moduleSpecifiers(path: string, text: string, source?: ts.SourceFile): SpecifierLiteral[];
|
|
41
|
+
/** The distinct specifiers of `text`, in source order. */
|
|
42
|
+
export declare function specifiersIn(path: string, text: string): string[];
|
|
43
|
+
/**
|
|
44
|
+
* `text` with each specifier replaced by what `replace` returns for it, or
|
|
45
|
+
* left as written when it returns null. Positions come from one parse, so
|
|
46
|
+
* a replacement never shifts another's.
|
|
47
|
+
*/
|
|
48
|
+
export declare function rewriteSpecifiers(path: string, text: string, replace: (literal: SpecifierLiteral) => string | null): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `carveout status`: whether a cut folder still matches its repository.
|
|
3
|
+
*
|
|
4
|
+
* The cut is made again in memory, with the request `carveout.json`
|
|
5
|
+
* records, against the repository as it stands (HEAD and the working
|
|
6
|
+
* tree), and every file it would write is compared with what the folder's
|
|
7
|
+
* record says was written. The folder is current when no file would differ;
|
|
8
|
+
* otherwise each difference is named with its cause: a slice file that
|
|
9
|
+
* changed, a cut module whose declarations changed (by symbol), a file the
|
|
10
|
+
* walk now reaches or no longer reaches, or generated configuration. A
|
|
11
|
+
* change to a cut module's bodies alone leaves its stub, and so the
|
|
12
|
+
* folder, as it was. The lockfile the cut resolved from the registry is not
|
|
13
|
+
* compared: it is the registry's answer, not the repository's.
|
|
14
|
+
*
|
|
15
|
+
* Nothing is written; the package types a cut reads come from the cache
|
|
16
|
+
* when the set is the same.
|
|
17
|
+
*/
|
|
18
|
+
export type StaleCause = "source_changed" | "source_added" | "source_removed" | "stub_changed" | "stub_added" | "stub_removed" | "surface_changed" | "generated_changed" | "cut_failed";
|
|
19
|
+
export interface StaleItem {
|
|
20
|
+
readonly cause: StaleCause;
|
|
21
|
+
readonly path: string | null;
|
|
22
|
+
readonly detail: string;
|
|
23
|
+
}
|
|
24
|
+
export interface Status {
|
|
25
|
+
readonly current: boolean;
|
|
26
|
+
readonly stale: readonly StaleItem[];
|
|
27
|
+
/** Resolution inputs that changed, whatever they did to the cut. */
|
|
28
|
+
readonly inputsChanged: readonly string[];
|
|
29
|
+
/** Files outside the slice that import it now and did not, or the reverse. */
|
|
30
|
+
readonly importers: {
|
|
31
|
+
readonly added: readonly string[];
|
|
32
|
+
readonly removed: readonly string[];
|
|
33
|
+
};
|
|
34
|
+
readonly commit: string | null;
|
|
35
|
+
}
|
|
36
|
+
/** Each exported name a declaration file declares, with its text. */
|
|
37
|
+
export declare function declaredSymbols(text: string): Map<string, string>;
|
|
38
|
+
/** Symbols added, removed and changed between two versions of a stub. */
|
|
39
|
+
export declare function symbolDiff(before: string, after: string): string;
|
|
40
|
+
export declare function statusProject(dir: string, options?: {
|
|
41
|
+
readonly repo?: string;
|
|
42
|
+
/** The cut's receipt file; else the one `CARVEOUT_HOME` keeps for its id. */
|
|
43
|
+
readonly receipt?: string;
|
|
44
|
+
/** No receipt: the folder's own record is all there is. */
|
|
45
|
+
readonly noReceipt?: boolean;
|
|
46
|
+
}): Promise<Status>;
|
package/dist/src/status.js
CHANGED
|
@@ -19,7 +19,8 @@ import { createHash } from "node:crypto";
|
|
|
19
19
|
import { readFileSync } from "node:fs";
|
|
20
20
|
import { join, resolve } from "node:path";
|
|
21
21
|
import ts from "typescript";
|
|
22
|
-
import { computeCut
|
|
22
|
+
import { computeCut } from "./carve.js";
|
|
23
|
+
import { trustedCut } from "./receipt.js";
|
|
23
24
|
import { trustedRepo } from "./policy.js";
|
|
24
25
|
const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
25
26
|
const sha256 = (data) => createHash("sha256").update(data).digest("hex");
|
|
@@ -78,7 +79,7 @@ export function symbolDiff(before, after) {
|
|
|
78
79
|
const stubOf = (path) => path.replace(/\.d\.([cm]?)ts$/, ".$1js");
|
|
79
80
|
export async function statusProject(dir, options = {}) {
|
|
80
81
|
const root = resolve(dir);
|
|
81
|
-
const record =
|
|
82
|
+
const record = trustedCut(root, options.receipt, options.noReceipt === true ? { noReceipt: true } : {}).record;
|
|
82
83
|
const repo = trustedRepo(record, options.repo);
|
|
83
84
|
const request = record.request;
|
|
84
85
|
let fresh;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether an edit to a test weakens it: read from the syntax of the test
|
|
3
|
+
* as it was and as it is, never run.
|
|
4
|
+
*
|
|
5
|
+
* A test may grow. Its assertions, the statements they rest on (the data
|
|
6
|
+
* passed in, the value compared with, the setup, a fake's method), and
|
|
7
|
+
* the tests it skips or focuses are what it held the code to, and an edit
|
|
8
|
+
* that removes, rewrites, guards or outruns one of them is held for the
|
|
9
|
+
* maintainer: an expected value changed may be the change that was asked
|
|
10
|
+
* for, or a test bent to pass. Statements are compared as TypeScript prints
|
|
11
|
+
* them, so formatting is no change and a string's own characters are.
|
|
12
|
+
*/
|
|
13
|
+
import ts from "typescript";
|
|
14
|
+
/** Calls whose function arguments hold a test or its hooks. */
|
|
15
|
+
export declare const TEST_STRUCTURE: ReadonlySet<string>;
|
|
16
|
+
/** The script kind TypeScript should parse a path as. */
|
|
17
|
+
export declare const scriptKindOf: (path: string) => ts.ScriptKind.TS | ts.ScriptKind.TSX;
|
|
18
|
+
/**
|
|
19
|
+
* A node as TypeScript prints it, its strings in one quoting: the same
|
|
20
|
+
* code, however it is laid out, quoted or semicoloned, prints the same,
|
|
21
|
+
* and a string's own characters are kept. A trailing comma is layout.
|
|
22
|
+
*/
|
|
23
|
+
export declare function canonical(node: ts.Node, source: ts.SourceFile): string;
|
|
24
|
+
/**
|
|
25
|
+
* The names a test file's assertions go by: `assert` and `expect`, every
|
|
26
|
+
* name imported from an assertion module (`strictEqual` from `node:assert`),
|
|
27
|
+
* and from a runner's module only its asserting names (`expect` from
|
|
28
|
+
* `vitest`, never `describe`), whether imported or required.
|
|
29
|
+
*/
|
|
30
|
+
export declare function assertionRoots(source: ts.SourceFile): Set<string>;
|
|
31
|
+
/** The identifier a call or member chain starts at, or null. */
|
|
32
|
+
export declare function chainRoot(node: ts.Expression): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Whether an expression is a constant the code can read without running
|
|
35
|
+
* it: `true`, `false`, `0`, `1`, `""`, `null`, `undefined`, `!x` of one, or
|
|
36
|
+
* one in parentheses. Null for anything else.
|
|
37
|
+
*/
|
|
38
|
+
export declare function constantOf(node: ts.Expression): boolean | null;
|
|
39
|
+
/** What `before` holds that `after` does not, counted as multisets. */
|
|
40
|
+
export declare function removedFrom(before: readonly string[], after: readonly string[]): string[];
|
|
41
|
+
/** Whether a module specifier names the code's own module: a path, or a path alias. */
|
|
42
|
+
export declare function ownSpecifier(specifier: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Why a change to a test weakens it, or null: an assertion removed,
|
|
45
|
+
* rewritten or put where it may not run; a test skipped or focused; a
|
|
46
|
+
* statement the assertions rest on rewritten or removed; a mock, spy or
|
|
47
|
+
* member write of the code's own module added, which can stand in for the
|
|
48
|
+
* code under test; or a statement added that changes what a kept assertion
|
|
49
|
+
* reads (one that declares a name the test already reads, a hook in a
|
|
50
|
+
* scope that holds tests, top-level code, or a statement before a kept
|
|
51
|
+
* assertion in its body). A new test, a new import, a new helper with a
|
|
52
|
+
* name of its own and statements after the last assertion pass.
|
|
53
|
+
*/
|
|
54
|
+
export declare function weakenedTest(path: string, before: string, after: string, noun?: string): string | null;
|