carveout 0.1.0 → 0.1.2

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.
Files changed (78) hide show
  1. package/AGENTS.md +21 -3
  2. package/NOTICE.md +2 -1
  3. package/README.md +274 -460
  4. package/dist/src/account.d.ts +37 -0
  5. package/dist/src/account.js +275 -0
  6. package/dist/src/api.d.ts +148 -0
  7. package/dist/src/api.js +120 -0
  8. package/dist/src/carve.d.ts +788 -0
  9. package/dist/src/carve.js +1721 -125
  10. package/dist/src/cli.d.ts +10 -0
  11. package/dist/src/cli.js +674 -47
  12. package/dist/src/command.d.ts +10 -0
  13. package/dist/src/core.d.ts +11 -0
  14. package/dist/src/cut.d.ts +78 -0
  15. package/dist/src/cut.js +25 -7
  16. package/dist/src/extract/compiler-config.d.ts +49 -0
  17. package/dist/src/extract/fixture.d.ts +103 -0
  18. package/dist/src/extract/hash.d.ts +6 -0
  19. package/dist/src/extract/package-registry.d.ts +47 -0
  20. package/dist/src/extract/package-types.d.ts +52 -0
  21. package/dist/src/extract/surface.d.ts +44 -0
  22. package/dist/src/extract/typescript.d.ts +180 -0
  23. package/dist/src/gate.d.ts +55 -0
  24. package/dist/src/gate.js +1 -0
  25. package/dist/src/graph.d.ts +142 -0
  26. package/dist/src/graph.js +4 -3
  27. package/dist/src/impact.d.ts +113 -0
  28. package/dist/src/imports.d.ts +67 -0
  29. package/dist/src/mergeback.d.ts +182 -0
  30. package/dist/src/mergeback.js +580 -35
  31. package/dist/src/platform.d.ts +69 -0
  32. package/dist/src/platform.js +184 -0
  33. package/dist/src/policy.d.ts +181 -0
  34. package/dist/src/policy.js +716 -230
  35. package/dist/src/project/build.d.ts +23 -0
  36. package/dist/src/project/catalog.d.ts +25 -0
  37. package/dist/src/project/project.d.ts +287 -0
  38. package/dist/src/project/project.js +479 -52
  39. package/dist/src/project/provenance.d.ts +73 -0
  40. package/dist/src/project/provenance.js +12 -0
  41. package/dist/src/project/runner.d.ts +19 -0
  42. package/dist/src/project/secrets.d.ts +64 -0
  43. package/dist/src/project/vitest-config.d.ts +70 -0
  44. package/dist/src/project/vitest-config.js +291 -0
  45. package/dist/src/pseudonyms/aliases.d.ts +107 -0
  46. package/dist/src/pseudonyms/aliases.js +394 -0
  47. package/dist/src/pseudonyms/draft.d.ts +64 -0
  48. package/dist/src/pseudonyms/draft.js +284 -0
  49. package/dist/src/pseudonyms/folder.d.ts +172 -0
  50. package/dist/src/pseudonyms/folder.js +527 -0
  51. package/dist/src/pseudonyms/index.d.ts +11 -0
  52. package/dist/src/pseudonyms/index.js +11 -0
  53. package/dist/src/pseudonyms/vocabulary.d.ts +113 -0
  54. package/dist/src/pseudonyms/vocabulary.js +522 -0
  55. package/dist/src/readiness.d.ts +71 -0
  56. package/dist/src/readiness.js +131 -0
  57. package/dist/src/receipt.d.ts +154 -0
  58. package/dist/src/receipt.js +296 -0
  59. package/dist/src/sandbox.d.ts +53 -0
  60. package/dist/src/sandbox.js +154 -0
  61. package/dist/src/slice/analysis.d.ts +75 -0
  62. package/dist/src/slice/externals.d.ts +51 -0
  63. package/dist/src/slice/graph.d.ts +69 -0
  64. package/dist/src/slice/manifest.d.ts +262 -0
  65. package/dist/src/slice/manifest.js +4 -3
  66. package/dist/src/slice/reach.d.ts +204 -0
  67. package/dist/src/slice/reach.js +11 -5
  68. package/dist/src/slice/report.d.ts +49 -0
  69. package/dist/src/slice/report.js +5 -2
  70. package/dist/src/specifiers.d.ts +48 -0
  71. package/dist/src/status.d.ts +46 -0
  72. package/dist/src/status.js +3 -2
  73. package/dist/src/test-integrity.d.ts +54 -0
  74. package/dist/src/test-integrity.js +812 -0
  75. package/package.json +24 -5
  76. package/skills/carveout/SKILL.md +517 -0
  77. package/skills/carveout-gate/SKILL.md +98 -0
  78. 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;
@@ -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[];
@@ -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
- const key = `\`${name(file)}\`: ${TEST_LEFT_REASON[reason]}`;
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>;
@@ -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, readRecord } from "./carve.js";
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 = readRecord(root);
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;