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.
- package/AGENTS.md +21 -3
- package/NOTICE.md +2 -1
- package/README.md +274 -460
- 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,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hosted platform (sandbox-factory, platform.lunox.work) as this CLI
|
|
3
|
+
* talks to it: the base URL, the shared credentials file, and one request
|
|
4
|
+
* function on `fetch`, so carveout keeps `typescript` as its only
|
|
5
|
+
* dependency. Nothing here runs unless a platform command (`login`,
|
|
6
|
+
* `whoami`, `logout`, in `account.ts`) is called; every other command stays
|
|
7
|
+
* local, and `carveout/api` never imports it.
|
|
8
|
+
*
|
|
9
|
+
* Credentials: `~/.lunox/credentials` (LUNOX_CREDENTIALS moves it), JSON,
|
|
10
|
+
* mode 600 in a 700 folder, written whole through a rename. duediligence
|
|
11
|
+
* shares the file, so each CLI keeps its own entry under the platform's URL:
|
|
12
|
+
*
|
|
13
|
+
* { "https://platform.lunox.work": {
|
|
14
|
+
* "carveout": { token, organizationId, organizationHandle, client, createdAt, expiresAt },
|
|
15
|
+
* "duediligence": { ... } } }
|
|
16
|
+
*
|
|
17
|
+
* A token is minted for one client with that client's scopes, so the two
|
|
18
|
+
* CLIs never share one; writing an entry keeps every other.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DEFAULT_PLATFORM = "https://platform.lunox.work";
|
|
21
|
+
export interface Credential {
|
|
22
|
+
readonly token: string;
|
|
23
|
+
readonly organizationId: string;
|
|
24
|
+
readonly organizationHandle: string;
|
|
25
|
+
readonly client: string;
|
|
26
|
+
readonly createdAt: string;
|
|
27
|
+
readonly expiresAt: string | null;
|
|
28
|
+
}
|
|
29
|
+
type CredentialsFile = Record<string, Record<string, unknown>>;
|
|
30
|
+
export declare class PlatformError extends Error {
|
|
31
|
+
readonly status: number | undefined;
|
|
32
|
+
readonly code: string | undefined;
|
|
33
|
+
constructor(message: string, details?: {
|
|
34
|
+
status?: number;
|
|
35
|
+
code?: string | undefined;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** `--platform`, else LUNOX_PLATFORM_URL, else production; no trailing slash. HTTPS, or plain HTTP to this machine only. */
|
|
39
|
+
export declare function platformUrl(given: string | undefined, env?: NodeJS.ProcessEnv): string;
|
|
40
|
+
export declare function credentialsPath(env?: NodeJS.ProcessEnv): string;
|
|
41
|
+
/** The whole file, `{}` when there is none. A file that is not a JSON object is refused, never overwritten. */
|
|
42
|
+
export declare function readCredentials(path: string): CredentialsFile;
|
|
43
|
+
/** Writes the file whole: a 600 temp file beside it, renamed over it. Removes it when nothing is left. */
|
|
44
|
+
export declare function writeCredentials(path: string, all: CredentialsFile): void;
|
|
45
|
+
/** This client's entry for a platform, or undefined. */
|
|
46
|
+
export declare function credentialFor(path: string, base: string, client: string): Credential | undefined;
|
|
47
|
+
/** Sets (or, with `entry` undefined, removes) this client's entry, keeping every other. */
|
|
48
|
+
export declare function saveCredential(path: string, base: string, client: string, entry: Credential | undefined): void;
|
|
49
|
+
export interface RequestOptions {
|
|
50
|
+
readonly base: string;
|
|
51
|
+
readonly path: string;
|
|
52
|
+
readonly client: string;
|
|
53
|
+
readonly version: string;
|
|
54
|
+
readonly method?: string;
|
|
55
|
+
readonly token?: string | undefined;
|
|
56
|
+
/** The token is a device sign-in's session rather than a saved API token. */
|
|
57
|
+
readonly session?: boolean;
|
|
58
|
+
readonly body?: unknown;
|
|
59
|
+
readonly fetchImpl?: typeof fetch;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* One call to the platform's API. Sends the token as a bearer, the client
|
|
63
|
+
* and its version as `User-Agent` and `Lunox-Client-Version`
|
|
64
|
+
* (`<client>/<version>`), and JSON. Answers the parsed body; throws a
|
|
65
|
+
* PlatformError whose message says what to do: 401 → sign in again, 426 →
|
|
66
|
+
* upgrade, otherwise the API's own `{ error, code }`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function request<T = Record<string, unknown>>(options: RequestOptions): Promise<T>;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hosted platform (sandbox-factory, platform.lunox.work) as this CLI
|
|
3
|
+
* talks to it: the base URL, the shared credentials file, and one request
|
|
4
|
+
* function on `fetch`, so carveout keeps `typescript` as its only
|
|
5
|
+
* dependency. Nothing here runs unless a platform command (`login`,
|
|
6
|
+
* `whoami`, `logout`, in `account.ts`) is called; every other command stays
|
|
7
|
+
* local, and `carveout/api` never imports it.
|
|
8
|
+
*
|
|
9
|
+
* Credentials: `~/.lunox/credentials` (LUNOX_CREDENTIALS moves it), JSON,
|
|
10
|
+
* mode 600 in a 700 folder, written whole through a rename. duediligence
|
|
11
|
+
* shares the file, so each CLI keeps its own entry under the platform's URL:
|
|
12
|
+
*
|
|
13
|
+
* { "https://platform.lunox.work": {
|
|
14
|
+
* "carveout": { token, organizationId, organizationHandle, client, createdAt, expiresAt },
|
|
15
|
+
* "duediligence": { ... } } }
|
|
16
|
+
*
|
|
17
|
+
* A token is minted for one client with that client's scopes, so the two
|
|
18
|
+
* CLIs never share one; writing an entry keeps every other.
|
|
19
|
+
*/
|
|
20
|
+
import { randomBytes } from "node:crypto";
|
|
21
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
export const DEFAULT_PLATFORM = "https://platform.lunox.work";
|
|
25
|
+
const LOOPBACK = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
26
|
+
export class PlatformError extends Error {
|
|
27
|
+
status;
|
|
28
|
+
code;
|
|
29
|
+
constructor(message, details = {}) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "PlatformError";
|
|
32
|
+
this.status = details.status;
|
|
33
|
+
this.code = details.code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** `--platform`, else LUNOX_PLATFORM_URL, else production; no trailing slash. HTTPS, or plain HTTP to this machine only. */
|
|
37
|
+
export function platformUrl(given, env = process.env) {
|
|
38
|
+
const raw = given ?? env["LUNOX_PLATFORM_URL"] ?? DEFAULT_PLATFORM;
|
|
39
|
+
let url;
|
|
40
|
+
try {
|
|
41
|
+
url = new URL(raw);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new PlatformError(`${raw} is not a URL.`);
|
|
45
|
+
}
|
|
46
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && LOOPBACK.has(url.hostname))) {
|
|
47
|
+
throw new PlatformError(`${raw}: the platform must be https:// (plain http only on localhost).`);
|
|
48
|
+
}
|
|
49
|
+
url.search = "";
|
|
50
|
+
url.hash = "";
|
|
51
|
+
return url.href.replace(/\/+$/, "");
|
|
52
|
+
}
|
|
53
|
+
export function credentialsPath(env = process.env) {
|
|
54
|
+
return env["LUNOX_CREDENTIALS"] ?? join(homedir(), ".lunox", "credentials");
|
|
55
|
+
}
|
|
56
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
57
|
+
/** The whole file, `{}` when there is none. A file that is not a JSON object is refused, never overwritten. */
|
|
58
|
+
export function readCredentials(path) {
|
|
59
|
+
if (!existsSync(path))
|
|
60
|
+
return {};
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new PlatformError(`${path} is not valid JSON (${error.message}); fix or remove it.`);
|
|
67
|
+
}
|
|
68
|
+
if (!isObject(parsed))
|
|
69
|
+
throw new PlatformError(`${path} does not hold a JSON object; fix or remove it.`);
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
/** Writes the file whole: a 600 temp file beside it, renamed over it. Removes it when nothing is left. */
|
|
73
|
+
export function writeCredentials(path, all) {
|
|
74
|
+
const dir = dirname(path);
|
|
75
|
+
if (!existsSync(dir)) {
|
|
76
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
77
|
+
chmodSync(dir, 0o700);
|
|
78
|
+
}
|
|
79
|
+
if (Object.keys(all).length === 0) {
|
|
80
|
+
rmSync(path, { force: true });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const temp = join(dir, `.credentials-${process.pid}-${randomBytes(6).toString("hex")}.tmp`);
|
|
84
|
+
try {
|
|
85
|
+
writeFileSync(temp, `${JSON.stringify(all, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
86
|
+
chmodSync(temp, 0o600);
|
|
87
|
+
renameSync(temp, path);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
rmSync(temp, { force: true });
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** This client's entry for a platform, or undefined. */
|
|
95
|
+
export function credentialFor(path, base, client) {
|
|
96
|
+
const entry = readCredentials(path)[base]?.[client];
|
|
97
|
+
return isObject(entry) && typeof entry["token"] === "string" ? entry : undefined;
|
|
98
|
+
}
|
|
99
|
+
/** Sets (or, with `entry` undefined, removes) this client's entry, keeping every other. */
|
|
100
|
+
export function saveCredential(path, base, client, entry) {
|
|
101
|
+
const all = readCredentials(path);
|
|
102
|
+
const forBase = { ...(isObject(all[base]) ? all[base] : {}) };
|
|
103
|
+
if (entry === undefined)
|
|
104
|
+
delete forBase[client];
|
|
105
|
+
else
|
|
106
|
+
forBase[client] = entry;
|
|
107
|
+
if (Object.keys(forBase).length === 0)
|
|
108
|
+
delete all[base];
|
|
109
|
+
else
|
|
110
|
+
all[base] = forBase;
|
|
111
|
+
writeCredentials(path, all);
|
|
112
|
+
}
|
|
113
|
+
/** The minimum version a 426 names, wherever the body puts it. */
|
|
114
|
+
function minimumOf(body) {
|
|
115
|
+
const named = body?.["minimumVersion"] ?? body?.["minimum_version"] ?? body?.["minimum"];
|
|
116
|
+
if (typeof named === "string")
|
|
117
|
+
return named;
|
|
118
|
+
const said = body?.["error"];
|
|
119
|
+
return typeof said === "string" ? /\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/.exec(said)?.[0] : undefined;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* One call to the platform's API. Sends the token as a bearer, the client
|
|
123
|
+
* and its version as `User-Agent` and `Lunox-Client-Version`
|
|
124
|
+
* (`<client>/<version>`), and JSON. Answers the parsed body; throws a
|
|
125
|
+
* PlatformError whose message says what to do: 401 → sign in again, 426 →
|
|
126
|
+
* upgrade, otherwise the API's own `{ error, code }`.
|
|
127
|
+
*/
|
|
128
|
+
export async function request(options) {
|
|
129
|
+
const { base, path, client, version, method = "GET", token, session = false, body, fetchImpl = fetch } = options;
|
|
130
|
+
const headers = {
|
|
131
|
+
Accept: "application/json",
|
|
132
|
+
"User-Agent": `${client}/${version}`,
|
|
133
|
+
"Lunox-Client-Version": `${client}/${version}`,
|
|
134
|
+
};
|
|
135
|
+
if (token !== undefined)
|
|
136
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
137
|
+
if (body !== undefined)
|
|
138
|
+
headers["Content-Type"] = "application/json";
|
|
139
|
+
let response;
|
|
140
|
+
try {
|
|
141
|
+
response = await fetchImpl(`${base}${path}`, {
|
|
142
|
+
method,
|
|
143
|
+
headers,
|
|
144
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
145
|
+
redirect: "error",
|
|
146
|
+
signal: AbortSignal.timeout(30_000),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
const cause = error.cause?.message;
|
|
151
|
+
throw new PlatformError(`Could not reach ${base}: ${cause ?? error.message}`);
|
|
152
|
+
}
|
|
153
|
+
const text = await response.text();
|
|
154
|
+
let parsed;
|
|
155
|
+
try {
|
|
156
|
+
const value = text === "" ? undefined : JSON.parse(text);
|
|
157
|
+
parsed = isObject(value) ? value : undefined;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
parsed = undefined;
|
|
161
|
+
}
|
|
162
|
+
if (response.ok)
|
|
163
|
+
return (parsed ?? {});
|
|
164
|
+
const status = response.status;
|
|
165
|
+
const error = parsed?.["error"];
|
|
166
|
+
const code = typeof parsed?.["code"] === "string"
|
|
167
|
+
? parsed["code"]
|
|
168
|
+
: typeof error === "string" && !error.includes(" ")
|
|
169
|
+
? error
|
|
170
|
+
: undefined;
|
|
171
|
+
if (status === 426) {
|
|
172
|
+
const minimum = minimumOf(parsed);
|
|
173
|
+
throw new PlatformError(`${base} needs a newer ${client}${minimum ? `: upgrade to at least ${minimum}` : ""} (this is ${version}). npm i -g ${client}@latest`, { status, code });
|
|
174
|
+
}
|
|
175
|
+
if (status === 401 && token !== undefined) {
|
|
176
|
+
const what = session ? "The browser sign-in" : "The saved sign-in";
|
|
177
|
+
throw new PlatformError(`${what} for ${base} is no longer valid (expired or revoked). Run \`${client} login\` again.`, {
|
|
178
|
+
status,
|
|
179
|
+
code,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const said = parsed?.["error_description"] ?? error ?? parsed?.["message"];
|
|
183
|
+
throw new PlatformError(typeof said === "string" ? `${said}${code && code !== said ? ` (${code})` : ""}` : `${method} ${path}: HTTP ${status}`, { status, code });
|
|
184
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a folder handed to someone outside may bring back.
|
|
3
|
+
*
|
|
4
|
+
* A folder comes back from a contributor the repository does not trust, so
|
|
5
|
+
* nothing in it is taken on its word: the paths its `carveout.json` names
|
|
6
|
+
* must stay inside the folder and the repository, the repository is the one
|
|
7
|
+
* the maintainer names (or the one the record names, checked to hold the
|
|
8
|
+
* cut's commit), and the patch carries only what a change to the slice can
|
|
9
|
+
* honestly need. New files outside the slice's packages, files that run on
|
|
10
|
+
* someone's machine or in CI without a review of code (workflows, hooks,
|
|
11
|
+
* manifests, lockfiles, environment), deleted tests and tests whose
|
|
12
|
+
* assertions were removed, rewritten or skipped are left out and named,
|
|
13
|
+
* unless the maintainer allows them by path or glob.
|
|
14
|
+
*/
|
|
15
|
+
import { weakenedTest } from "./test-integrity.js";
|
|
16
|
+
/**
|
|
17
|
+
* Paths a patch from a slice never carries unless allowed: they run in CI
|
|
18
|
+
* with its secrets, on a maintainer's machine (hooks, editor tasks), at
|
|
19
|
+
* install (manifests, lockfiles, registry configuration), or hold
|
|
20
|
+
* configuration a change to code does not need.
|
|
21
|
+
*/
|
|
22
|
+
export declare const PROTECTED_PATHS: readonly string[];
|
|
23
|
+
/**
|
|
24
|
+
* Throws unless `path` is a plain relative path that stays where it is
|
|
25
|
+
* joined: `/`-separated, not absolute, no `.` or `..` segment, no `.git`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertSafePath(path: unknown, what: string): asserts path is string;
|
|
28
|
+
/** What `checkRecordPaths` reads of a record. */
|
|
29
|
+
interface RecordPaths {
|
|
30
|
+
readonly manifest: {
|
|
31
|
+
readonly included: readonly {
|
|
32
|
+
readonly path: unknown;
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
35
|
+
readonly generated: readonly {
|
|
36
|
+
readonly path: unknown;
|
|
37
|
+
}[];
|
|
38
|
+
readonly copied?: readonly {
|
|
39
|
+
readonly path: unknown;
|
|
40
|
+
}[];
|
|
41
|
+
readonly extras?: readonly {
|
|
42
|
+
readonly path: unknown;
|
|
43
|
+
}[];
|
|
44
|
+
readonly importRewrites: readonly {
|
|
45
|
+
readonly file: unknown;
|
|
46
|
+
}[];
|
|
47
|
+
readonly request: {
|
|
48
|
+
readonly entryPoints: readonly unknown[];
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Every path a record makes carveout read, write or delete, checked. */
|
|
52
|
+
export declare function checkRecordPaths(record: RecordPaths): void;
|
|
53
|
+
/**
|
|
54
|
+
* The checkout to diff and apply against: the one the maintainer names, else
|
|
55
|
+
* the record's. Either way it must be a folder that holds the cut's commit,
|
|
56
|
+
* so a record cannot point carveout at another folder of the machine.
|
|
57
|
+
*/
|
|
58
|
+
export declare function trustedRepo(record: {
|
|
59
|
+
readonly repo?: string | null;
|
|
60
|
+
readonly manifest: {
|
|
61
|
+
readonly sourceCommitSha: string | null;
|
|
62
|
+
};
|
|
63
|
+
}, given: string | undefined): string;
|
|
64
|
+
export interface Refusal {
|
|
65
|
+
readonly path: string;
|
|
66
|
+
readonly reason: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Configuration that decides what a check holds code to: compiler, linter
|
|
70
|
+
* and test-runner settings. A change that loosens one makes every check
|
|
71
|
+
* after it pass more, so `gate` names it for a review.
|
|
72
|
+
*/
|
|
73
|
+
export declare const CHECK_CONFIG_PATHS: readonly string[];
|
|
74
|
+
/**
|
|
75
|
+
* How many check suppressions a script's text holds. A test's
|
|
76
|
+
* `@ts-expect-error` asserts that a call does not type-check, which is a
|
|
77
|
+
* test of the types, not a suppression.
|
|
78
|
+
*/
|
|
79
|
+
export declare function suppressionsOf(text: string, path?: string): number;
|
|
80
|
+
/**
|
|
81
|
+
* Judges each path a patch would carry. `packages` are the repository's
|
|
82
|
+
* package folders the slice holds files of ("" for the root package);
|
|
83
|
+
* `allow` are the maintainer's paths and globs that pass whatever the rule.
|
|
84
|
+
*/
|
|
85
|
+
export declare function handoffPolicy(options: {
|
|
86
|
+
/** Without it, a new file may go anywhere. */
|
|
87
|
+
readonly packages?: ReadonlySet<string>;
|
|
88
|
+
readonly packageOf?: (path: string) => string;
|
|
89
|
+
readonly allow?: readonly string[];
|
|
90
|
+
/**
|
|
91
|
+
* Also refuse what loosens a check: an edit to compiler, linter or
|
|
92
|
+
* test-runner configuration, and a script that gains suppressions
|
|
93
|
+
* (`@ts-ignore`, `eslint-disable`, coverage ignores). `gate` turns it on.
|
|
94
|
+
*/
|
|
95
|
+
readonly checks?: boolean;
|
|
96
|
+
/** How a refusal names a test: "a test the slice was handed" by default. */
|
|
97
|
+
readonly testNoun?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Also refuse a script that gains a reach it did not have: the disk, the
|
|
100
|
+
* network, the process or its environment, a service SDK, or code loaded
|
|
101
|
+
* by a computed name (`reachOf`). A returned test or module runs beside
|
|
102
|
+
* the whole repository, the closed part included, so `patch` turns it on.
|
|
103
|
+
*/
|
|
104
|
+
readonly reach?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Also refuse a script that gains a way to pass a test without the code
|
|
107
|
+
* passing it (`integrityOf`): ending the test run early, swallowing its
|
|
108
|
+
* failures, telling a test run from a real one, rewriting the assertion
|
|
109
|
+
* library or a built-in. `patch` and `gate` turn it on.
|
|
110
|
+
*/
|
|
111
|
+
readonly integrity?: boolean;
|
|
112
|
+
/** Packages the folder installs or mocks: a bare import of another is new reach. */
|
|
113
|
+
readonly knownPackages?: ReadonlySet<string>;
|
|
114
|
+
/** Whether a repository path is one the slice holds or stubs; see `ReachContext.holds`. */
|
|
115
|
+
readonly holds?: (path: string) => boolean;
|
|
116
|
+
/** Whether a path is a test's helper by what imports it (only tests do). */
|
|
117
|
+
readonly testSupport?: (path: string) => boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Whether a path is a setup file the test runner runs before every test
|
|
120
|
+
* (Vitest's `setupFiles`): whatever its name, a change to it is a change
|
|
121
|
+
* to what a check holds code to. A `vi.mock` there replaces the code
|
|
122
|
+
* under test in every test of the package.
|
|
123
|
+
*/
|
|
124
|
+
readonly testSetup?: (path: string) => boolean;
|
|
125
|
+
}): (path: string, change: "added" | "changed" | "deleted", texts?: {
|
|
126
|
+
readonly before: string | null;
|
|
127
|
+
readonly after: string;
|
|
128
|
+
}) => string | null;
|
|
129
|
+
/** What a script is read against: the packages and package folders the slice has. */
|
|
130
|
+
export interface ReachContext {
|
|
131
|
+
/** Packages the folder installs or mocks; a bare import of any other is new reach. */
|
|
132
|
+
readonly knownPackages?: ReadonlySet<string>;
|
|
133
|
+
/** The slice's package folders, and how a path maps to one. */
|
|
134
|
+
readonly packages?: ReadonlySet<string>;
|
|
135
|
+
readonly packageOf?: (path: string) => string;
|
|
136
|
+
/** Whether a repository path is one the slice holds or stubs already: importing it reaches nothing new. */
|
|
137
|
+
readonly holds?: (path: string) => boolean;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* What a script reaches beyond its own computation, read from its syntax
|
|
141
|
+
* and never run: Node modules with the disk, the network or the process
|
|
142
|
+
* (`node:fs`), service SDKs, a package the slice does not have, a module
|
|
143
|
+
* outside the slice's packages, network and code-running globals (`fetch`,
|
|
144
|
+
* `eval`, `Function` by any route), the environment (`process.env`,
|
|
145
|
+
* `import.meta.env`, `process.report`), the module loader, a file loaded
|
|
146
|
+
* raw or as native code, and modules or globals named by a computed value
|
|
147
|
+
* or hidden behind one (`const p = process`), which could be any of these.
|
|
148
|
+
* Each as a short label, sorted. Text-level: a hold for a reader, not a
|
|
149
|
+
* sandbox.
|
|
150
|
+
*/
|
|
151
|
+
export declare function reachOf(path: string, text: string, context?: ReachContext): string[];
|
|
152
|
+
/**
|
|
153
|
+
* How a script could make a test pass without the code under test passing
|
|
154
|
+
* it, read from its syntax and never run. In any script: ending the
|
|
155
|
+
* process early with no failing code (`process.exit(0)` before the tests
|
|
156
|
+
* run reports success; in the code under test, when it loads), listening
|
|
157
|
+
* for the failures it would otherwise die of (`process.on("uncaughtException",
|
|
158
|
+
* …)`), rewriting the assertion library or the runner (`assert.equal = …`,
|
|
159
|
+
* an alias of it, `expect.extend` over a built-in matcher,
|
|
160
|
+
* `addEqualityTesters`), and rewriting a built-in (`Object.is = …`), which
|
|
161
|
+
* a test may do only for the clock and the dice (`Date.now`,
|
|
162
|
+
* `Math.random`). In code that is not a test nor a test's helper: telling
|
|
163
|
+
* a test run from a real one (`typeof describe`, `import.meta.vitest`,
|
|
164
|
+
* `process.env.VITEST`, `NODE_ENV === "test"`, `process.argv` matched
|
|
165
|
+
* against a runner, the stack read for one). `process` and the built-ins
|
|
166
|
+
* are followed through aliases and casts. Each as a short label, sorted.
|
|
167
|
+
* Text-level, like `reachOf`: a hold for a reader, not a sandbox.
|
|
168
|
+
*/
|
|
169
|
+
export declare function integrityOf(path: string, text: string): string[];
|
|
170
|
+
/**
|
|
171
|
+
* The ways to pass a test without the code that a script's new text has
|
|
172
|
+
* more of than its old text (`integrityOf`), or nothing when it is no script.
|
|
173
|
+
*/
|
|
174
|
+
export declare function gainedIntegrity(path: string, before: string | null, after: string): string[];
|
|
175
|
+
/**
|
|
176
|
+
* The reach a script's new text has more of than its old text did
|
|
177
|
+
* (`reachOf`), or nothing when it is no script. A file that already read
|
|
178
|
+
* the disk may keep doing so: only what the change adds is a question.
|
|
179
|
+
*/
|
|
180
|
+
export declare function gainedReach(path: string, before: string | null, after: string, context?: ReachContext): string[];
|
|
181
|
+
export { weakenedTest };
|