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,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running code that came back from outside in a container.
|
|
3
|
+
*
|
|
4
|
+
* A folder that came back holds the contributor's code, and its tests run
|
|
5
|
+
* it. On the maintainer's machine that code would run as the maintainer,
|
|
6
|
+
* with their files (the closed repository, the receipts that map the
|
|
7
|
+
* folder's pseudonyms back to the repository's names, credentials) and
|
|
8
|
+
* their network. Here each step is its own `docker run --rm` that mounts one
|
|
9
|
+
* directory, the scratch copy, and nothing else: no HOME, no receipts, no
|
|
10
|
+
* checkout beyond the copy. Only the install step has a network, and it runs
|
|
11
|
+
* a package manager with lifecycle scripts off on the receipt's manifests,
|
|
12
|
+
* so none of the folder's code runs while the network is there. Every other
|
|
13
|
+
* step (the build, the tests, the owner's acceptance tests, a merge's
|
|
14
|
+
* verify command) runs with `--network none`, all capabilities dropped, a
|
|
15
|
+
* read-only root, and bounds on memory and processes.
|
|
16
|
+
*
|
|
17
|
+
* Docker by default; `CARVEOUT_CONTAINER` names another runtime that takes
|
|
18
|
+
* the same arguments (podman), and `CARVEOUT_SANDBOX_IMAGE` the image (a
|
|
19
|
+
* Node image at this process's major version unless given).
|
|
20
|
+
*/
|
|
21
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
22
|
+
import { randomBytes } from "node:crypto";
|
|
23
|
+
import { relative, resolve, sep } from "node:path";
|
|
24
|
+
/** The runtime and image from the environment, or Docker and a Node image at this process's major version. */
|
|
25
|
+
export function defaultSandbox(image) {
|
|
26
|
+
return {
|
|
27
|
+
runtime: process.env["CARVEOUT_CONTAINER"] ?? "docker",
|
|
28
|
+
image: image ?? process.env["CARVEOUT_SANDBOX_IMAGE"] ?? `node:${process.versions.node.split(".")[0] ?? "22"}`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Why the sandbox cannot run, or null when it can: the runtime answers, its
|
|
33
|
+
* daemon runs Linux containers, and the image is there or can be pulled.
|
|
34
|
+
*/
|
|
35
|
+
export function sandboxUnavailable(spec) {
|
|
36
|
+
try {
|
|
37
|
+
const os = execFileSync(spec.runtime, ["info", "--format", "{{.OSType}}"], {
|
|
38
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
39
|
+
timeout: 20_000,
|
|
40
|
+
})
|
|
41
|
+
.toString("utf8")
|
|
42
|
+
.trim();
|
|
43
|
+
if (os !== "" && os !== "linux")
|
|
44
|
+
return `${spec.runtime} runs ${os} containers; the sandbox needs Linux ones.`;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return `${spec.runtime} does not answer (${firstLine(error)}): start it, or set CARVEOUT_CONTAINER to a runtime that takes Docker's arguments.`;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
execFileSync(spec.runtime, ["image", "inspect", spec.image], { stdio: "ignore", timeout: 20_000 });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
try {
|
|
54
|
+
execFileSync(spec.runtime, ["pull", "--quiet", spec.image], { stdio: "ignore", timeout: 10 * 60_000 });
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return `${spec.runtime} has no image ${spec.image} and could not pull it (${firstLine(error)}): pull it, or name another with --sandbox-image or CARVEOUT_SANDBOX_IMAGE.`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const firstLine = (error) => (error instanceof Error ? (error.stderr?.toString("utf8") || error.message) : String(error)).trim().split("\n")[0] ?? "";
|
|
63
|
+
/** Where a directory under the mounted one is inside the container. */
|
|
64
|
+
export const MOUNT = "/work";
|
|
65
|
+
/**
|
|
66
|
+
* Runs one command in a fresh container that mounts `root` at `/work` and
|
|
67
|
+
* nothing else, in `cwd` (a directory under `root`). Host paths under
|
|
68
|
+
* `root` in the arguments are rewritten to the container's, and this Node's
|
|
69
|
+
* own path to `node`. Only the names in `env` are passed, with HOME and the
|
|
70
|
+
* npm cache in the container's own /tmp. With `network` false the container
|
|
71
|
+
* has none. At `limitMs` the container is killed.
|
|
72
|
+
*/
|
|
73
|
+
export function sandboxRun(spec, root, command, options) {
|
|
74
|
+
const base = resolve(root);
|
|
75
|
+
const inside = (path) => {
|
|
76
|
+
const at = relative(base, resolve(path));
|
|
77
|
+
if (at === "")
|
|
78
|
+
return MOUNT;
|
|
79
|
+
if (at.startsWith("..") || at.startsWith(sep))
|
|
80
|
+
throw new Error(`${path} is outside the sandbox's folder ${base}.`);
|
|
81
|
+
return `${MOUNT}/${at.split(sep).join("/")}`;
|
|
82
|
+
};
|
|
83
|
+
const mapArgument = (value) => {
|
|
84
|
+
if (value === process.execPath)
|
|
85
|
+
return "node";
|
|
86
|
+
return value.split(base).join(MOUNT);
|
|
87
|
+
};
|
|
88
|
+
const [executable = "true", ...args] = command;
|
|
89
|
+
const name = `carveout-${randomBytes(6).toString("hex")}`;
|
|
90
|
+
const env = {};
|
|
91
|
+
for (const [key, value] of Object.entries(options.env ?? {}))
|
|
92
|
+
if (key !== "PATH" && key !== "HOME" && key !== "SystemRoot" && !key.startsWith("npm_config_"))
|
|
93
|
+
env[key] = value;
|
|
94
|
+
const runArgs = [
|
|
95
|
+
"run",
|
|
96
|
+
"--rm",
|
|
97
|
+
"--name",
|
|
98
|
+
name,
|
|
99
|
+
"--init",
|
|
100
|
+
...(options.network ? [] : ["--network", "none"]),
|
|
101
|
+
...(typeof process.getuid === "function" && typeof process.getgid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : []),
|
|
102
|
+
"--cap-drop",
|
|
103
|
+
"ALL",
|
|
104
|
+
"--security-opt",
|
|
105
|
+
"no-new-privileges",
|
|
106
|
+
"--read-only",
|
|
107
|
+
"--tmpfs",
|
|
108
|
+
"/tmp:rw,exec,size=2g",
|
|
109
|
+
"--memory",
|
|
110
|
+
process.env["CARVEOUT_SANDBOX_MEMORY"] ?? "4g",
|
|
111
|
+
"--pids-limit",
|
|
112
|
+
"2048",
|
|
113
|
+
"--volume",
|
|
114
|
+
`${base}:${MOUNT}`,
|
|
115
|
+
"--workdir",
|
|
116
|
+
inside(options.cwd ?? base),
|
|
117
|
+
"--env",
|
|
118
|
+
"HOME=/tmp",
|
|
119
|
+
"--env",
|
|
120
|
+
"npm_config_cache=/tmp/.npm",
|
|
121
|
+
"--env",
|
|
122
|
+
"npm_config_update_notifier=false",
|
|
123
|
+
"--env",
|
|
124
|
+
"CI=1",
|
|
125
|
+
...Object.entries(env).flatMap(([key, value]) => ["--env", `${key}=${mapArgument(value)}`]),
|
|
126
|
+
spec.image,
|
|
127
|
+
mapArgument(executable),
|
|
128
|
+
...args.map(mapArgument),
|
|
129
|
+
];
|
|
130
|
+
return new Promise((resolvePromise) => {
|
|
131
|
+
const child = spawn(spec.runtime, runArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
132
|
+
let stdout = "";
|
|
133
|
+
let stderr = "";
|
|
134
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
|
|
135
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
|
|
136
|
+
const timer = setTimeout(() => {
|
|
137
|
+
try {
|
|
138
|
+
execFileSync(spec.runtime, ["kill", name], { stdio: "ignore", timeout: 30_000 });
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Already gone.
|
|
142
|
+
}
|
|
143
|
+
child.kill("SIGKILL");
|
|
144
|
+
}, options.limitMs ?? 10 * 60_000);
|
|
145
|
+
child.on("error", (error) => {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
resolvePromise({ stdout, stderr: `${stderr}${error.message}`, status: null });
|
|
148
|
+
});
|
|
149
|
+
child.on("close", (code) => {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
resolvePromise({ stdout, stderr, status: code });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request a cut is made from, and the JSON form every record is
|
|
3
|
+
* written in, so two cuts of the same bytes give the same files.
|
|
4
|
+
*/
|
|
5
|
+
export declare const SLICE_TOOL_VERSION = "carveout-slice@2";
|
|
6
|
+
export interface SliceBudget {
|
|
7
|
+
/** Files inside the slice; the first file past it on any path is a cut. */
|
|
8
|
+
readonly maxFiles: number;
|
|
9
|
+
/** Edges from the nearest entry point; a file further away is a cut. */
|
|
10
|
+
readonly maxDepth: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const SLICE_BUDGET_DEFAULTS: SliceBudget;
|
|
13
|
+
export interface SliceParams {
|
|
14
|
+
/** Repository-relative file or directory paths, sorted and unique. */
|
|
15
|
+
readonly entryPoints: readonly string[];
|
|
16
|
+
readonly budget: SliceBudget;
|
|
17
|
+
/** Also bring along the tests outside the slice that import it. */
|
|
18
|
+
readonly tests: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The setup files the package's Vitest tests run before them (its
|
|
21
|
+
* config's `setupFiles`), walked from as the entry points are but no
|
|
22
|
+
* entry point: an edit to one is held as a test-runner setting's is.
|
|
23
|
+
*/
|
|
24
|
+
readonly setupFiles?: readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* The runner the package's own tests run on, when it is Vitest: the
|
|
27
|
+
* folder installs it, pinned as the package pins it, for the tests the
|
|
28
|
+
* contributor writes, whether or not a test of the slice came along.
|
|
29
|
+
*/
|
|
30
|
+
readonly testRunner?: "vitest";
|
|
31
|
+
/**
|
|
32
|
+
* Service packages kept real rather than mocked: installed and typed as
|
|
33
|
+
* the repository has them, with the environment their importers read
|
|
34
|
+
* left to the shell. Absent when none.
|
|
35
|
+
*/
|
|
36
|
+
readonly live?: readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* Include every file of the entry points' package the walk reaches, at
|
|
39
|
+
* any depth: the cut falls at package and service boundaries only, so a
|
|
40
|
+
* sibling module is copied rather than stubbed. Absent when off.
|
|
41
|
+
*/
|
|
42
|
+
readonly wholePackage?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Follow an import of one of the repository's own packages into its
|
|
45
|
+
* source, as an import of a file is followed, rather than stubbing the
|
|
46
|
+
* package at the seam. Absent when off.
|
|
47
|
+
*/
|
|
48
|
+
readonly followWorkspaces?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Copy pure code the slice runs (validators, error classes, formatters)
|
|
51
|
+
* rather than mock it, past the depth; see `ReachOptions.pure`. Absent
|
|
52
|
+
* when off.
|
|
53
|
+
*/
|
|
54
|
+
readonly copyPure?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Files never copied, as paths or globs from the repository's root:
|
|
57
|
+
* wherever the walk reaches one it is cut and stubbed, with its literal
|
|
58
|
+
* values widened. Absent when none.
|
|
59
|
+
*/
|
|
60
|
+
readonly private?: readonly string[];
|
|
61
|
+
/**
|
|
62
|
+
* Folders (or files) copied in whole beside the slice, as paths from the
|
|
63
|
+
* repository's root: what the code reads by a name the walk cannot
|
|
64
|
+
* follow, such as a migrations folder. A path read into one is no
|
|
65
|
+
* blocker. Absent when none.
|
|
66
|
+
*/
|
|
67
|
+
readonly copy?: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether a repository path matches one of `patterns`: the path itself,
|
|
71
|
+
* anything under it as a folder, or a glob (`*` within one segment, `**`
|
|
72
|
+
* across segments). Pure, so a slice's record can say what was kept out.
|
|
73
|
+
*/
|
|
74
|
+
export declare function pathMatcher(patterns: readonly string[]): (path: string) => boolean;
|
|
75
|
+
export declare function canonicalJson(value: unknown): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a slice reaches outside the repository: the service SDKs and HTTP
|
|
3
|
+
* clients its files import, and the environment variables they read. Listed
|
|
4
|
+
* by name so the sandbox phase knows what to mock; values are never read.
|
|
5
|
+
*
|
|
6
|
+
* A fixed table over import specifiers plus a regex pass over the text, so
|
|
7
|
+
* the result is the same for the same bytes. The regexes are deliberately
|
|
8
|
+
* simple: a false positive costs one spurious line in a list a person reads;
|
|
9
|
+
* a parser per language would cost a dependency per language.
|
|
10
|
+
*/
|
|
11
|
+
export interface ExternalServiceRule {
|
|
12
|
+
/** A neutral name for the service family: `aws`, `postgres`, `http`. */
|
|
13
|
+
readonly service: string;
|
|
14
|
+
/** Exact specifiers, or a scope prefix ending in `/*`. */
|
|
15
|
+
readonly specifiers: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
export declare const EXTERNAL_SERVICE_RULES: readonly ExternalServiceRule[];
|
|
18
|
+
export interface ExternalPackage {
|
|
19
|
+
readonly specifier: string;
|
|
20
|
+
readonly service: string;
|
|
21
|
+
/** Included files importing it, sorted. */
|
|
22
|
+
readonly files: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
export interface EnvironmentRead {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly files: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
export interface SliceExternals {
|
|
29
|
+
readonly packages: readonly ExternalPackage[];
|
|
30
|
+
readonly environment: readonly EnvironmentRead[];
|
|
31
|
+
}
|
|
32
|
+
export interface ExternalsInput {
|
|
33
|
+
readonly path: string;
|
|
34
|
+
readonly text: string;
|
|
35
|
+
}
|
|
36
|
+
/** The service a specifier belongs to, or null when the table has none. */
|
|
37
|
+
export declare function serviceOf(specifier: string): string | null;
|
|
38
|
+
export declare function detectExternals(files: readonly ExternalsInput[]): SliceExternals;
|
|
39
|
+
/** Node's modules pure code may import: no disk, network or process. */
|
|
40
|
+
export declare const PURE_NODE_MODULES: ReadonlySet<string>;
|
|
41
|
+
/**
|
|
42
|
+
* What makes a module pure for `ReachOptions.pure`: it imports no service
|
|
43
|
+
* package and none of Node's modules that reach the disk, the network or
|
|
44
|
+
* the process, and its own text reads no environment and calls no
|
|
45
|
+
* network. Read from the text, so a false "impure" costs only a mock, as
|
|
46
|
+
* before; `read` gives a file's text, or null when it cannot be read.
|
|
47
|
+
*/
|
|
48
|
+
export declare function purityRules(read: (file: string) => string | null, isBuiltin: (specifier: string) => boolean): {
|
|
49
|
+
readonly package: (specifier: string) => boolean;
|
|
50
|
+
readonly sideEffects: (file: string) => boolean;
|
|
51
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The import graph of a checkout, as the slice walk reads it.
|
|
3
|
+
*
|
|
4
|
+
* Node ids: `file:<path>` for a file of the checkout, and
|
|
5
|
+
* `dependency:<path>:<specifier>` for an import from `<path>` that resolves
|
|
6
|
+
* to no file of it (a package, a path that misses, a computed name). A
|
|
7
|
+
* node's `sourceFile` is the repository path it was read from.
|
|
8
|
+
*
|
|
9
|
+
* Each link is one import, re-export or reference as the source wrote it,
|
|
10
|
+
* with the names it takes, so the walk can follow a barrel only to the
|
|
11
|
+
* modules that supply those names, and pass over an import of types.
|
|
12
|
+
*/
|
|
13
|
+
export type SliceNodeKind = "file" | "dependency";
|
|
14
|
+
/** The names a file declares and exports itself, read from its syntax. */
|
|
15
|
+
export interface FileExports {
|
|
16
|
+
/** Names that exist at runtime, `default` among them. */
|
|
17
|
+
readonly values: readonly string[];
|
|
18
|
+
/** Interfaces and type aliases, which the compiler erases. */
|
|
19
|
+
readonly types: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
export interface SliceGraphNode {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly kind: SliceNodeKind;
|
|
24
|
+
readonly label: string;
|
|
25
|
+
/** Repository-relative path, `/`-separated. */
|
|
26
|
+
readonly sourceFile: string;
|
|
27
|
+
/** Only on `dependency` nodes: a package, an import nothing resolved, or
|
|
28
|
+
* a literal path read at runtime that is no file a slice can copy. */
|
|
29
|
+
readonly dependencyStatus: "external" | "unresolved" | "path" | null;
|
|
30
|
+
/** On a script's `file` node: its own exports; null when not known
|
|
31
|
+
* (`export =`, or a file that is not a script). */
|
|
32
|
+
readonly exports: FileExports | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* - `imports`: an import, re-export, `require` or `import()` of a literal.
|
|
36
|
+
* - `references`: a module named to a test runner or a resolver
|
|
37
|
+
* (`vi.mock`, `jest.requireActual`, `require.resolve`), which needs the
|
|
38
|
+
* module to exist but never loads its code into the slice.
|
|
39
|
+
* - `reads`: a file read by a literal path at runtime
|
|
40
|
+
* (`new URL("./x", import.meta.url)`, `join(__dirname, "x")`), which needs
|
|
41
|
+
* the file itself at that path.
|
|
42
|
+
*/
|
|
43
|
+
export type SliceRelation = "imports" | "references" | "reads";
|
|
44
|
+
export interface SliceGraphLink {
|
|
45
|
+
readonly source: string;
|
|
46
|
+
readonly target: string;
|
|
47
|
+
readonly relation: SliceRelation;
|
|
48
|
+
/** The text as written, for an import or a reference; null otherwise. */
|
|
49
|
+
readonly specifier: string | null;
|
|
50
|
+
/** `L<n>`, the line it is on. */
|
|
51
|
+
readonly location: string | null;
|
|
52
|
+
/** `import type`, `export type`, `import("x").T`: erased at runtime. */
|
|
53
|
+
readonly typeOnly: boolean;
|
|
54
|
+
/** The names it takes, or null for the whole module. */
|
|
55
|
+
readonly names: readonly string[] | null;
|
|
56
|
+
/**
|
|
57
|
+
* For `export ... from`, what it forwards as [name in the module, name
|
|
58
|
+
* exported]: `*` for `export *`, `["*", "ns"]` for `export * as ns`.
|
|
59
|
+
* Null for anything else.
|
|
60
|
+
*/
|
|
61
|
+
readonly forwards: readonly (readonly [string, string])[] | "*" | null;
|
|
62
|
+
}
|
|
63
|
+
export interface SliceGraph {
|
|
64
|
+
readonly nodes: readonly SliceGraphNode[];
|
|
65
|
+
readonly links: readonly SliceGraphLink[];
|
|
66
|
+
}
|
|
67
|
+
export declare function nodeKind(id: string): SliceNodeKind | null;
|
|
68
|
+
/** The file a node belongs to; a dependency placeholder belongs to none. */
|
|
69
|
+
export declare function fileOf(node: SliceGraphNode): string | null;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a slice run writes, and the rules that read it.
|
|
3
|
+
*
|
|
4
|
+
* `slice-manifest.json` is the immutable record of a proposed slice: the
|
|
5
|
+
* source it was cut from, the copied files pinned by hash, the generated
|
|
6
|
+
* files kept apart from them, the cut edges, the externals and whatever
|
|
7
|
+
* blocks the slice from becoming a sandbox. `boundary-contract.json` is the
|
|
8
|
+
* machine-readable shape of the boundary: the symbols the slice imports from
|
|
9
|
+
* outside (stubs) and the symbols outside code imports from it (its public
|
|
10
|
+
* surface), with their declarations. The markdown views are rendered from
|
|
11
|
+
* the contract and never carry anything it does not.
|
|
12
|
+
*/
|
|
13
|
+
import type { SliceBudget, SliceParams } from "./analysis.js";
|
|
14
|
+
import type { SliceExternals } from "./externals.js";
|
|
15
|
+
import type { CutEdge } from "./reach.js";
|
|
16
|
+
export declare const SLICE_MANIFEST_SCHEMA_VERSION = 1;
|
|
17
|
+
export declare const BOUNDARY_CONTRACT_SCHEMA_VERSION = 1;
|
|
18
|
+
/**
|
|
19
|
+
* How much of the boundary carries declarations. `full` means every cut
|
|
20
|
+
* and public symbol has one and the fixture compiled; it says nothing about
|
|
21
|
+
* runtime readiness. `partial` and `names-only` are diagnostic output.
|
|
22
|
+
*/
|
|
23
|
+
export type StubCoverage = "full" | "partial" | "names-only";
|
|
24
|
+
export declare const SLICE_BLOCKER_CODES: readonly ["unknown_entry_point", "unresolved_import", "dynamic_dependency", "path_read", "missing_build_input", "no_extractor", "declaration_incomplete", "compile_error", "secret_file"];
|
|
25
|
+
export type SliceBlockerCode = (typeof SLICE_BLOCKER_CODES)[number];
|
|
26
|
+
/**
|
|
27
|
+
* The blockers a contributor meets in the code itself: an import that
|
|
28
|
+
* resolves to no file, a dynamic import of a computed name, a path read at
|
|
29
|
+
* runtime the folder will not have. Every other
|
|
30
|
+
* code is the slicer's own build falling short (a compile error in its
|
|
31
|
+
* fixture, a configuration it could not read, a language it cannot
|
|
32
|
+
* extract), which says how well the code was measured, not how hard the
|
|
33
|
+
* work is, and one cause can raise dozens of them.
|
|
34
|
+
*/
|
|
35
|
+
export declare const WORK_BLOCKER_CODES: readonly SliceBlockerCode[];
|
|
36
|
+
export declare const isWorkBlocker: (blocker: Pick<SliceBlocker, "code">) => boolean;
|
|
37
|
+
export interface SliceBlocker {
|
|
38
|
+
readonly code: SliceBlockerCode;
|
|
39
|
+
readonly file: string | null;
|
|
40
|
+
readonly location: string | null;
|
|
41
|
+
readonly detail: string;
|
|
42
|
+
}
|
|
43
|
+
export interface BoundarySymbol {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
/** `function`, `class`, `interface`, `type`, `enum`, `variable`, `module`, `unknown`. */
|
|
46
|
+
readonly kind: string;
|
|
47
|
+
/** Declaration text, `.d.ts` style; null when the language has no extractor. */
|
|
48
|
+
readonly declaration: string | null;
|
|
49
|
+
}
|
|
50
|
+
export interface BoundaryModule {
|
|
51
|
+
/** Repository path of the module. */
|
|
52
|
+
readonly module: string;
|
|
53
|
+
readonly symbols: readonly BoundarySymbol[];
|
|
54
|
+
/** Files on the other side of the cut that import it, sorted. */
|
|
55
|
+
readonly importedBy: readonly string[];
|
|
56
|
+
/** For an outbound module: the generated stub's artifact path. */
|
|
57
|
+
readonly stubPath: string | null;
|
|
58
|
+
}
|
|
59
|
+
export interface CompilationResult {
|
|
60
|
+
readonly attempted: boolean;
|
|
61
|
+
readonly ok: boolean;
|
|
62
|
+
readonly diagnostics: readonly {
|
|
63
|
+
readonly file: string | null;
|
|
64
|
+
readonly line: number | null;
|
|
65
|
+
readonly code: string;
|
|
66
|
+
readonly message: string;
|
|
67
|
+
}[];
|
|
68
|
+
/** Package specifiers declared `any` so the fixture could type-check. */
|
|
69
|
+
readonly shimmedPackages: readonly string[];
|
|
70
|
+
/**
|
|
71
|
+
* Errors the source has too, checked the same way with its own code in
|
|
72
|
+
* place of the stubs: what declaring its packages untyped costs, not
|
|
73
|
+
* counted against the slice. Absent when nothing failed.
|
|
74
|
+
*/
|
|
75
|
+
readonly untypedPackageErrors?: CompilationResult["diagnostics"];
|
|
76
|
+
/**
|
|
77
|
+
* Packages whose published types the check read, at the versions read,
|
|
78
|
+
* rather than declaring them untyped. Absent when none were fetched.
|
|
79
|
+
*/
|
|
80
|
+
readonly typedPackages?: readonly {
|
|
81
|
+
readonly name: string;
|
|
82
|
+
readonly version: string;
|
|
83
|
+
}[];
|
|
84
|
+
}
|
|
85
|
+
export interface BoundaryContract {
|
|
86
|
+
readonly schemaVersion: typeof BOUNDARY_CONTRACT_SCHEMA_VERSION;
|
|
87
|
+
readonly toolVersion: string;
|
|
88
|
+
readonly extractorVersion: string | null;
|
|
89
|
+
/** The commit the source was read at; null outside Git. */
|
|
90
|
+
readonly sourceCommitSha: string | null;
|
|
91
|
+
readonly language: "typescript" | "none";
|
|
92
|
+
/** Slice → outside: what the slice needs, as stubs. */
|
|
93
|
+
readonly outbound: readonly BoundaryModule[];
|
|
94
|
+
/** Outside → slice: what the slice must keep offering. */
|
|
95
|
+
readonly inbound: readonly BoundaryModule[];
|
|
96
|
+
readonly stubCoverage: StubCoverage;
|
|
97
|
+
readonly compilation: CompilationResult;
|
|
98
|
+
readonly blockers: readonly SliceBlocker[];
|
|
99
|
+
}
|
|
100
|
+
export interface IncludedFile {
|
|
101
|
+
readonly path: string;
|
|
102
|
+
/** Git's blob id of the bytes read, so the sandbox can verify its copy. */
|
|
103
|
+
readonly blobId: string;
|
|
104
|
+
readonly sha256: string;
|
|
105
|
+
/** `100644` or `100755`, as Git records it. */
|
|
106
|
+
readonly mode: string;
|
|
107
|
+
readonly sizeBytes: number;
|
|
108
|
+
readonly operations: readonly "edit"[];
|
|
109
|
+
}
|
|
110
|
+
export interface SyntheticFile {
|
|
111
|
+
readonly path: string;
|
|
112
|
+
readonly kind: "stub" | "shim" | "config";
|
|
113
|
+
readonly generator: string;
|
|
114
|
+
readonly generatorVersion: string;
|
|
115
|
+
readonly sha256: string;
|
|
116
|
+
readonly sizeBytes: number;
|
|
117
|
+
}
|
|
118
|
+
export interface RequiredBuildInputs {
|
|
119
|
+
/** Compiler and package configuration the included files are read with. */
|
|
120
|
+
readonly configs: readonly string[];
|
|
121
|
+
/** Packages the included files import, with the version the source pins. */
|
|
122
|
+
readonly packages: readonly {
|
|
123
|
+
readonly name: string;
|
|
124
|
+
readonly version: string | null;
|
|
125
|
+
readonly declaredIn: string | null;
|
|
126
|
+
/**
|
|
127
|
+
* One of the repository's own workspace packages: its source lives
|
|
128
|
+
* here, not on the registry, so it is mocked and never installed.
|
|
129
|
+
*/
|
|
130
|
+
readonly workspace?: true;
|
|
131
|
+
/**
|
|
132
|
+
* Where the repository says it is fetched from; absent when nothing
|
|
133
|
+
* it holds says. Only a package shown to come from the public
|
|
134
|
+
* registry is installed by its name.
|
|
135
|
+
*/
|
|
136
|
+
readonly registry?: PackageRegistry;
|
|
137
|
+
}[];
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* What a repository's lockfiles and npm configuration say of a package's
|
|
141
|
+
* registry. `public` is a lockfile entry resolved from the public registry
|
|
142
|
+
* with no configuration mapping its scope elsewhere; `private` is any
|
|
143
|
+
* lockfile entry or configuration that names another registry.
|
|
144
|
+
*/
|
|
145
|
+
export interface PackageRegistry {
|
|
146
|
+
readonly source: "public" | "private";
|
|
147
|
+
/** The file that says so, such as `package-lock.json` or `.npmrc`. */
|
|
148
|
+
readonly evidence: string;
|
|
149
|
+
}
|
|
150
|
+
/** Every operation a contribution may make on the slice has a stated rule. */
|
|
151
|
+
export interface OperationPolicy {
|
|
152
|
+
readonly edit: "included-source-only";
|
|
153
|
+
readonly add: "requires-approved-path-mapping";
|
|
154
|
+
readonly delete: "requires-explicit-allowance";
|
|
155
|
+
readonly rename: "requires-explicit-allowance";
|
|
156
|
+
readonly synthetic: "rejected";
|
|
157
|
+
readonly unknown: "refused";
|
|
158
|
+
}
|
|
159
|
+
export declare const OPERATION_POLICY: OperationPolicy;
|
|
160
|
+
export interface SliceMeta {
|
|
161
|
+
readonly includedFiles: number;
|
|
162
|
+
readonly includedBytes: number;
|
|
163
|
+
readonly outboundCuts: number;
|
|
164
|
+
readonly inboundCuts: number;
|
|
165
|
+
readonly stubCoverage: StubCoverage;
|
|
166
|
+
readonly externals: number;
|
|
167
|
+
readonly blockers: number;
|
|
168
|
+
readonly ready: boolean;
|
|
169
|
+
}
|
|
170
|
+
export interface SliceManifest {
|
|
171
|
+
readonly schemaVersion: typeof SLICE_MANIFEST_SCHEMA_VERSION;
|
|
172
|
+
readonly toolVersion: string;
|
|
173
|
+
readonly extractorVersion: string | null;
|
|
174
|
+
/** The commit the source was read at; null outside Git. */
|
|
175
|
+
readonly sourceCommitSha: string | null;
|
|
176
|
+
/** Whether any included file differed from that commit. */
|
|
177
|
+
readonly sourceDirty: boolean;
|
|
178
|
+
/** The import graph walked, as `canonicalJson` writes it. */
|
|
179
|
+
readonly graphSha256: string;
|
|
180
|
+
readonly params: SliceParams;
|
|
181
|
+
readonly entryPoints: readonly string[];
|
|
182
|
+
readonly budget: SliceBudget;
|
|
183
|
+
readonly included: readonly IncludedFile[];
|
|
184
|
+
readonly synthetic: readonly SyntheticFile[];
|
|
185
|
+
readonly requiredBuildInputs: RequiredBuildInputs;
|
|
186
|
+
readonly cuts: {
|
|
187
|
+
readonly outbound: readonly CutEdge[];
|
|
188
|
+
readonly inbound: readonly CutEdge[];
|
|
189
|
+
};
|
|
190
|
+
/** Import edges between included files, with their specifiers; see `reach`. */
|
|
191
|
+
readonly internalImports: readonly CutEdge[];
|
|
192
|
+
readonly externals: SliceExternals;
|
|
193
|
+
readonly blockers: readonly SliceBlocker[];
|
|
194
|
+
readonly policy: OperationPolicy;
|
|
195
|
+
readonly boundaryContractSha256: string;
|
|
196
|
+
readonly meta: SliceMeta;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Whether a slice may advance past diagnosis. Only full declaration
|
|
200
|
+
* coverage with no blockers may; the sandbox phase then proves the runtime.
|
|
201
|
+
*/
|
|
202
|
+
export declare function sliceReadiness(input: {
|
|
203
|
+
readonly stubCoverage: StubCoverage;
|
|
204
|
+
readonly blockers: readonly SliceBlocker[];
|
|
205
|
+
}): {
|
|
206
|
+
ready: boolean;
|
|
207
|
+
reasons: string[];
|
|
208
|
+
};
|
|
209
|
+
/** The bounded view of a boundary a console renders without the files. */
|
|
210
|
+
export interface SliceBoundarySummary {
|
|
211
|
+
readonly schemaVersion: 1;
|
|
212
|
+
readonly language: BoundaryContract["language"];
|
|
213
|
+
readonly stubCoverage: StubCoverage;
|
|
214
|
+
readonly ready: boolean;
|
|
215
|
+
readonly counts: {
|
|
216
|
+
readonly includedFiles: number;
|
|
217
|
+
readonly includedBytes: number;
|
|
218
|
+
readonly outboundModules: number;
|
|
219
|
+
readonly inboundModules: number;
|
|
220
|
+
readonly stubs: number;
|
|
221
|
+
readonly publicSymbols: number;
|
|
222
|
+
readonly externals: number;
|
|
223
|
+
readonly blockers: number;
|
|
224
|
+
};
|
|
225
|
+
readonly included: readonly string[];
|
|
226
|
+
readonly outbound: readonly SummaryModule[];
|
|
227
|
+
readonly inbound: readonly SummaryModule[];
|
|
228
|
+
readonly externals: {
|
|
229
|
+
readonly packages: readonly {
|
|
230
|
+
specifier: string;
|
|
231
|
+
service: string;
|
|
232
|
+
}[];
|
|
233
|
+
readonly environment: readonly string[];
|
|
234
|
+
};
|
|
235
|
+
readonly blockers: readonly SliceBlocker[];
|
|
236
|
+
/** Whether any list above was cut short. */
|
|
237
|
+
readonly truncated: boolean;
|
|
238
|
+
}
|
|
239
|
+
export interface SummaryModule {
|
|
240
|
+
readonly module: string;
|
|
241
|
+
readonly symbols: readonly string[];
|
|
242
|
+
readonly importedBy: readonly string[];
|
|
243
|
+
readonly truncated: boolean;
|
|
244
|
+
}
|
|
245
|
+
export declare const SUMMARY_LIMITS: {
|
|
246
|
+
readonly files: 200;
|
|
247
|
+
readonly modules: 100;
|
|
248
|
+
readonly symbols: 50;
|
|
249
|
+
readonly importers: 10;
|
|
250
|
+
readonly blockers: 50;
|
|
251
|
+
readonly externalPackages: 100;
|
|
252
|
+
readonly environment: 100;
|
|
253
|
+
};
|
|
254
|
+
export declare function boundarySummary(contract: BoundaryContract, manifest: Pick<SliceManifest, "included" | "externals" | "meta">): SliceBoundarySummary;
|
|
255
|
+
/**
|
|
256
|
+
* `BOUNDARY.md`: an index of the boundary, rendered from the contract. What
|
|
257
|
+
* the slice holds is in the README and `carveout.json`; what each stub
|
|
258
|
+
* declares is in the stub.
|
|
259
|
+
*/
|
|
260
|
+
export declare function renderBoundaryMarkdown(contract: BoundaryContract, manifest: Pick<SliceManifest, "included" | "entryPoints" | "budget" | "externals" | "meta">): string;
|
|
261
|
+
/** `public-surface.md`: only what a contribution must keep. */
|
|
262
|
+
export declare function renderPublicSurfaceMarkdown(contract: BoundaryContract): string;
|
|
@@ -169,13 +169,14 @@ export function boundarySummary(contract, manifest) {
|
|
|
169
169
|
* them; repeating them here cost a session an eighth of everything it read
|
|
170
170
|
* (E6c: BOUNDARY.md opened in 60 of 60 slice runs).
|
|
171
171
|
*/
|
|
172
|
-
function renderModules(modules, empty) {
|
|
172
|
+
function renderModules(modules, empty, rest = "") {
|
|
173
173
|
if (modules.length === 0)
|
|
174
174
|
return [empty];
|
|
175
175
|
return modules.map((module) => {
|
|
176
176
|
const names = module.symbols.map((symbol) => symbol.name);
|
|
177
177
|
const shown = names.slice(0, 12).map((name) => `\`${name}\``).join(", ");
|
|
178
|
-
|
|
178
|
+
// Where every name of a long list is, rather than all of them here.
|
|
179
|
+
const more = names.length > 12 ? `, +${names.length - 12} more${rest}` : "";
|
|
179
180
|
// The folder writes each stub beside the module's path, not where the
|
|
180
181
|
// contract files it.
|
|
181
182
|
const stub = module.stubPath === null
|
|
@@ -206,7 +207,7 @@ export function renderBoundaryMarkdown(contract, manifest) {
|
|
|
206
207
|
lines.push(`- ${reason}`);
|
|
207
208
|
lines.push("");
|
|
208
209
|
}
|
|
209
|
-
lines.push("## Stubbed modules", "", "What the slice imports from outside, with the names it takes. Each stub's `.d.ts` declares them; the `.js` beside it is a recording mock.", "", ...renderModules(contract.outbound, "The slice imports nothing outside itself."), "", "## Public surface", "", "What outside code imports from the slice; a change must keep these exports to fit back.", "", ...renderModules(contract.inbound, "Nothing outside the slice imports it."), "", "## Externals to mock", "");
|
|
210
|
+
lines.push("## Stubbed modules", "", "What the slice imports from outside, with the names it takes. Each stub's `.d.ts` declares them; the `.js` beside it is a recording mock.", "", ...renderModules(contract.outbound, "The slice imports nothing outside itself.", " (the stub declares each)"), "", "## Public surface", "", "What outside code imports from the slice; a change must keep these exports to fit back.", "", ...renderModules(contract.inbound, "Nothing outside the slice imports it.", " (`carveout/surface/check.ts` holds each, a line apiece)"), "", "## Externals to mock", "");
|
|
210
211
|
if (manifest.externals.packages.length === 0 &&
|
|
211
212
|
manifest.externals.environment.length === 0)
|
|
212
213
|
lines.push("No service SDKs or environment reads were detected.");
|