carveout 0.1.1 → 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.
- 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 +148 -0
- package/dist/src/api.js +120 -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 +517 -0
- package/skills/carveout-gate/SKILL.md +98 -0
- package/SKILL.md +0 -423
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { type SurfaceFile } from "./surface.js";
|
|
2
|
+
export { declarationPath as declarationPathFor, packageNameOf, } from "../core.js";
|
|
3
|
+
export { loadConfig } from "./compiler-config.js";
|
|
4
|
+
import type { BoundaryModule, CutEdge, SliceBlocker } from "../core.js";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
export declare const TYPESCRIPT_EXTRACTOR_VERSION: string;
|
|
7
|
+
export declare const ALL = "*";
|
|
8
|
+
export type RequestedNames = ReadonlySet<string> | typeof ALL;
|
|
9
|
+
export declare const STUB_CLOSURE_MAX = 500;
|
|
10
|
+
/**
|
|
11
|
+
* Repository files one extraction may parse. The compiler runs on the
|
|
12
|
+
* event loop; a program the size of a monorepo would hold it past the
|
|
13
|
+
* heartbeat and lose the lease, so the slice stops with a blocker instead.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PROGRAM_FILES_MAX = 2000;
|
|
16
|
+
/** Modules Node provides; never a package the sandbox must install. */
|
|
17
|
+
export declare const NODE_BUILTINS: readonly string[];
|
|
18
|
+
export declare function isScriptPath(path: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Files a script imports as data (`import data from "./data.json"`) or
|
|
21
|
+
* reads by path (a query, a schema, a template). They ride along in a
|
|
22
|
+
* TypeScript slice; code in any other language does not.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isDataPath(path: string): boolean;
|
|
25
|
+
export declare function isNodeBuiltin(specifier: string): boolean;
|
|
26
|
+
/** The stub's artifact path for a module: `stubs/<path>.d.ts`. */
|
|
27
|
+
export declare function stubPathFor(module: string): string;
|
|
28
|
+
export interface ImportFact {
|
|
29
|
+
readonly specifier: string;
|
|
30
|
+
readonly names: RequestedNames;
|
|
31
|
+
readonly line: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The names read off a namespace binding (`ns.a`, `ns.T` in a type), or
|
|
35
|
+
* `ALL` once it escapes: passed or returned as a value, re-exported, read
|
|
36
|
+
* with a computed key, taken as `typeof ns`, or shadowed by another
|
|
37
|
+
* binding of the same name. Only a namespace that never escapes can be
|
|
38
|
+
* stubbed with less than the whole module.
|
|
39
|
+
*/
|
|
40
|
+
export declare function namespaceUses(nodes: readonly ts.Node[], name: string, binding: ts.Node): RequestedNames;
|
|
41
|
+
/** Every import a file makes, with the names it takes, from the syntax. */
|
|
42
|
+
export declare function importsOf(source: ts.SourceFile): ImportFact[];
|
|
43
|
+
/** Where a snapshot's bare specifiers resolve, having no node_modules. */
|
|
44
|
+
export interface ModuleFallbacks {
|
|
45
|
+
/** The repository path of one of its own packages; see `workspaceModuleResolver`. */
|
|
46
|
+
readonly workspaceModule?: (specifier: string) => string | null;
|
|
47
|
+
/** A directory whose node_modules holds fetched package types. */
|
|
48
|
+
readonly packageTypes?: string | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolves imports as the compiler does, then a bare specifier it cannot
|
|
52
|
+
* resolve as one of the repository's own packages, then from the fetched
|
|
53
|
+
* package types, as if imported beside their node_modules.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveModulesWith(host: ts.CompilerHost, root: string, options: ts.CompilerOptions, fallbacks: ModuleFallbacks): void;
|
|
56
|
+
/**
|
|
57
|
+
* A declaration file with the module specifiers `to` maps replaced, in
|
|
58
|
+
* imports, re-exports and `import("...")` types; the rest is left as is.
|
|
59
|
+
*/
|
|
60
|
+
export declare function rewriteModuleSpecifiers(text: string, to: (specifier: string) => string | null): string;
|
|
61
|
+
/**
|
|
62
|
+
* A compiler host that reads only the source tree, the compiler's libs
|
|
63
|
+
* and the fetched package types.
|
|
64
|
+
*/
|
|
65
|
+
export declare function boundedHost(root: string, options: ts.CompilerOptions, fallbacks?: ModuleFallbacks): ts.CompilerHost;
|
|
66
|
+
/** Options for declaration emit: the source's, with output settings forced. */
|
|
67
|
+
export declare function extractionOptions(base: ts.CompilerOptions): ts.CompilerOptions;
|
|
68
|
+
/**
|
|
69
|
+
* Options for a stub's declaration emit. A stub leaves the repository, so
|
|
70
|
+
* it carries no comments; the context builders, which stay private, keep
|
|
71
|
+
* them through `extractionOptions`. `@internal` declarations are emitted
|
|
72
|
+
* like any other, without their comments: the compiler's `stripInternal`
|
|
73
|
+
* would drop one the slice's code imports or a kept declaration names
|
|
74
|
+
* across the cut, and the stub would not declare what that code compiles
|
|
75
|
+
* against. `filterDeclaration` keeps only what is asked for and what that
|
|
76
|
+
* needs, so one nothing reaches is left out all the same.
|
|
77
|
+
*/
|
|
78
|
+
export declare function stubExtractionOptions(base: ts.CompilerOptions): ts.CompilerOptions;
|
|
79
|
+
/**
|
|
80
|
+
* An emitted declaration file as a stub may show it. The emitter writes a
|
|
81
|
+
* literal constant's value (`declare const KEY = "sk_live_..."`, `static
|
|
82
|
+
* readonly KEY = "..."`), the literal types an `as const` value or an
|
|
83
|
+
* annotation spells it with, every comment it was not told to drop and
|
|
84
|
+
* each private member's name. A stub keeps each value's type with its
|
|
85
|
+
* literals widened to `string`, `number`, `bigint` or `boolean`, no
|
|
86
|
+
* comments, and private members only as one nameless brand, since no code
|
|
87
|
+
* outside the class can use them.
|
|
88
|
+
*
|
|
89
|
+
* A constant the file reads as a type (`(typeof PROVIDERS)[number]`, `kind:
|
|
90
|
+
* typeof KIND`) keeps its name-like string literals (`"github"`, `"draft"`):
|
|
91
|
+
* they are that type's members, and widened, the type would be `string` and
|
|
92
|
+
* the slice would stop checking the code that relies on it (a guard taken
|
|
93
|
+
* out compiles). Its other literals (numbers, URLs, keys, sentences) widen
|
|
94
|
+
* as any value's do.
|
|
95
|
+
*
|
|
96
|
+
* A string enum member's value becomes its own name: it stays a string
|
|
97
|
+
* enum, each member its own type, without the text it stood for. Numeric
|
|
98
|
+
* members keep their values, which an ambient enum needs for its members
|
|
99
|
+
* to be constant, and a number names nothing.
|
|
100
|
+
*/
|
|
101
|
+
export declare function sanitizeDeclaration(text: string, options?: {
|
|
102
|
+
readonly widenLiterals?: boolean;
|
|
103
|
+
}): string;
|
|
104
|
+
export interface FilteredDeclaration {
|
|
105
|
+
readonly text: string;
|
|
106
|
+
/** Requested names that no kept statement exports and no `export *` could. */
|
|
107
|
+
readonly missing: readonly string[];
|
|
108
|
+
/** Names forwarded to `export *` targets because nothing local declared them. */
|
|
109
|
+
readonly forwarded: readonly {
|
|
110
|
+
specifier: string;
|
|
111
|
+
names: RequestedNames;
|
|
112
|
+
}[];
|
|
113
|
+
/** Modules the kept declarations import from, with the names taken. */
|
|
114
|
+
readonly imports: readonly {
|
|
115
|
+
specifier: string;
|
|
116
|
+
names: RequestedNames;
|
|
117
|
+
}[];
|
|
118
|
+
readonly symbols: readonly {
|
|
119
|
+
name: string;
|
|
120
|
+
kind: string;
|
|
121
|
+
declaration: string;
|
|
122
|
+
}[];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Narrows an emitted declaration file to the requested exports and what
|
|
126
|
+
* they need. `ALL` keeps the file whole.
|
|
127
|
+
*/
|
|
128
|
+
export declare function filterDeclaration(text: string, requested: RequestedNames): FilteredDeclaration;
|
|
129
|
+
export interface TypeScriptBoundary {
|
|
130
|
+
readonly extractorVersion: string;
|
|
131
|
+
readonly outbound: readonly BoundaryModule[];
|
|
132
|
+
readonly inbound: readonly BoundaryModule[];
|
|
133
|
+
/** Declaration files standing in for cut modules, by artifact path. */
|
|
134
|
+
readonly stubs: readonly {
|
|
135
|
+
path: string;
|
|
136
|
+
module: string;
|
|
137
|
+
text: string;
|
|
138
|
+
}[];
|
|
139
|
+
readonly blockers: readonly SliceBlocker[];
|
|
140
|
+
/** Configuration files the included files are read with, sorted. */
|
|
141
|
+
readonly configs: readonly string[];
|
|
142
|
+
/** Package specifiers imported by included files or kept stubs, with the files. */
|
|
143
|
+
readonly packages: ReadonlyMap<string, ReadonlySet<string>>;
|
|
144
|
+
/** The options the fixture compiles with. */
|
|
145
|
+
readonly compilerOptions: ts.CompilerOptions;
|
|
146
|
+
/**
|
|
147
|
+
* Imports of the repository's own packages, which resolve to their
|
|
148
|
+
* source: the cuts and internal imports they add, with specifiers, and
|
|
149
|
+
* each specifier's module.
|
|
150
|
+
*/
|
|
151
|
+
readonly workspace: {
|
|
152
|
+
readonly outbound: readonly CutEdge[];
|
|
153
|
+
readonly internal: readonly CutEdge[];
|
|
154
|
+
readonly modules: ReadonlyMap<string, string>;
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* The public surface's declarations and the check `npm test` holds them
|
|
158
|
+
* to (`carveout/surface/`); empty when there is nothing to check.
|
|
159
|
+
*/
|
|
160
|
+
readonly surface: readonly SurfaceFile[];
|
|
161
|
+
}
|
|
162
|
+
export interface TypeScriptBoundaryInput {
|
|
163
|
+
readonly root: string;
|
|
164
|
+
readonly included: readonly string[];
|
|
165
|
+
readonly outbound: readonly CutEdge[];
|
|
166
|
+
readonly inbound: readonly CutEdge[];
|
|
167
|
+
readonly texts: ReadonlyMap<string, string>;
|
|
168
|
+
/** Defaults to `PROGRAM_FILES_MAX`. */
|
|
169
|
+
readonly maxProgramFiles?: number;
|
|
170
|
+
/** The source of a repository package's specifier; see `workspaceModuleResolver`. */
|
|
171
|
+
readonly workspaceModule?: (specifier: string) => string | null;
|
|
172
|
+
/** A directory whose node_modules holds fetched package types. */
|
|
173
|
+
readonly packageTypes?: string | null;
|
|
174
|
+
/**
|
|
175
|
+
* Whether stubs widen literal values to their types, so no constant's
|
|
176
|
+
* value leaves the repository. Defaults to true.
|
|
177
|
+
*/
|
|
178
|
+
readonly widenLiterals?: boolean;
|
|
179
|
+
}
|
|
180
|
+
export declare function extractTypeScriptBoundary(input: TypeScriptBoundaryInput): TypeScriptBoundary;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `carveout gate`: the hand-off policy on a change made in the repository
|
|
3
|
+
* itself, by an agent or anyone else, rather than on a folder that came
|
|
4
|
+
* back from outside.
|
|
5
|
+
*
|
|
6
|
+
* It reads the change from Git (the base commit against the working tree,
|
|
7
|
+
* untracked files included) and names what a change to code should not
|
|
8
|
+
* slip in unread: protected paths (CI, hooks, manifests, lockfiles,
|
|
9
|
+
* environment), deleted tests, tests whose assertions were removed or
|
|
10
|
+
* rewritten or that were skipped or focused with `.only`, edits to the
|
|
11
|
+
* configuration a check reads (tsconfig, linter, test runner), and new
|
|
12
|
+
* check suppressions (`@ts-ignore`, `eslint-disable`, coverage ignores).
|
|
13
|
+
* With `scope`, a file outside the paths the change was meant to touch is
|
|
14
|
+
* named too. A refusal is a question for a reviewer, not a verdict: a
|
|
15
|
+
* change asked to alter a behaviour rewrites the assertion that held it.
|
|
16
|
+
*
|
|
17
|
+
* Reads only: it runs no code of the repository.
|
|
18
|
+
*/
|
|
19
|
+
import { type Refusal } from "./policy.js";
|
|
20
|
+
export type ChangeKind = "added" | "changed" | "deleted";
|
|
21
|
+
export interface RepositoryChange {
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly change: ChangeKind;
|
|
24
|
+
}
|
|
25
|
+
export interface Gate {
|
|
26
|
+
/** The checkout's root; every path is relative to it. */
|
|
27
|
+
readonly repo: string;
|
|
28
|
+
/** The base as given, and the commit it names. */
|
|
29
|
+
readonly base: string;
|
|
30
|
+
readonly baseCommit: string;
|
|
31
|
+
readonly changes: readonly RepositoryChange[];
|
|
32
|
+
readonly refused: readonly Refusal[];
|
|
33
|
+
}
|
|
34
|
+
/** The root of the Git checkout holding `dir`. */
|
|
35
|
+
export declare function checkoutRoot(dir: string): string;
|
|
36
|
+
/** The commit `base` names in `root`, or an error that says so. */
|
|
37
|
+
export declare function baseCommitOf(root: string, base: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* What changed from `baseCommit` to the working tree: tracked files by
|
|
40
|
+
* `git diff` (a rename is a deletion and an addition), and untracked files
|
|
41
|
+
* Git does not ignore as added. Sorted by path.
|
|
42
|
+
*/
|
|
43
|
+
export declare function changesSince(root: string, baseCommit: string): RepositoryChange[];
|
|
44
|
+
/** A file's text at `commit`, or null when it has none there. */
|
|
45
|
+
export declare function textAt(root: string, commit: string, path: string): string | null;
|
|
46
|
+
/** A file's text in the working tree, or null when it is not a file. */
|
|
47
|
+
export declare function textNow(root: string, path: string): string | null;
|
|
48
|
+
/** Judges the change from `base` (default `HEAD`) to the working tree. */
|
|
49
|
+
export declare function gateRepository(options: {
|
|
50
|
+
readonly repo: string;
|
|
51
|
+
readonly base?: string;
|
|
52
|
+
readonly allow?: readonly string[];
|
|
53
|
+
/** Paths or globs the change was meant to stay within. */
|
|
54
|
+
readonly scope?: readonly string[];
|
|
55
|
+
}): Gate;
|
package/dist/src/gate.js
CHANGED
|
@@ -98,6 +98,7 @@ export function gateRepository(options) {
|
|
|
98
98
|
const changes = changesSince(root, baseCommit);
|
|
99
99
|
const judge = handoffPolicy({
|
|
100
100
|
checks: true,
|
|
101
|
+
integrity: true,
|
|
101
102
|
testNoun: "a test",
|
|
102
103
|
...(options.allow === undefined ? {} : { allow: options.allow }),
|
|
103
104
|
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The import graph of a checkout, as the slice walk reads it.
|
|
3
|
+
*
|
|
4
|
+
* Every script file the checkout tracks becomes a `file:` node, with the
|
|
5
|
+
* names it exports itself. Each import, re-export and module reference the
|
|
6
|
+
* compiler resolves to a file in the checkout is a link to that file, with
|
|
7
|
+
* the names it takes and whether only types; each literal path a file reads
|
|
8
|
+
* at runtime is a `reads` link. Every other import (a package, a path that
|
|
9
|
+
* resolves to nothing, a dynamic import of a computed name) is a link to a
|
|
10
|
+
* `dependency:` node, which the walk reports rather than follows. Modules resolve with
|
|
11
|
+
* the nearest `tsconfig.json` (Node's resolution when there is none), and
|
|
12
|
+
* never through `node_modules`, so a workspace package is a bare specifier
|
|
13
|
+
* here, as it is in a snapshot without its installs. With
|
|
14
|
+
* `followWorkspaces`, an import of one of the repository's own packages is
|
|
15
|
+
* resolved to its source through the package's manifest and becomes a link
|
|
16
|
+
* to that file, so the walk copies or cuts it as it does any module.
|
|
17
|
+
*
|
|
18
|
+
* Nothing here runs repository code. The same files give the same graph.
|
|
19
|
+
*/
|
|
20
|
+
import ts from "typescript";
|
|
21
|
+
import { type ReachOptions, type ReachResult, type SliceBudget, type SliceGraph, type SliceGraphLink, type SliceRelation } from "./core.js";
|
|
22
|
+
/** Directories never read, whatever Git tracks. */
|
|
23
|
+
export declare const IGNORED_DIRECTORIES: readonly string[];
|
|
24
|
+
/** Files a graph reads before it stops; past it the checkout is too large. */
|
|
25
|
+
export declare const GRAPH_FILES_MAX = 50000;
|
|
26
|
+
/**
|
|
27
|
+
* The checkout's files, `/`-separated and sorted: what Git tracks plus
|
|
28
|
+
* untracked files it does not ignore, or every file outside Git.
|
|
29
|
+
*/
|
|
30
|
+
export declare function listFiles(root: string): string[];
|
|
31
|
+
/**
|
|
32
|
+
* The checkout's files a Vite glob's patterns match, as the bundler reads
|
|
33
|
+
* them: relative to the importing file's folder, or to the root for a
|
|
34
|
+
* pattern starting with `/`; a `!` pattern excludes. Never node_modules.
|
|
35
|
+
*/
|
|
36
|
+
export declare function globMatches(root: string, directory: string, patterns: readonly string[]): string[];
|
|
37
|
+
export interface BuiltGraph {
|
|
38
|
+
readonly graph: SliceGraph;
|
|
39
|
+
/** Script files read, sorted. */
|
|
40
|
+
readonly files: readonly string[];
|
|
41
|
+
/** Files whose `tsconfig.json` could not be read. */
|
|
42
|
+
readonly invalidConfigs: readonly string[];
|
|
43
|
+
/** Where the time went, in milliseconds: resolving is the compiler's
|
|
44
|
+
* configuration and module resolution, parsing the rest of the walk. */
|
|
45
|
+
readonly timings: {
|
|
46
|
+
readonly listMs: number;
|
|
47
|
+
readonly parseMs: number;
|
|
48
|
+
readonly resolveMs: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** How the graph reads imports of the repository's own packages. */
|
|
52
|
+
export interface GraphOptions {
|
|
53
|
+
/** Resolve a workspace package's import to its source file. */
|
|
54
|
+
readonly followWorkspaces?: boolean;
|
|
55
|
+
/** Files read as if the checkout had none (the owner's acceptance tests). */
|
|
56
|
+
readonly unseen?: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
/** What `walkGraph` takes: the walk's options and the graph's. */
|
|
59
|
+
export type WalkOptions = ReachOptions & GraphOptions;
|
|
60
|
+
/** The graph of every script file of the checkout. */
|
|
61
|
+
export declare function buildGraph(root: string, listed?: readonly string[], options?: GraphOptions): BuiltGraph;
|
|
62
|
+
/**
|
|
63
|
+
* The graph a slice walk needs, read lazily: the entry points, every file
|
|
64
|
+
* the walk reaches, what those files name and what that re-exports (so a
|
|
65
|
+
* barrel and an import of types are judged from their targets), and every
|
|
66
|
+
* file that may import the slice. The walk on this graph gives what it
|
|
67
|
+
* gives on the whole checkout's graph, without parsing the rest of a large
|
|
68
|
+
* monorepo.
|
|
69
|
+
*
|
|
70
|
+
* Importers are found by text first and confirmed by the compiler: a file
|
|
71
|
+
* is read when it mentions an included file's name (its folder's, for an
|
|
72
|
+
* `index`), an alias with no wildcard that maps to it, `import.meta.glob`,
|
|
73
|
+
* or an import of folders alone (`.`, `..`, `../..`). A relative or wildcard-alias import spells
|
|
74
|
+
* the file's name or its folder's, and a workspace package's name is a
|
|
75
|
+
* package here, not a link, so no importer is missed for being unread.
|
|
76
|
+
*/
|
|
77
|
+
export declare function walkGraph(root: string, entryPoints: readonly string[], budget: SliceBudget, options?: WalkOptions): {
|
|
78
|
+
built: BuiltGraph;
|
|
79
|
+
walk: ReachResult;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The walk with the files that import the slice by a workspace package's
|
|
83
|
+
* name among its inbound cuts. Unfollowed, such an import is a package
|
|
84
|
+
* here, not a link, and the walk never sees it; but the package's other
|
|
85
|
+
* workspaces compile against the module its name resolves to, so a change
|
|
86
|
+
* to that module's exports breaks them. The public surface, the interface
|
|
87
|
+
* test and `status` read the inbound cuts, and must count them. The walk
|
|
88
|
+
* itself is unchanged: nothing is copied or cut for it.
|
|
89
|
+
*/
|
|
90
|
+
export declare function workspaceImportersOf(root: string): (walk: ReachResult, graph: SliceGraph) => ReachResult;
|
|
91
|
+
/** A confirmed link from an importer to a file `importersOf` searched for. */
|
|
92
|
+
export interface ImporterLink {
|
|
93
|
+
readonly from: string;
|
|
94
|
+
readonly to: string;
|
|
95
|
+
readonly specifier: string | null;
|
|
96
|
+
readonly relation: SliceRelation;
|
|
97
|
+
readonly typeOnly: boolean;
|
|
98
|
+
readonly forwards: SliceGraphLink["forwards"];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Every file that imports `targets`, directly or through other importers,
|
|
102
|
+
* up to `maxDepth` hops (unbounded by default): the reverse of the slice
|
|
103
|
+
* walk, read as lazily. Each hop finds candidates by text (a target's name,
|
|
104
|
+
* its folder's for an `index`, an exact alias, a workspace package's name,
|
|
105
|
+
* a folder-only import, `import.meta.glob`) and keeps those whose import
|
|
106
|
+
* the compiler resolves to a target. A target that no longer exists (a
|
|
107
|
+
* deleted module) is matched by resolving imports as if it still were.
|
|
108
|
+
* `references` (`vi.mock`) count: such a test runs against the module.
|
|
109
|
+
*
|
|
110
|
+
* It follows names, as the slice walk does: a barrel carries the change
|
|
111
|
+
* only through the names it forwards from a changed module, so a file that
|
|
112
|
+
* imports other names from the barrel is not reached. A file whose own code
|
|
113
|
+
* takes a name that carries the change is reached, and carries it on only
|
|
114
|
+
* through its exports whose declarations reach that name, by way of its
|
|
115
|
+
* other top-level declarations. Nothing here runs repository code.
|
|
116
|
+
*/
|
|
117
|
+
export declare function importersOf(root: string, targets: readonly string[], options?: {
|
|
118
|
+
readonly maxDepth?: number;
|
|
119
|
+
/** The names of each target that changed (`changedExports`); all by default. */
|
|
120
|
+
readonly seeds?: ReadonlyMap<string, ReadonlySet<string> | "*">;
|
|
121
|
+
}): {
|
|
122
|
+
built: BuiltGraph;
|
|
123
|
+
/** Hops from the nearest target; the targets themselves at 0. */
|
|
124
|
+
depthOf: ReadonlyMap<string, number>;
|
|
125
|
+
links: readonly ImporterLink[];
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* The exports of a module whose declarations reach one of `locals`, through
|
|
129
|
+
* its other top-level declarations. Read by the identifiers each statement
|
|
130
|
+
* names, so a shadowing name counts too: it errs toward more.
|
|
131
|
+
*/
|
|
132
|
+
export declare function exportsReachingIn(source: ts.SourceFile, locals: ReadonlySet<string>): Set<string> | "*";
|
|
133
|
+
/**
|
|
134
|
+
* The exports of a module that a change from `before` to `after` reaches:
|
|
135
|
+
* those whose own declaration changed, those that reach a changed
|
|
136
|
+
* declaration or import through the module's other declarations, and
|
|
137
|
+
* those added, removed or re-pointed by an export list or a re-export. A
|
|
138
|
+
* changed statement that declares nothing (code run at load, `export *`)
|
|
139
|
+
* reaches everything. Null `before` or `after`: an added or deleted module,
|
|
140
|
+
* all of it.
|
|
141
|
+
*/
|
|
142
|
+
export declare function changedExports(path: string, before: string | null, after: string | null): Set<string> | "*";
|
package/dist/src/graph.js
CHANGED
|
@@ -140,8 +140,9 @@ export function buildGraph(root, listed, options = {}) {
|
|
|
140
140
|
*/
|
|
141
141
|
export function walkGraph(root, entryPoints, budget, options = {}) {
|
|
142
142
|
const started = performance.now();
|
|
143
|
-
const
|
|
144
|
-
const
|
|
143
|
+
const { followWorkspaces, unseen, ...rest } = options;
|
|
144
|
+
const hidden = new Set(unseen ?? []);
|
|
145
|
+
const files = listFiles(root).filter((file) => !hidden.has(file));
|
|
145
146
|
const graphOptions = followWorkspaces === undefined ? {} : { followWorkspaces };
|
|
146
147
|
const builder = graphBuilder(root, files, performance.now() - started, graphOptions);
|
|
147
148
|
const absoluteRoot = resolve(root);
|
|
@@ -150,7 +151,7 @@ export function walkGraph(root, entryPoints, budget, options = {}) {
|
|
|
150
151
|
packageOf: rest.packageOf ?? packageFolder(absoluteRoot),
|
|
151
152
|
};
|
|
152
153
|
const scripts = files.filter((file) => SCRIPT.test(file));
|
|
153
|
-
const prefixes = entryPoints.map((entry) => entry.replaceAll("\\", "/").replace(/^\.\//, "").replace(/^\/+|\/+$/g, ""));
|
|
154
|
+
const prefixes = [...entryPoints, ...(options.roots ?? [])].map((entry) => entry.replaceAll("\\", "/").replace(/^\.\//, "").replace(/^\/+|\/+$/g, ""));
|
|
154
155
|
builder.add(scripts.filter((file) => prefixes.some((prefix) => file === prefix || file.startsWith(`${prefix}/`))));
|
|
155
156
|
const aliases = exactAliases(absoluteRoot, files);
|
|
156
157
|
// A workspace package is imported by its name, which spells neither the
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `carveout impact`: what a change made in the repository reaches, for the
|
|
3
|
+
* agent or person who made it, before the repository's CI says so.
|
|
4
|
+
*
|
|
5
|
+
* From the change (Git's base commit against the working tree, or the
|
|
6
|
+
* files named) it finds:
|
|
7
|
+
*
|
|
8
|
+
* - every file that imports a changed module, directly or through others,
|
|
9
|
+
* by the same lazy, compiler-confirmed search the slice walk uses;
|
|
10
|
+
* - the exports of each changed module whose type changed or that are
|
|
11
|
+
* gone, read by the TypeScript checker at the base and now, and which of
|
|
12
|
+
* those importers take each one (through barrels too);
|
|
13
|
+
* - the tests among the importers, with their runner, and the commands
|
|
14
|
+
* that run just them, package by package.
|
|
15
|
+
*
|
|
16
|
+
* The search, the type comparison and the test selection run no code of
|
|
17
|
+
* the repository; `runImpactTests` runs the commands, in the repository,
|
|
18
|
+
* with the caller's environment, as its own test script would.
|
|
19
|
+
*
|
|
20
|
+
* A type is compared as the checker prints it, one level of members deep:
|
|
21
|
+
* a change inside a named type another module declares shows on that
|
|
22
|
+
* module's own exports when it changed too, not on this one.
|
|
23
|
+
*/
|
|
24
|
+
import { type ChangeKind } from "./gate.js";
|
|
25
|
+
export type ImpactRunner = "node" | "vitest" | "jest";
|
|
26
|
+
export interface ImpactExport {
|
|
27
|
+
readonly file: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly change: "removed" | "changed";
|
|
30
|
+
/** The type as the checker prints it, one level of members deep. */
|
|
31
|
+
readonly before: string;
|
|
32
|
+
readonly after: string | null;
|
|
33
|
+
/** Files outside the change that take this export, sorted. */
|
|
34
|
+
readonly usedBy: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
export interface ImpactTest {
|
|
37
|
+
readonly file: string;
|
|
38
|
+
/** Hops from the nearest changed module; 0 for a test the change edits. */
|
|
39
|
+
readonly depth: number;
|
|
40
|
+
readonly package: string;
|
|
41
|
+
/** Null: no runner carveout knows (mocha, ava, a custom script). */
|
|
42
|
+
readonly runner: ImpactRunner | null;
|
|
43
|
+
}
|
|
44
|
+
export interface ImpactCommand {
|
|
45
|
+
/** The package folder to run it in, from the checkout's root ("" the root). */
|
|
46
|
+
readonly cwd: string;
|
|
47
|
+
readonly runner: ImpactRunner | null;
|
|
48
|
+
readonly argv: readonly string[];
|
|
49
|
+
readonly tests: readonly string[];
|
|
50
|
+
/** Runs the package's whole test script, not only these tests. */
|
|
51
|
+
readonly whole: boolean;
|
|
52
|
+
}
|
|
53
|
+
export interface Impact {
|
|
54
|
+
readonly repo: string;
|
|
55
|
+
/** The base as given and its commit; null when files were named. */
|
|
56
|
+
readonly base: string | null;
|
|
57
|
+
readonly baseCommit: string | null;
|
|
58
|
+
/** The script files the change touches, with how. */
|
|
59
|
+
readonly changed: readonly {
|
|
60
|
+
readonly path: string;
|
|
61
|
+
readonly change: ChangeKind;
|
|
62
|
+
}[];
|
|
63
|
+
/** Files that import a changed module, with their hops from it, sorted. */
|
|
64
|
+
readonly importers: readonly {
|
|
65
|
+
readonly file: string;
|
|
66
|
+
readonly depth: number;
|
|
67
|
+
}[];
|
|
68
|
+
readonly exports: readonly ImpactExport[];
|
|
69
|
+
readonly tests: readonly ImpactTest[];
|
|
70
|
+
readonly commands: readonly ImpactCommand[];
|
|
71
|
+
/** Changed modules whose types could not be compared, with why. */
|
|
72
|
+
readonly unchecked: readonly {
|
|
73
|
+
readonly file: string;
|
|
74
|
+
readonly reason: string;
|
|
75
|
+
}[];
|
|
76
|
+
readonly timings: {
|
|
77
|
+
readonly graphMs: number;
|
|
78
|
+
readonly typesMs: number;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A test file's runner: the one it imports (`node:test`, `vitest`,
|
|
83
|
+
* `@jest/globals`), else the one its package (or the root) depends on
|
|
84
|
+
* when the file calls a runner's globals.
|
|
85
|
+
*/
|
|
86
|
+
export declare function impactRunnerResolver(root: string, packageOf: (file: string) => string): (file: string, specifiers: readonly string[]) => ImpactRunner | null;
|
|
87
|
+
/** The commands that run `tests`, one per package and runner. */
|
|
88
|
+
export declare function impactCommands(tests: readonly ImpactTest[]): ImpactCommand[];
|
|
89
|
+
/** What the change from `base` (or the files named) reaches. */
|
|
90
|
+
export declare function impactOf(options: {
|
|
91
|
+
readonly repo: string;
|
|
92
|
+
readonly base?: string;
|
|
93
|
+
/** Files to start from instead of the change since `base`. */
|
|
94
|
+
readonly files?: readonly string[];
|
|
95
|
+
/** Hops of importers to follow; unbounded by default. */
|
|
96
|
+
readonly maxDepth?: number;
|
|
97
|
+
}): Impact;
|
|
98
|
+
export interface ImpactRun {
|
|
99
|
+
readonly command: ImpactCommand;
|
|
100
|
+
readonly ok: boolean;
|
|
101
|
+
readonly exitCode: number | null;
|
|
102
|
+
/** The last lines of what it printed. */
|
|
103
|
+
readonly output: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Runs each command at its package's folder, one at a time, with the
|
|
107
|
+
* caller's environment: these are the repository's own tests, run where
|
|
108
|
+
* its test script would run them. `timeoutMs` bounds each.
|
|
109
|
+
*/
|
|
110
|
+
export declare function runImpactTests(root: string, commands: readonly ImpactCommand[], options?: {
|
|
111
|
+
readonly timeoutMs?: number;
|
|
112
|
+
readonly log?: (line: string) => void;
|
|
113
|
+
}): ImpactRun[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imports as the repository writes them. The cut rewrites a slice's
|
|
3
|
+
* imports to paths Node resolves inside the folder, and records each
|
|
4
|
+
* rewrite; `patch` turns them back. An import the folder's own edits added
|
|
5
|
+
* has no record, so it is restored by what it resolves to: the specifier
|
|
6
|
+
* the slice already uses for that target, else one the target's other
|
|
7
|
+
* importers use, else a relative path in the package's own style.
|
|
8
|
+
*/
|
|
9
|
+
import { type ImportRewrite } from "./core.js";
|
|
10
|
+
/**
|
|
11
|
+
* `text` with the folder's import rewrites put back as the source wrote
|
|
12
|
+
* them. Two specifiers the cut rewrote to one (`@/lib/format` and
|
|
13
|
+
* `../../lib/format` both to `../../lib/format.js`) are told apart by
|
|
14
|
+
* `original`, the file as the cut read it: they come back in the order it
|
|
15
|
+
* wrote them. Without it, the last recorded wins. `path` tells the parser
|
|
16
|
+
* what kind of file the text is (`.tsx` reads JSX); a `.ts` without it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function undoRewrites(text: string, rewrites: readonly ImportRewrite[], original?: string | null, path?: string): string;
|
|
19
|
+
/** Every specifier a source writes, in source order. */
|
|
20
|
+
export declare function specifiersOf(text: string, path?: string): Set<string>;
|
|
21
|
+
/** How a package spells a relative import's extension. */
|
|
22
|
+
export type ExtensionStyle = "none" | "js" | "ts";
|
|
23
|
+
/** What a cut knows about the slice's imports, for restoring new ones. */
|
|
24
|
+
export interface ImportContext {
|
|
25
|
+
/** Import edges the cut resolved, by importer, target and specifier. */
|
|
26
|
+
readonly edges: readonly {
|
|
27
|
+
from: string;
|
|
28
|
+
to: string;
|
|
29
|
+
specifier: string;
|
|
30
|
+
}[];
|
|
31
|
+
/** The folder's source files, by repository path: slice and new files. */
|
|
32
|
+
readonly files: ReadonlySet<string>;
|
|
33
|
+
/** Cut modules by repository path; their stubs sit beside them. */
|
|
34
|
+
readonly stubs: ReadonlySet<string>;
|
|
35
|
+
/** The repository's package folder holding a path, "" for the root. */
|
|
36
|
+
readonly packageOf: (path: string) => string;
|
|
37
|
+
/**
|
|
38
|
+
* How a package writes the extension of a relative import: none, the
|
|
39
|
+
* emitted `.js`, or the source's own `.ts` (Node's type stripping,
|
|
40
|
+
* `allowImportingTsExtensions`).
|
|
41
|
+
*/
|
|
42
|
+
readonly extensionStyle: (pkg: string) => ExtensionStyle;
|
|
43
|
+
}
|
|
44
|
+
/** The folder file or cut module a relative specifier in `file` names. */
|
|
45
|
+
export declare function importTarget(file: string, specifier: string, context: Pick<ImportContext, "files" | "stubs">): string | null;
|
|
46
|
+
export interface RestoredImports {
|
|
47
|
+
readonly text: string;
|
|
48
|
+
/** Specifiers restored relative across packages: no spelling was known. */
|
|
49
|
+
readonly crossPackage: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A slice file's text with its imports as the repository writes them.
|
|
53
|
+
* `original` is the file as the cut read it (null for a new file): the
|
|
54
|
+
* specifiers it wrote are kept as they are.
|
|
55
|
+
*/
|
|
56
|
+
export declare function restoreImports(file: string, text: string, original: string | null, rewrites: readonly ImportRewrite[], context: ImportContext): RestoredImports;
|
|
57
|
+
/** The nearest folder above `path` with a package.json in `repo`, "" for none. */
|
|
58
|
+
export declare function packageFolder(repo: string, cache?: Map<string, string>): (path: string) => string;
|
|
59
|
+
/**
|
|
60
|
+
* How each package writes relative imports, by the imports the cut read
|
|
61
|
+
* there: without an extension, with the emitted `.js`, or with the
|
|
62
|
+
* source's `.ts`. The most common style wins; `.js` on a tie or none.
|
|
63
|
+
*/
|
|
64
|
+
export declare function extensionStyles(edges: readonly {
|
|
65
|
+
from: string;
|
|
66
|
+
specifier: string;
|
|
67
|
+
}[], packageOf: (path: string) => string): (pkg: string) => ExtensionStyle;
|