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
package/dist/src/cli.js
CHANGED
|
@@ -4,61 +4,115 @@
|
|
|
4
4
|
* Exit codes: 0 done, 1 refused or failed, 2 written with blockers (cut)
|
|
5
5
|
* or a step failed (check, merge), 3 nothing to patch, 4 stale (status),
|
|
6
6
|
* 5 the edits reach past the slice (patch, merge), 6 the hand-off policy
|
|
7
|
-
* kept files out of the patch (patch, merge) or held a change (gate)
|
|
7
|
+
* kept files out of the patch (patch, merge) or held a change (gate), 7 the
|
|
8
|
+
* folder's edits and an applied change overlap (apply).
|
|
8
9
|
*/
|
|
9
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { resolve } from "node:path";
|
|
10
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
11
12
|
import { parseArgs } from "node:util";
|
|
12
|
-
import {
|
|
13
|
+
import { defaultSandbox, sandboxUnavailable } from "./sandbox.js";
|
|
14
|
+
import { CARVEOUT_VERSION, carveoutHome, checkProject, cutSlice, folderCameBack, patchProject, planBlockers, planSlice, readRecord, readRepoConfig, } from "./carve.js";
|
|
13
15
|
import { SLICE_BUDGET_DEFAULTS, TEST_LEFT_REASON, renderExposure, renderOutside, testsVerdict, } from "./core.js";
|
|
14
16
|
import { gateRepository } from "./gate.js";
|
|
15
17
|
import { impactOf, runImpactTests } from "./impact.js";
|
|
16
|
-
import { applyPatch, mergeProject } from "./mergeback.js";
|
|
18
|
+
import { applyPatch, mergeProject, refreshProject, widenProject } from "./mergeback.js";
|
|
17
19
|
import { statusProject } from "./status.js";
|
|
20
|
+
import { hasAdvice, renderReadiness } from "./readiness.js";
|
|
21
|
+
import { trustedCut, writeReceipt } from "./receipt.js";
|
|
22
|
+
import { runAccount } from "./account.js";
|
|
18
23
|
const USAGE = `carveout ${CARVEOUT_VERSION}: carve a slice out of a TypeScript repository.
|
|
19
24
|
|
|
25
|
+
carveout handoff <entry>... --out <dir> [--task <file|text>] [--depth N] [--max-files N]
|
|
26
|
+
[--private <path>]... [--name <word>]... [--copy <dir>]... [--live <pkg>]...
|
|
27
|
+
[--accept <test>...] [--no-whole-package] [--no-copy-pure] [--receipt <file>]
|
|
28
|
+
[--max-exposure-lines N] [--max-exposure-percent P] [--json]
|
|
29
|
+
The hand-off in one step: a cut with the tests that run the code, its
|
|
30
|
+
whole package and the pure code it runs, for someone outside (public,
|
|
31
|
+
in pseudonyms), and its check. Prints whether the folder is ready (its
|
|
32
|
+
tests pass before any edit), what it shows, the receipt to keep, and
|
|
33
|
+
what to send. --task writes TASK.md: the work, and when it is accepted.
|
|
34
|
+
Exits 0 only when the folder is ready to hand over.
|
|
35
|
+
|
|
20
36
|
carveout plan <entry>... [--repo <dir>] [--max-files N] [--depth N]
|
|
21
37
|
[--with-tests] [--whole-package] [--follow-workspaces]
|
|
22
|
-
[--copy-pure] [--private <path>]... [--
|
|
38
|
+
[--copy-pure] [--private <path>]... [--max-exposure-lines N]
|
|
39
|
+
[--max-exposure-percent P] [--json]
|
|
23
40
|
What a cut would hold, from the import graph alone. Writes nothing.
|
|
41
|
+
--max-exposure-lines and --max-exposure-percent (or maxExposureLines
|
|
42
|
+
and maxExposurePercent in carveout.config.json) bound the lines a
|
|
43
|
+
folder shows, and the share of the repository's TypeScript lines; a
|
|
44
|
+
cut past either is written with an exposure_exceeded blocker.
|
|
24
45
|
|
|
25
46
|
carveout cut <entry>... --out <dir> [--repo <dir>] [--max-files N] [--depth N]
|
|
26
47
|
[--with-tests] [--whole-package] [--follow-workspaces]
|
|
27
48
|
[--copy-pure] [--private <path>]... [--copy <dir>]...
|
|
28
|
-
[--mock <package>]...
|
|
49
|
+
[--max-exposure-lines N] [--max-exposure-percent P] [--mock <package>]...
|
|
29
50
|
[--install <package>]... [--live <package>]... [--offline]
|
|
30
|
-
[--widen-literals] [--public] [--
|
|
51
|
+
[--widen-literals] [--public] [--name <word>]... [--keep-names] [--task <file|text>]
|
|
52
|
+
[--receipt <file> | --no-receipt] [--json]
|
|
31
53
|
Copies the slice to <dir> at its repository paths, with a .d.ts stub and a
|
|
32
54
|
recording mock for every module it imports from outside, mock packages for
|
|
33
55
|
what it does not install, package.json, tsconfig.json and a lockfile.
|
|
34
56
|
Writes nothing when a file it would write holds a credential. --public
|
|
35
57
|
makes a folder for someone outside: literals widened, and neither the
|
|
36
58
|
checkout's path nor the files outside the slice that import it named.
|
|
59
|
+
Writes the maintainer's receipt to ~/.carveout/receipts/<cut id>.json
|
|
60
|
+
(or --receipt <file>): the record the folder that comes back is judged
|
|
61
|
+
by, which a public folder's own record leaves the repository's names to.
|
|
62
|
+
A public folder is written in pseudonyms: the names the repository goes
|
|
63
|
+
by (its packages', its owner's, its remote's, --name and "names" in the
|
|
64
|
+
configuration) renamed under a table the receipt keeps, and patch
|
|
65
|
+
renames the folder back. --keep-names writes it in the repository's
|
|
66
|
+
names; a name left in the folder is a blocker either way.
|
|
37
67
|
|
|
38
|
-
carveout check <dir> [--in-place] [--isolated] [--
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
carveout check <dir> [--in-place] [--isolated] [--accept <test>...] [--allow <path>]...
|
|
69
|
+
[--no-sandbox] [--sandbox-image <image>] [--receipt <file>] [--json]
|
|
70
|
+
npm ci, npm run build and npm test in a scratch copy of <dir>. A folder
|
|
71
|
+
that came back (edited since the cut, or with no receipt to say it is
|
|
72
|
+
not) is read by the hand-off policy first: what patch keeps out of the
|
|
73
|
+
test side is put back as the cut wrote it, what the folder added is taken
|
|
74
|
+
out, and any other file it keeps out stops the check (exit 6) until
|
|
75
|
+
--allow names it. Then every step runs in a container with only the
|
|
76
|
+
copy mounted and no network but for npm ci (scripts off); --no-sandbox
|
|
77
|
+
runs it here as you. --isolated: a scratch HOME and npm cache and no
|
|
78
|
+
install scripts, and the same as a folder that came back.
|
|
42
79
|
|
|
43
|
-
carveout status <dir> [--repo <checkout>] [--json]
|
|
80
|
+
carveout status <dir> [--repo <checkout>] [--receipt <file>] [--json]
|
|
44
81
|
Whether <dir> still matches the repository, and what changed if not.
|
|
45
82
|
|
|
46
|
-
carveout patch <dir> [--repo <checkout>] [--allow <path>]... [--
|
|
83
|
+
carveout patch <dir> [--repo <checkout>] [--allow <path>]... [--receipt <file>]
|
|
84
|
+
[--output <file>] [--json]
|
|
47
85
|
The edits made in <dir> as a patch for the repository (git apply). Runs
|
|
48
86
|
none of the folder's code. Keeps out, and names, new files outside the
|
|
49
87
|
slice's packages, protected paths (CI, hooks, manifests, lockfiles,
|
|
50
88
|
environment) and deleted tests; --allow <path|glob> lets one through.
|
|
51
89
|
|
|
52
|
-
carveout apply <dir> <patch-file> [--repo <checkout>] [--stubs] [--json]
|
|
90
|
+
carveout apply <dir> <patch-file> [--repo <checkout>] [--stubs] [--receipt <file>] [--json]
|
|
53
91
|
Brings a patch for the repository at the cut's commit into <dir>, with its
|
|
54
92
|
imports rewritten as a cut writes them. --stubs also writes the stubs the
|
|
55
93
|
change needs from the modules the slice cuts.
|
|
56
94
|
|
|
95
|
+
carveout refresh <dir> [--resolved <path>...] [--repo <checkout>] [--receipt <file>] [--json]
|
|
96
|
+
The repository's commits since the cut, brought into the folder: the
|
|
97
|
+
change in the slice's packages and the modules it stubs, merged into
|
|
98
|
+
the folder's own edits (an overlap is left, and named: exit 7).
|
|
99
|
+
|
|
100
|
+
carveout widen <dir> --entry <path>... [--out <dir>] [--depth N] [--max-files N]
|
|
101
|
+
[--allow <path>]... [--receipt <file>] [--json]
|
|
102
|
+
The folder cut again with more entry points, every other bound as it
|
|
103
|
+
was, and the contributor's work so far carried into it: for a change
|
|
104
|
+
that needs a module the slice cut. A public folder keeps its names.
|
|
105
|
+
|
|
57
106
|
carveout merge <dir> --into <checkout> [--verify <command>] [--verify-env <name>]...
|
|
58
|
-
[--
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
107
|
+
[--verify-setup <command>] [--no-sandbox] [--sandbox-image <image>]
|
|
108
|
+
[--allow <path>]... [--receipt <file>] [--json]
|
|
109
|
+
patch, then git apply --3way in <checkout>, then <command> in a
|
|
110
|
+
container on a copy of the checkout (tracked and unignored files): the
|
|
111
|
+
install the lockfile names (or --verify-setup), scripts off, with a
|
|
112
|
+
network; then <command> with none, and only the environment variables
|
|
113
|
+
named. --no-sandbox runs it at the checkout's root as you. Applies
|
|
114
|
+
nothing while the hand-off policy keeps a file out, or when the
|
|
115
|
+
container cannot run.
|
|
62
116
|
|
|
63
117
|
carveout gate [--repo <dir>] [--base <ref>] [--scope <path>]... [--allow <path>]...
|
|
64
118
|
[--hook] [--json]
|
|
@@ -82,6 +136,16 @@ const USAGE = `carveout ${CARVEOUT_VERSION}: carve a slice out of a TypeScript r
|
|
|
82
136
|
carveout where
|
|
83
137
|
The folder fetched package types are cached in (CARVEOUT_HOME).
|
|
84
138
|
|
|
139
|
+
carveout login [--platform <url>] [--org <handle|id>] [--open]
|
|
140
|
+
carveout whoami [--platform <url>]
|
|
141
|
+
carveout logout [--platform <url>]
|
|
142
|
+
Optional: sign in to the Lunox platform (https://platform.lunox.work, or
|
|
143
|
+
LUNOX_PLATFORM_URL) with a code approved in the browser, and keep a
|
|
144
|
+
workspace token in ~/.lunox/credentials (LUNOX_CREDENTIALS), shared with
|
|
145
|
+
duediligence; whoami prints the account, workspace and token expiry;
|
|
146
|
+
logout deletes the token here (the platform cannot revoke it from the
|
|
147
|
+
CLI). No other command talks to the platform.
|
|
148
|
+
|
|
85
149
|
An entry point is a file or a directory, relative to --repo (default: the
|
|
86
150
|
current directory, which should be the repository's root). Defaults:
|
|
87
151
|
--max-files ${SLICE_BUDGET_DEFAULTS.maxFiles}, --depth ${SLICE_BUDGET_DEFAULTS.maxDepth}.
|
|
@@ -97,6 +161,67 @@ or folder (or glob) out of every slice: stubbed wherever reached, literals
|
|
|
97
161
|
widened. --copy <dir> copies a folder the code reads by name (migrations).
|
|
98
162
|
A carveout.config.json at the repository's root sets the same for every cut.
|
|
99
163
|
`;
|
|
164
|
+
/**
|
|
165
|
+
* A run's counts as a sentence: the folder's tests (node:test's and
|
|
166
|
+
* Vitest's), then carveout's own checks of the public surface, which are
|
|
167
|
+
* no test of the work's.
|
|
168
|
+
*/
|
|
169
|
+
function countsSentence(counts, edited) {
|
|
170
|
+
if (counts === undefined)
|
|
171
|
+
return "";
|
|
172
|
+
// A runner with no test of the folder's (node:test's only file the interface test) is left out.
|
|
173
|
+
const runners = Object.entries(counts.runners ?? {}).filter(([, each]) => each.tests > 0);
|
|
174
|
+
const label = (runner) => (runner === "vitest" ? "Vitest" : runner);
|
|
175
|
+
const by = runners.length > 1
|
|
176
|
+
? ` (${runners.map(([runner, each]) => `${label(runner)} ${each.pass} of ${each.tests}`).join(", ")})`
|
|
177
|
+
: runners.length === 1
|
|
178
|
+
? ` (${label(runners[0]?.[0] ?? "")})`
|
|
179
|
+
: "";
|
|
180
|
+
const tests = counts.tests === 0 && counts.surface !== undefined
|
|
181
|
+
? ""
|
|
182
|
+
: ` ${counts.pass} of ${counts.tests} test(s) pass${edited ? "" : " before any edit"}${by}.`;
|
|
183
|
+
const surface = counts.surface === undefined
|
|
184
|
+
? ""
|
|
185
|
+
: ` ${counts.surface.checks - counts.surface.fail} of ${counts.surface.checks} check(s) that the public surface keeps its exports and types hold.`;
|
|
186
|
+
return `${tests}${surface}`;
|
|
187
|
+
}
|
|
188
|
+
/** What a check says of a folder as a hand-off: whether "make the tests pass" is a task someone can finish there. */
|
|
189
|
+
function verdictOf(steps, record) {
|
|
190
|
+
const own = steps.filter((step) => step.command !== "accept");
|
|
191
|
+
const ok = own.length === 3 && own.every((step) => step.ok);
|
|
192
|
+
const testStep = steps.find((step) => step.command === "npm test");
|
|
193
|
+
const baseline = testStep?.baselineInEffect !== undefined && testStep.baselineInEffect.length > 0 ? testStep.baselineInEffect : undefined;
|
|
194
|
+
const counts = testStep?.counts;
|
|
195
|
+
const carried = record.tests?.carried.length ?? 0;
|
|
196
|
+
return {
|
|
197
|
+
ok,
|
|
198
|
+
// A cut with blockers (a name left in the folder, a stub it could not
|
|
199
|
+
// write) is not ready whatever its tests say.
|
|
200
|
+
handoff: !record.ready ? "blocked" : !ok ? "failed" : baseline !== undefined ? "red-baseline" : carried === 0 ? "no-tests" : "ready",
|
|
201
|
+
...(counts === undefined ? {} : { counts }),
|
|
202
|
+
...(baseline === undefined ? {} : { baseline }),
|
|
203
|
+
carried,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** The owner's acceptance tests, by their paths in the repository (`--accept`). */
|
|
207
|
+
function acceptOption(files, repo) {
|
|
208
|
+
const root = resolve(repo ?? ".");
|
|
209
|
+
return (files ?? []).map((file) => {
|
|
210
|
+
const at = resolve(file);
|
|
211
|
+
const path = relative(root, at).split("\\").join("/");
|
|
212
|
+
if (path.startsWith("..") || !existsSync(at))
|
|
213
|
+
fail(`--accept ${file}: give a test file inside the repository (run from its root, or pass --repo), at the path it would have there.`);
|
|
214
|
+
return { path, text: readFileSync(at, "utf8") };
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/** What a command wrote into a folder in pseudonyms that still names the repository: for the maintainer, in full. */
|
|
218
|
+
function leaksText(leaks) {
|
|
219
|
+
return `The folder now holds names the repository goes by; do not send it as it is (rename them, or cut again):\n${list(leaks.map(({ term, files }) => `${term}: ${files.join(", ")}`))}\n`;
|
|
220
|
+
}
|
|
221
|
+
/** Slice files a command left unwritten: in the folder's names they would not map back. */
|
|
222
|
+
function unmappedText(paths) {
|
|
223
|
+
return `Not written, left as the folder had them: in the folder's names their text would not map back to the repository's byte for byte (a mixed-case name, a placeholder word the repository's text holds), so a patch would rewrite it. Cut again to take them:\n${list(paths)}\n`;
|
|
224
|
+
}
|
|
100
225
|
function fail(message) {
|
|
101
226
|
process.stderr.write(`carveout: ${message}\n`);
|
|
102
227
|
process.exit(1);
|
|
@@ -123,9 +248,16 @@ async function main(argv) {
|
|
|
123
248
|
process.stdout.write(`${CARVEOUT_VERSION}\n`);
|
|
124
249
|
return;
|
|
125
250
|
}
|
|
251
|
+
if (commandName === "login" || commandName === "whoami" || commandName === "logout") {
|
|
252
|
+
process.exitCode = await runAccount(commandName, rest, { client: "carveout", version: CARVEOUT_VERSION });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
126
255
|
const { values, positionals } = parseArgs({
|
|
127
256
|
args: [...rest],
|
|
128
257
|
allowPositionals: true,
|
|
258
|
+
// `--no-whole-package`, `--no-copy-pure`, `--no-public`: handoff's
|
|
259
|
+
// defaults turned off by name.
|
|
260
|
+
allowNegative: true,
|
|
129
261
|
options: {
|
|
130
262
|
repo: { type: "string" },
|
|
131
263
|
out: { type: "string" },
|
|
@@ -141,23 +273,45 @@ async function main(argv) {
|
|
|
141
273
|
"whole-package": { type: "boolean" },
|
|
142
274
|
"follow-workspaces": { type: "boolean" },
|
|
143
275
|
"copy-pure": { type: "boolean" },
|
|
276
|
+
"max-exposure-lines": { type: "string" },
|
|
277
|
+
"max-exposure-percent": { type: "string" },
|
|
144
278
|
private: { type: "string", multiple: true },
|
|
145
279
|
copy: { type: "string", multiple: true },
|
|
146
280
|
"in-place": { type: "boolean" },
|
|
281
|
+
accept: { type: "string", multiple: true },
|
|
282
|
+
resolved: { type: "string", multiple: true },
|
|
147
283
|
stubs: { type: "boolean" },
|
|
148
284
|
into: { type: "string" },
|
|
149
285
|
verify: { type: "string" },
|
|
150
286
|
"verify-env": { type: "string", multiple: true },
|
|
287
|
+
"verify-setup": { type: "string" },
|
|
151
288
|
allow: { type: "string", multiple: true },
|
|
152
289
|
public: { type: "boolean" },
|
|
153
290
|
isolated: { type: "boolean" },
|
|
291
|
+
sandbox: { type: "boolean" },
|
|
292
|
+
"sandbox-image": { type: "string" },
|
|
154
293
|
base: { type: "string" },
|
|
155
294
|
scope: { type: "string", multiple: true },
|
|
156
295
|
hook: { type: "boolean" },
|
|
157
296
|
run: { type: "boolean" },
|
|
158
297
|
json: { type: "boolean" },
|
|
298
|
+
receipt: { type: "string" },
|
|
299
|
+
"no-receipt": { type: "boolean" },
|
|
300
|
+
name: { type: "string", multiple: true },
|
|
301
|
+
"keep-names": { type: "boolean" },
|
|
302
|
+
task: { type: "string" },
|
|
303
|
+
entry: { type: "string", multiple: true },
|
|
159
304
|
},
|
|
160
305
|
});
|
|
306
|
+
// With negated flags read, `--no-receipt` comes back as `receipt: false`
|
|
307
|
+
// rather than as its own flag: the same thing, said one way.
|
|
308
|
+
{
|
|
309
|
+
const parsed = values;
|
|
310
|
+
if (parsed["receipt"] === false) {
|
|
311
|
+
delete parsed["receipt"];
|
|
312
|
+
parsed["no-receipt"] = true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
161
315
|
const budget = {
|
|
162
316
|
maxFiles: integer(values["max-files"], "max-files", SLICE_BUDGET_DEFAULTS.maxFiles),
|
|
163
317
|
maxDepth: integer(values.depth, "depth", SLICE_BUDGET_DEFAULTS.maxDepth),
|
|
@@ -170,12 +324,64 @@ async function main(argv) {
|
|
|
170
324
|
* else the record's, or the current directory when the record names none
|
|
171
325
|
* (a public cut). The library checks it holds the cut's commit.
|
|
172
326
|
*/
|
|
173
|
-
const repoFor = (dir) =>
|
|
327
|
+
const repoFor = (dir) => {
|
|
328
|
+
if (values.repo !== undefined)
|
|
329
|
+
return repo;
|
|
330
|
+
let named;
|
|
331
|
+
try {
|
|
332
|
+
named = trustedCut(dir, values.receipt, { optional: true }).record.repo;
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
named = readRecord(dir).repo;
|
|
336
|
+
}
|
|
337
|
+
return named === undefined ? repo : undefined;
|
|
338
|
+
};
|
|
339
|
+
/** `--mock`, `--install` and `--live` as the cut's dependency choices, refused when they clash. */
|
|
340
|
+
const dependencyChoices = () => {
|
|
341
|
+
const choices = {};
|
|
342
|
+
for (const name of values.mock ?? [])
|
|
343
|
+
choices[name] = "runtime-mock";
|
|
344
|
+
for (const name of values.install ?? []) {
|
|
345
|
+
if (choices[name] !== undefined)
|
|
346
|
+
fail(`${name} is given to both --mock and --install.`);
|
|
347
|
+
choices[name] = "approved-package";
|
|
348
|
+
}
|
|
349
|
+
for (const name of values.live ?? [])
|
|
350
|
+
if (choices[name] === "runtime-mock")
|
|
351
|
+
fail(`${name} is given to both --mock and --live.`);
|
|
352
|
+
return choices;
|
|
353
|
+
};
|
|
354
|
+
/** `--task`: a file's text, or the text given. */
|
|
355
|
+
const taskOption = () => values.task === undefined
|
|
356
|
+
? {}
|
|
357
|
+
: { task: existsSync(values.task) && statSync(values.task).isFile() ? readFileSync(values.task, "utf8") : values.task };
|
|
358
|
+
/** The receipt a folder's command was given, as an option; or none, by `--no-receipt`. */
|
|
359
|
+
const receiptOption = {
|
|
360
|
+
...(values.receipt === undefined ? {} : { receipt: values.receipt }),
|
|
361
|
+
...(values["no-receipt"] === true ? { noReceipt: true } : {}),
|
|
362
|
+
};
|
|
363
|
+
/** `--max-exposure-lines` and `--max-exposure-percent`: over the configuration's. */
|
|
364
|
+
const maxExposure = (() => {
|
|
365
|
+
const positive = (value, name) => {
|
|
366
|
+
if (value === undefined)
|
|
367
|
+
return undefined;
|
|
368
|
+
const parsed = Number(value);
|
|
369
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
370
|
+
fail(`--${name} takes a number above 0, not ${value}.`);
|
|
371
|
+
return parsed;
|
|
372
|
+
};
|
|
373
|
+
const lines = positive(values["max-exposure-lines"], "max-exposure-lines");
|
|
374
|
+
const percent = positive(values["max-exposure-percent"], "max-exposure-percent");
|
|
375
|
+
return lines === undefined && percent === undefined
|
|
376
|
+
? {}
|
|
377
|
+
: { maxExposure: { ...(lines === undefined ? {} : { lines }), ...(percent === undefined ? {} : { percent }) } };
|
|
378
|
+
})();
|
|
174
379
|
const bounds = {
|
|
175
380
|
...(values["whole-package"] === true ? { wholePackage: true } : {}),
|
|
176
381
|
...(values["follow-workspaces"] === true ? { followWorkspaces: true } : {}),
|
|
177
382
|
...(values["copy-pure"] === true ? { copyPure: true } : {}),
|
|
178
383
|
...(values.private === undefined ? {} : { private: values.private }),
|
|
384
|
+
...maxExposure,
|
|
179
385
|
};
|
|
180
386
|
switch (commandName) {
|
|
181
387
|
case "where":
|
|
@@ -213,6 +419,7 @@ async function main(argv) {
|
|
|
213
419
|
testsVerdict: testsVerdict(plan.walk),
|
|
214
420
|
originOf: plan.walk.originOf,
|
|
215
421
|
exposure: plan.exposure,
|
|
422
|
+
exposureBudget: plan.exposureBudget ?? null,
|
|
216
423
|
cutModules: plan.cutModules,
|
|
217
424
|
typeOnlyCuts: plan.typeOnlyCuts,
|
|
218
425
|
inbound: [...new Set(plan.walk.cuts.inbound.map((e) => e.from))].sort(),
|
|
@@ -228,12 +435,21 @@ async function main(argv) {
|
|
|
228
435
|
const importers = [
|
|
229
436
|
...new Set(plan.walk.cuts.inbound.map((edge) => edge.from)),
|
|
230
437
|
].sort();
|
|
231
|
-
const
|
|
232
|
-
const readsCut = plan.walk.cuts.outbound.filter((edge) => edge.relation === "reads");
|
|
438
|
+
const blockers = planBlockers(plan);
|
|
233
439
|
process.stdout.write([
|
|
234
440
|
`${plan.walk.included.length} files from ${plan.entryPoints.join(", ")} (budget ${plan.budget.maxFiles} files, depth ${plan.budget.maxDepth}${plan.wholePackage ? ", the whole package" : ""}${plan.followWorkspaces ? ", workspace packages followed" : ""}${plan.copyPure ? ", pure code copied" : ""}; ${plan.graphFiles} script files read)${atLimit ? ", at the file limit: raise --max-files or narrow the entry points" : ""}.`,
|
|
235
441
|
"",
|
|
236
442
|
`Shows ${renderExposure(plan.exposure)}`,
|
|
443
|
+
...(plan.exposureBudget === undefined
|
|
444
|
+
? []
|
|
445
|
+
: [
|
|
446
|
+
plan.exposureBudget.over === null
|
|
447
|
+
? `Within the owner's bound on what a folder shows (${[
|
|
448
|
+
...(plan.exposureBudget.lines === undefined ? [] : [`${plan.exposureBudget.lines.toLocaleString("en-US")} lines`]),
|
|
449
|
+
...(plan.exposureBudget.percent === undefined ? [] : [`${plan.exposureBudget.percent}%; this is ${plan.exposureBudget.shownPercent ?? 0}%`]),
|
|
450
|
+
].join(", ")}).`
|
|
451
|
+
: `Past the owner's bound on what a folder shows: ${plan.exposureBudget.over}. A cut would be written with an exposure_exceeded blocker, never ready.`,
|
|
452
|
+
]),
|
|
237
453
|
"",
|
|
238
454
|
...(renderOutside(plan.exposure).length === 0 ? [] : [...renderOutside(plan.exposure), ""]),
|
|
239
455
|
"Included:",
|
|
@@ -293,26 +509,150 @@ async function main(argv) {
|
|
|
293
509
|
...plan.externals.packages.map((item) => `${item.specifier} (${item.service})`),
|
|
294
510
|
...plan.externals.environment.map((item) => `$${item.name}`),
|
|
295
511
|
]),
|
|
296
|
-
...(
|
|
297
|
-
broken.length +
|
|
298
|
-
readsCut.length +
|
|
299
|
-
plan.secretFiles.length ===
|
|
300
|
-
0
|
|
301
|
-
? []
|
|
302
|
-
: [
|
|
303
|
-
"",
|
|
304
|
-
"Blockers:",
|
|
305
|
-
list([
|
|
306
|
-
...plan.walk.unknownEntryPoints.map((entry) => `${entry}: names no script file in the repository`),
|
|
307
|
-
...broken.map((dependency) => `${dependency.file}${dependency.location === null ? "" : `:${dependency.location.slice(1)}`}: ${dependency.status === "dynamic" ? "dynamic import of a computed name" : dependency.status === "path" ? `reads ${dependency.specifier ?? ""} by path, which a slice cannot copy` : `${dependency.specifier ?? ""} resolves to no file`}`),
|
|
308
|
-
...readsCut.map((edge) => `${edge.from}${edge.location === null ? "" : `:${edge.location.slice(1)}`}: reads ${edge.to} by path, and it is outside the slice`),
|
|
309
|
-
...plan.secretFiles.map((path) => `${path}: a credentials file is never copied`),
|
|
310
|
-
]),
|
|
311
|
-
]),
|
|
512
|
+
...(blockers.length === 0 ? [] : ["", "Blockers:", list(blockers)]),
|
|
312
513
|
"",
|
|
313
514
|
].join("\n"));
|
|
314
515
|
return;
|
|
315
516
|
}
|
|
517
|
+
case "handoff": {
|
|
518
|
+
if (positionals.length === 0)
|
|
519
|
+
fail("handoff needs at least one entry point.");
|
|
520
|
+
if (values.out === undefined)
|
|
521
|
+
fail("handoff needs --out <dir>.");
|
|
522
|
+
// The hand-off's own defaults, unless a flag or the owner's
|
|
523
|
+
// configuration says otherwise: the tests that run the code, its
|
|
524
|
+
// whole package, the pure code it runs, and a folder for someone
|
|
525
|
+
// outside, in pseudonyms.
|
|
526
|
+
const config = readRepoConfig(repo);
|
|
527
|
+
// The owner's acceptance tests are never sent: a test that imports the
|
|
528
|
+
// slice would otherwise come with --with-tests, from the working tree.
|
|
529
|
+
const accepted = acceptOption(values.accept, values.repo);
|
|
530
|
+
const result = await cutSlice({
|
|
531
|
+
repo,
|
|
532
|
+
out: values.out,
|
|
533
|
+
entryPoints: positionals,
|
|
534
|
+
budget,
|
|
535
|
+
choices: dependencyChoices(),
|
|
536
|
+
...(values.live === undefined ? {} : { live: values.live }),
|
|
537
|
+
tests: values["with-tests"] ?? true,
|
|
538
|
+
wholePackage: values["whole-package"] ?? config.wholePackage ?? true,
|
|
539
|
+
copyPure: values["copy-pure"] ?? config.copyPure ?? true,
|
|
540
|
+
...(values["follow-workspaces"] === true ? { followWorkspaces: true } : {}),
|
|
541
|
+
...(values.private === undefined ? {} : { private: values.private }),
|
|
542
|
+
...maxExposure,
|
|
543
|
+
...(accepted.length === 0 ? {} : { unseen: accepted.map((test) => test.path) }),
|
|
544
|
+
public: values.public ?? config.public ?? true,
|
|
545
|
+
...(values.offline === true ? { offline: true } : {}),
|
|
546
|
+
...(values.copy === undefined ? {} : { copy: values.copy }),
|
|
547
|
+
...(values["no-receipt"] === true ? { receipt: false } : values.receipt === undefined ? {} : { receipt: values.receipt }),
|
|
548
|
+
...(values.name === undefined ? {} : { names: values.name }),
|
|
549
|
+
...(values["keep-names"] === true ? { pseudonyms: false } : {}),
|
|
550
|
+
...taskOption(),
|
|
551
|
+
log: (line) => {
|
|
552
|
+
if (values.json !== true)
|
|
553
|
+
process.stderr.write(`${line}\n`);
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
const steps = await checkProject(result.out, {
|
|
557
|
+
...(result.receipt === null ? { noReceipt: true } : { receipt: result.receipt }),
|
|
558
|
+
});
|
|
559
|
+
// The owner's acceptance tests on the folder as it goes out: they
|
|
560
|
+
// should not pass before the work, or they test nothing of it.
|
|
561
|
+
const preSteps = accepted.length === 0 || steps.some((step) => !step.ok)
|
|
562
|
+
? null
|
|
563
|
+
: await checkProject(result.out, {
|
|
564
|
+
...(result.receipt === null ? { noReceipt: true } : { receipt: result.receipt }),
|
|
565
|
+
accept: accepted,
|
|
566
|
+
});
|
|
567
|
+
// The acceptance tests' own verdict: the folder's tests are judged apart.
|
|
568
|
+
const vacuous = preSteps === null ? null : preSteps.find((step) => step.command === "accept")?.ok === true;
|
|
569
|
+
// A build that fails on an acceptance test itself (an import it makes
|
|
570
|
+
// that the folder lacks) is no failure of the work's.
|
|
571
|
+
const unbuilt = preSteps === null || preSteps.some((step) => step.command === "accept")
|
|
572
|
+
? undefined
|
|
573
|
+
: preSteps
|
|
574
|
+
.find((step) => !step.ok)
|
|
575
|
+
?.output.split("\n")
|
|
576
|
+
// A module it imports that is not there (TS2307), not a name
|
|
577
|
+
// the work is yet to add (TS2305, TS2339: failing as it should).
|
|
578
|
+
.find((line) => /TS2307/.test(line) && accepted.some((test) => line.includes(test.path)));
|
|
579
|
+
const verdict = verdictOf(steps, result.record);
|
|
580
|
+
const record = result.record;
|
|
581
|
+
// The step that failed, and what it printed last: what to fix first.
|
|
582
|
+
const failedStep = steps.find((step) => !step.ok);
|
|
583
|
+
if (values.json === true)
|
|
584
|
+
print({
|
|
585
|
+
out: result.out,
|
|
586
|
+
ready: verdict.handoff === "ready",
|
|
587
|
+
handoff: verdict.handoff,
|
|
588
|
+
counts: verdict.counts ?? null,
|
|
589
|
+
baseline: verdict.baseline ?? [],
|
|
590
|
+
blockers: record.blockers,
|
|
591
|
+
exposure: record.exposure ?? null,
|
|
592
|
+
pseudonyms: result.pseudonyms === null ? null : { renames: result.pseudonyms.rules.length, kept: result.pseudonyms.kept },
|
|
593
|
+
readiness: result.readiness,
|
|
594
|
+
receipt: result.receipt,
|
|
595
|
+
// The owner's acceptance tests on the folder as it goes out.
|
|
596
|
+
acceptance: vacuous === null ? null : vacuous ? "pass-before-the-work" : "fail-before-the-work",
|
|
597
|
+
steps,
|
|
598
|
+
});
|
|
599
|
+
else {
|
|
600
|
+
const lines = [
|
|
601
|
+
`Hand-off folder: ${result.out}`,
|
|
602
|
+
` Verdict: ${!record.ready
|
|
603
|
+
? "not ready: the cut has blockers (below)"
|
|
604
|
+
: verdict.handoff === "ready"
|
|
605
|
+
? "ready: it installs, builds, and its tests pass before any edit"
|
|
606
|
+
: verdict.handoff === "red-baseline"
|
|
607
|
+
? `its tests run, but ${verdict.baseline?.length ?? 0} fail on the mocks before any edit (not counted)`
|
|
608
|
+
: verdict.handoff === "no-tests"
|
|
609
|
+
? "it builds, but no test of this code came with it: write the tests the work must pass"
|
|
610
|
+
: `it does not ${failedStep === undefined ? "install, build or test" : failedStep.command} on its own (below)`}`,
|
|
611
|
+
...(verdict.counts === undefined
|
|
612
|
+
? []
|
|
613
|
+
: [` ${verdict.counts.tests === 0 && verdict.counts.surface !== undefined ? "Checks" : "Tests"}:${countsSentence(verdict.counts, false)}`]),
|
|
614
|
+
...(record.exposure === undefined ? [] : [` Shows ${renderExposure(record.exposure)}`]),
|
|
615
|
+
` Names: ${result.pseudonyms === null
|
|
616
|
+
? record.request.public === true
|
|
617
|
+
? "in the repository's own (--keep-names or no receipt)"
|
|
618
|
+
: "in the repository's own (not public)"
|
|
619
|
+
: result.pseudonyms.rules.length === 0
|
|
620
|
+
? "none of the repository's is in the folder"
|
|
621
|
+
: `${result.pseudonyms.rules.length} rename(s) hide the repository's; the receipt keeps the table`}.`,
|
|
622
|
+
...(record.blockers.length === 0
|
|
623
|
+
? []
|
|
624
|
+
: ["", "Blockers:", list(record.blockers.map((b) => `${b.code}${b.file === null ? "" : ` ${b.file}`}: ${b.detail}`))]),
|
|
625
|
+
...(verdict.handoff === "red-baseline" && hasAdvice(result.readiness)
|
|
626
|
+
? ["", "To run those on real code, cut again with:", ...renderReadiness(result.readiness)]
|
|
627
|
+
: []),
|
|
628
|
+
...(failedStep === undefined
|
|
629
|
+
? []
|
|
630
|
+
: ["", `${failedStep.command} failed; its last lines:`, ...failedStep.output.trim().split("\n").slice(-25).map((line) => ` ${line}`)]),
|
|
631
|
+
...(vacuous === null
|
|
632
|
+
? []
|
|
633
|
+
: [
|
|
634
|
+
"",
|
|
635
|
+
vacuous
|
|
636
|
+
? `The owner's ${accepted.length} acceptance test file(s) already pass on the folder as it goes out: they test nothing of the work (or it is done). Make them fail before it.`
|
|
637
|
+
: unbuilt !== undefined
|
|
638
|
+
? `The owner's acceptance tests do not build in the folder, whatever the work does: ${unbuilt.trim()}. Import what they test by a relative path the folder holds, and run handoff again.`
|
|
639
|
+
: `The owner's ${accepted.length} acceptance test file(s) fail on the folder as it goes out, as they should before the work; keep them, and run them on the folder that comes back with carveout check --accept.`,
|
|
640
|
+
]),
|
|
641
|
+
"",
|
|
642
|
+
result.receipt === null
|
|
643
|
+
? "No receipt: the folder that comes back will be judged by its own record (--no-receipt on patch)."
|
|
644
|
+
: `Receipt: ${result.receipt}\n Keep it, and never send it: the folder that comes back is judged by it.`,
|
|
645
|
+
`Send the folder without node_modules: (cd ${dirname(result.out)} && zip -qr ${basename(result.out)}.zip ${basename(result.out)} -x '*/node_modules/*')`,
|
|
646
|
+
"When it comes back, from the repository's root: carveout patch <returned folder> --output change.patch; read all of it; then git apply --3way change.patch on a branch, and let CI decide.",
|
|
647
|
+
"",
|
|
648
|
+
];
|
|
649
|
+
process.stdout.write(lines.join("\n"));
|
|
650
|
+
}
|
|
651
|
+
// Acceptance tests that pass before the work would accept anything.
|
|
652
|
+
if (!record.ready || verdict.handoff !== "ready" || vacuous === true)
|
|
653
|
+
process.exitCode = 2;
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
316
656
|
case "cut": {
|
|
317
657
|
if (positionals.length === 0)
|
|
318
658
|
fail("cut needs at least one entry point.");
|
|
@@ -342,6 +682,10 @@ async function main(argv) {
|
|
|
342
682
|
...(values.public === true ? { public: true } : {}),
|
|
343
683
|
...bounds,
|
|
344
684
|
...(values.copy === undefined ? {} : { copy: values.copy }),
|
|
685
|
+
...(values["no-receipt"] === true ? { receipt: false } : values.receipt === undefined ? {} : { receipt: values.receipt }),
|
|
686
|
+
...(values.name === undefined ? {} : { names: values.name }),
|
|
687
|
+
...(values["keep-names"] === true ? { pseudonyms: false } : {}),
|
|
688
|
+
...taskOption(),
|
|
345
689
|
log: (line) => {
|
|
346
690
|
if (values.json !== true)
|
|
347
691
|
process.stderr.write(`${line}\n`);
|
|
@@ -357,6 +701,12 @@ async function main(argv) {
|
|
|
357
701
|
dependencies: record.dependencies,
|
|
358
702
|
optionalPeers: record.optionalPeers,
|
|
359
703
|
importRewrites: record.importRewrites,
|
|
704
|
+
tests: record.tests,
|
|
705
|
+
readiness: result.readiness,
|
|
706
|
+
receipt: result.receipt,
|
|
707
|
+
pseudonyms: result.pseudonyms === null
|
|
708
|
+
? null
|
|
709
|
+
: { renames: result.pseudonyms.rules.length, kept: result.pseudonyms.kept },
|
|
360
710
|
});
|
|
361
711
|
else {
|
|
362
712
|
const mocked = record.dependencies
|
|
@@ -393,6 +743,30 @@ async function main(argv) {
|
|
|
393
743
|
"Blockers:",
|
|
394
744
|
list(record.blockers.map((b) => `${b.code}${b.file === null ? "" : ` ${b.file}`}: ${b.detail}`)),
|
|
395
745
|
]),
|
|
746
|
+
...(result.pseudonyms === null
|
|
747
|
+
? []
|
|
748
|
+
: [
|
|
749
|
+
"",
|
|
750
|
+
result.pseudonyms.rules.length === 0
|
|
751
|
+
? `Pseudonyms: none of the ${result.pseudonyms.vocabulary.length} name(s) the repository goes by is in the folder; nothing was renamed.`
|
|
752
|
+
: `Pseudonyms: the folder holds the slice under ${result.pseudonyms.rules.length} rename(s) that hide the names the repository goes by (the receipt keeps the table; patch renames the folder back).${result.pseudonyms.kept.length === 0 ? "" : ` Kept, as npm installs them: ${result.pseudonyms.kept.join(", ")}.`}`,
|
|
753
|
+
]),
|
|
754
|
+
...(result.readiness.tests === 0
|
|
755
|
+
? [
|
|
756
|
+
"",
|
|
757
|
+
"No test of this code is in the folder: a pass means it installs, builds and keeps its exports. Cut with --with-tests, or write the tests the work must pass before handing it over.",
|
|
758
|
+
]
|
|
759
|
+
: hasAdvice(result.readiness)
|
|
760
|
+
? [
|
|
761
|
+
"",
|
|
762
|
+
"If check finds tests that fail on the mocks before any edit, these turn them toward real code:",
|
|
763
|
+
...renderReadiness(result.readiness),
|
|
764
|
+
]
|
|
765
|
+
: []),
|
|
766
|
+
"",
|
|
767
|
+
result.receipt === null
|
|
768
|
+
? "No receipt: patch, status and apply will judge the folder by its own record, checked against the repository."
|
|
769
|
+
: `Receipt: ${result.receipt}\n Keep it, and do not send it: patch, status, apply and merge judge the folder that comes back by it${record.request.public === true ? ", and the folder's own record leaves the repository's names to it" : ""}.`,
|
|
396
770
|
"",
|
|
397
771
|
`Next: carveout check ${values.out}`,
|
|
398
772
|
"",
|
|
@@ -407,14 +781,106 @@ async function main(argv) {
|
|
|
407
781
|
if (dir === undefined)
|
|
408
782
|
fail("check needs the folder a cut wrote.");
|
|
409
783
|
readRecord(dir);
|
|
784
|
+
const accept = acceptOption(values.accept, values.repo);
|
|
785
|
+
// Every later cut of the folder (widen, apply, refresh) reads them as
|
|
786
|
+
// absent, as handoff --accept records them.
|
|
787
|
+
if (accept.length > 0) {
|
|
788
|
+
const held = trustedCut(dir, values.receipt, { optional: true });
|
|
789
|
+
if (held.receipt !== null && held.receiptPath !== null) {
|
|
790
|
+
const unseen = [...new Set([...(held.receipt.unseen ?? []), ...accept.map((test) => test.path)])].sort();
|
|
791
|
+
if (unseen.length !== (held.receipt.unseen ?? []).length)
|
|
792
|
+
writeReceipt(held.receiptPath, { ...held.receipt, unseen });
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
// A folder that came back holds someone else's code: the hand-off
|
|
796
|
+
// policy reads it first, and it runs only in a container.
|
|
797
|
+
const cameBack = folderCameBack(dir, receiptOption);
|
|
798
|
+
const returned = cameBack || accept.length > 0 || values.isolated === true;
|
|
799
|
+
// What patch keeps out never runs (a weakened helper the acceptance
|
|
800
|
+
// tests import, a new test that reads the disk): read first, without
|
|
801
|
+
// running anything of the folder's.
|
|
802
|
+
const heldBack = !returned
|
|
803
|
+
? []
|
|
804
|
+
: await patchProject(dir, {
|
|
805
|
+
...(values.repo === undefined ? {} : { repo: resolve(values.repo) }),
|
|
806
|
+
...(values.allow === undefined ? {} : { allow: values.allow }),
|
|
807
|
+
...receiptOption,
|
|
808
|
+
})
|
|
809
|
+
.then((patch) => patch.refused.map((refusal) => refusal.path))
|
|
810
|
+
// Unread, nothing would be held back: the folder's own code
|
|
811
|
+
// would run unread.
|
|
812
|
+
.catch((error) => fail(`check reads what the patch of a folder that came back holds back before running any of it, and could not (run it at the repository's root, or pass --repo): ${error instanceof Error ? error.message : String(error)}`));
|
|
813
|
+
let sandbox;
|
|
814
|
+
if (values.sandbox === true || (values.sandbox !== false && returned)) {
|
|
815
|
+
sandbox = defaultSandbox(values["sandbox-image"]);
|
|
816
|
+
const why = sandboxUnavailable(sandbox);
|
|
817
|
+
if (why !== null)
|
|
818
|
+
fail(`A folder that came back runs only in a container, with no network but for the install and nothing of this machine's mounted but its copy; ${why} Or pass --no-sandbox to run its code as you, with your files and network.`);
|
|
819
|
+
}
|
|
410
820
|
const steps = await checkProject(dir, {
|
|
411
821
|
...(values["in-place"] === true ? { inPlace: true } : {}),
|
|
412
822
|
...(values.isolated === true ? { isolated: true } : {}),
|
|
823
|
+
...(accept.length === 0 ? {} : { accept }),
|
|
824
|
+
...(returned ? { held: heldBack, refuseHeld: true } : {}),
|
|
825
|
+
...(sandbox === undefined ? {} : { sandbox }),
|
|
826
|
+
...receiptOption,
|
|
413
827
|
});
|
|
414
|
-
const
|
|
828
|
+
const refusedStep = steps.find((step) => step.command === "policy");
|
|
829
|
+
if (refusedStep !== undefined) {
|
|
830
|
+
if (values.json === true)
|
|
831
|
+
print({ ok: false, handoff: "failed", policy: refusedStep.output, steps });
|
|
832
|
+
else
|
|
833
|
+
process.stderr.write(`carveout: ${refusedStep.output}\n`);
|
|
834
|
+
process.exitCode = 6;
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const own = steps.filter((step) => step.command !== "accept");
|
|
838
|
+
const ok = own.length === 3 && own.every((step) => step.ok);
|
|
839
|
+
const acceptStep = steps.find((step) => step.command === "accept");
|
|
840
|
+
const checked = trustedCut(dir, values.receipt, { optional: true }).record;
|
|
841
|
+
const testStep = steps.find((step) => step.command === "npm test");
|
|
842
|
+
const restoredStep = steps.find((step) => step.restored !== undefined);
|
|
843
|
+
const baselineOf = testStep?.baselineInEffect !== undefined && testStep.baselineInEffect.length > 0 ? testStep.baselineInEffect : undefined;
|
|
844
|
+
const baselineEdited = steps.find((step) => step.baselineEdited !== undefined)?.baselineEdited;
|
|
845
|
+
const counts = steps.find((step) => step.counts !== undefined)?.counts;
|
|
846
|
+
const carried = checked.tests?.carried.length ?? 0;
|
|
847
|
+
// Whether "make the tests pass" is a task someone can finish here; a
|
|
848
|
+
// cut with blockers (a name left in a public folder) is not, whatever
|
|
849
|
+
// its tests say.
|
|
850
|
+
const handoff = !checked.ready
|
|
851
|
+
? "blocked"
|
|
852
|
+
: !ok
|
|
853
|
+
? "failed"
|
|
854
|
+
: baselineOf !== undefined
|
|
855
|
+
? "red-baseline"
|
|
856
|
+
: carried === 0
|
|
857
|
+
? "no-tests"
|
|
858
|
+
: "ready";
|
|
415
859
|
if (values.json === true)
|
|
416
|
-
print({
|
|
860
|
+
print({
|
|
861
|
+
ok,
|
|
862
|
+
handoff,
|
|
863
|
+
counts: counts ?? null,
|
|
864
|
+
baseline: baselineOf ?? [],
|
|
865
|
+
baselineEdited: baselineEdited ?? [],
|
|
866
|
+
restored: restoredStep?.restored ?? [],
|
|
867
|
+
addedPackages: restoredStep?.addedPackages ?? [],
|
|
868
|
+
receipt: testStep?.receiptRead ?? false,
|
|
869
|
+
steps,
|
|
870
|
+
});
|
|
417
871
|
else {
|
|
872
|
+
if (restoredStep?.restored !== undefined)
|
|
873
|
+
process.stdout.write(`The folder's generated files were not the cut's; the check ran on the receipt's:\n${list(restoredStep.restored)}\n`);
|
|
874
|
+
if (restoredStep?.addedPackages !== undefined)
|
|
875
|
+
process.stdout.write(`The folder's package.json adds packages the repository does not install, so the check did not either:\n${list(restoredStep.addedPackages)}\nThe repository's package files are not patched: \`carveout patch\` names the file each one goes into, for the owner to add, or take them out.\n`);
|
|
876
|
+
if (testStep !== undefined && testStep.receiptRead === false && values["no-receipt"] !== true)
|
|
877
|
+
process.stdout.write("No receipt for this folder: its own carveout/baseline.json was not read, so every failing test counts.\n");
|
|
878
|
+
if (baselineEdited !== undefined)
|
|
879
|
+
process.stdout.write(`The folder's carveout/baseline.json names ${baselineEdited.length} test(s) the receipt's baseline does not; they were counted:\n${list(baselineEdited)}\n`);
|
|
880
|
+
if (sandbox !== undefined)
|
|
881
|
+
process.stdout.write(`Ran in a container (${sandbox.runtime}, ${sandbox.image}): only the check's copy mounted, no network but for the install (scripts off).${heldBack.length === 0 ? "" : ` patch keeps ${heldBack.length} file(s) out (${heldBack.join(", ")}): the tests, helpers and setup files among them ran as the cut wrote them, and none the folder added.`}\n`);
|
|
882
|
+
else if (cameBack)
|
|
883
|
+
process.stdout.write("Ran on this machine, as you (--no-sandbox): the folder's code had your files and network.\n");
|
|
418
884
|
for (const step of steps) {
|
|
419
885
|
process.stdout.write(`${step.ok ? "ok " : "FAIL"} ${step.command}\n`);
|
|
420
886
|
if (!step.ok || step.baseline !== undefined)
|
|
@@ -428,14 +894,45 @@ async function main(argv) {
|
|
|
428
894
|
process.stdout.write(record.optionalPeers.some((peer) => peer.name === name)
|
|
429
895
|
? `${name} is an optional peer the repository installs: cut again with --install ${name}.\n`
|
|
430
896
|
: `${name} is not installed: cut again with --install ${name} if the repository installs it, or --mock ${name}.\n`);
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
897
|
+
const edited = steps.some((step) => step.edited === true);
|
|
898
|
+
const passing = countsSentence(counts, edited);
|
|
899
|
+
const readiness = checked.readiness ?? trustedCut(dir, values.receipt, { optional: true }).receipt?.readiness;
|
|
900
|
+
const advice = readiness !== undefined && hasAdvice(readiness)
|
|
901
|
+
? ["To run them on real code instead, cut again with:", ...renderReadiness(readiness)]
|
|
902
|
+
: readiness !== undefined
|
|
903
|
+
? [
|
|
904
|
+
"Nothing a cut's flags change runs them on real code: they test what the slice cuts. Leave them out of the task, or widen the cut (more entries, or --depth) to hold what they test.",
|
|
905
|
+
]
|
|
906
|
+
: ["Cut again, on this machine, to see what would run them on real code (--copy-pure, --live, --copy)."];
|
|
907
|
+
process.stdout.write(`${[
|
|
908
|
+
handoff === "blocked"
|
|
909
|
+
? `Not ready to hand over: the cut has blockers (${checked.blockers.map((blocker) => blocker.code).join(", ")}); carveout status names them.${passing}`
|
|
910
|
+
: handoff === "failed"
|
|
911
|
+
? edited && steps.some((step) => step.command === "npm test" && !step.ok)
|
|
912
|
+
? "npm test fails on the folder's edits (above): what it lists failed after the edits and not before them, on the same mocks."
|
|
913
|
+
: "The slice does not stand on its own yet."
|
|
914
|
+
: handoff === "red-baseline"
|
|
915
|
+
? edited
|
|
916
|
+
? `The edited folder installs, builds and passes its tests, but for the ${baselineOf?.length ?? 0} test(s) that failed on the mocks before any edit (not counted). A pass here is a pass against mocks: the repository's CI decides.${passing}`
|
|
917
|
+
: `The slice installs, builds and tests on its own, but ${baselineOf?.length ?? 0} test(s) fail on the mocks before any edit (carveout/baseline.json): a contributor cannot make those pass here, so a fixed task should not rest on them.${passing}`
|
|
918
|
+
: handoff === "no-tests"
|
|
919
|
+
? `The slice installs, builds and tests on its own, but no test of this code came with it: a pass means it builds and keeps its exports. Cut with --with-tests, or write the tests the work must pass before handing it over.${passing}`
|
|
920
|
+
: edited
|
|
921
|
+
? `The edited folder installs, builds and passes its tests (${carried} test file(s) from the repository among them). A pass here is a pass against mocks: the repository's CI decides.${passing}`
|
|
922
|
+
: `Ready to hand over: the slice installs, builds, and its tests pass before any edit (${carried} test file(s) from the repository).${passing}`,
|
|
923
|
+
...(handoff === "red-baseline" ? advice : []),
|
|
924
|
+
...(accept.length === 0
|
|
925
|
+
? []
|
|
926
|
+
: [
|
|
927
|
+
acceptStep === undefined
|
|
928
|
+
? `The owner's ${accept.length} acceptance test file(s) did not run: the folder did not install or build (above).`
|
|
929
|
+
: acceptStep.ok
|
|
930
|
+
? `The owner's ${accept.length} acceptance test file(s) ran on their own, in the check's copy, and pass.${heldBack.length === 0 ? "" : ` patch keeps ${heldBack.length} file(s) out (${heldBack.join(", ")}): the run had the tests, helpers and setup files among them as the cut wrote them and none the folder added.`}`
|
|
931
|
+
: `The owner's acceptance tests do not pass on the folder that came back:\n${acceptStep.output}`,
|
|
932
|
+
]),
|
|
933
|
+
].join("\n")}\n`);
|
|
437
934
|
}
|
|
438
|
-
if (!ok)
|
|
935
|
+
if (!ok || (accept.length > 0 && acceptStep?.ok !== true))
|
|
439
936
|
process.exitCode = 2;
|
|
440
937
|
return;
|
|
441
938
|
}
|
|
@@ -446,6 +943,7 @@ async function main(argv) {
|
|
|
446
943
|
const given = repoFor(dir);
|
|
447
944
|
const status = await statusProject(dir, {
|
|
448
945
|
...(given === undefined ? {} : { repo: given }),
|
|
946
|
+
...receiptOption,
|
|
449
947
|
});
|
|
450
948
|
if (values.json === true)
|
|
451
949
|
print(status);
|
|
@@ -473,9 +971,13 @@ async function main(argv) {
|
|
|
473
971
|
const patch = await patchProject(dir, {
|
|
474
972
|
...(given === undefined ? {} : { repo: given }),
|
|
475
973
|
...(values.allow === undefined ? {} : { allow: values.allow }),
|
|
974
|
+
...receiptOption,
|
|
476
975
|
});
|
|
477
976
|
const boundary = new Set(patch.boundary.map((edit) => edit.path));
|
|
478
977
|
const notes = [
|
|
978
|
+
...(patch.recordFrom === "folder"
|
|
979
|
+
? ["no receipt for this folder: it was judged by its own carveout.json, checked against the repository (pass --receipt <file> if the cut wrote one elsewhere)."]
|
|
980
|
+
: []),
|
|
479
981
|
...patch.generatedEdits
|
|
480
982
|
.filter((path) => !boundary.has(path))
|
|
481
983
|
.map((path) => `${path} is generated; its edit is not in the patch.`),
|
|
@@ -490,6 +992,12 @@ async function main(argv) {
|
|
|
490
992
|
]),
|
|
491
993
|
...patch.noThreeWay.map((path) => `${path} was cut from uncommitted changes, so --3way cannot merge it; apply it by hand if git apply refuses.`),
|
|
492
994
|
...patch.deleted.map((path) => `deletes ${path}.`),
|
|
995
|
+
...(patch.renamed ?? []).map(({ from, to }) => `${from} is a new test where the repository already holds a file the slice did not (an acceptance test of yours, say); the patch adds it as ${to}, so both run.`),
|
|
996
|
+
...(patch.skipped.length === 0
|
|
997
|
+
? []
|
|
998
|
+
: [
|
|
999
|
+
`not in the patch, and not judged (the folder's own tests may run them): ${patch.skipped.join(", ")}.`,
|
|
1000
|
+
]),
|
|
493
1001
|
...patch.refused.map(({ path, reason }) => `kept out: ${path}, ${reason}.`),
|
|
494
1002
|
...(patch.sliceOnly.length === 0
|
|
495
1003
|
? []
|
|
@@ -536,6 +1044,7 @@ async function main(argv) {
|
|
|
536
1044
|
const applied = await applyPatch(dir, readFileSync(file, "utf8"), {
|
|
537
1045
|
...(values.stubs === true ? { stubs: true } : {}),
|
|
538
1046
|
...(given === undefined ? {} : { repo: given }),
|
|
1047
|
+
...receiptOption,
|
|
539
1048
|
});
|
|
540
1049
|
if (values.json === true)
|
|
541
1050
|
print(applied);
|
|
@@ -543,11 +1052,115 @@ async function main(argv) {
|
|
|
543
1052
|
process.stdout.write(`${applied.changed.length} changed, ${applied.added.length} new and ${applied.deleted.length} deleted slice files.\n`);
|
|
544
1053
|
if (applied.outside.length > 0)
|
|
545
1054
|
process.stdout.write(`Outside the slice, not applied:\n${list(applied.outside)}\n`);
|
|
1055
|
+
if (applied.merged.length > 0)
|
|
1056
|
+
process.stdout.write(`Merged into the folder's own edits:\n${list(applied.merged)}\n`);
|
|
1057
|
+
if (applied.conflicts.length > 0)
|
|
1058
|
+
process.stdout.write(`The folder's edits and the change overlap here; left as the folder had them, to merge by hand:\n${list(applied.conflicts)}\n`);
|
|
546
1059
|
if (applied.stubs.length > 0)
|
|
547
1060
|
process.stdout.write(`${applied.stubs.some((stub) => stub.written) ? "Stubs written" : "Stubs the change needs (apply with --stubs to write them)"}:\n${list(applied.stubs.map((stub) => `${stub.path}: ${stub.detail}`))}\n`);
|
|
1061
|
+
if (applied.leaks.length > 0)
|
|
1062
|
+
process.stdout.write(leaksText(applied.leaks));
|
|
1063
|
+
if (applied.unmapped.length > 0)
|
|
1064
|
+
process.stdout.write(unmappedText(applied.unmapped));
|
|
548
1065
|
}
|
|
549
1066
|
if (applied.stubs.some((stub) => !stub.written))
|
|
550
1067
|
process.exitCode = 5;
|
|
1068
|
+
if (applied.leaks.length > 0 || applied.unmapped.length > 0)
|
|
1069
|
+
process.exitCode = 2;
|
|
1070
|
+
if (applied.conflicts.length > 0)
|
|
1071
|
+
process.exitCode = 7;
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
case "refresh": {
|
|
1075
|
+
const [dir] = positionals;
|
|
1076
|
+
if (dir === undefined)
|
|
1077
|
+
fail("refresh needs the folder a cut wrote.");
|
|
1078
|
+
const given = repoFor(dir);
|
|
1079
|
+
const refreshed = await refreshProject(dir, {
|
|
1080
|
+
...(given === undefined ? {} : { repo: given }),
|
|
1081
|
+
...(values.resolved === undefined ? {} : { resolved: values.resolved }),
|
|
1082
|
+
...receiptOption,
|
|
1083
|
+
log: (line) => {
|
|
1084
|
+
if (values.json !== true)
|
|
1085
|
+
process.stderr.write(`${line}\n`);
|
|
1086
|
+
},
|
|
1087
|
+
});
|
|
1088
|
+
if (values.json === true)
|
|
1089
|
+
print(refreshed);
|
|
1090
|
+
else if (refreshed === null)
|
|
1091
|
+
process.stdout.write("Nothing the slice reads has moved since the cut.\n");
|
|
1092
|
+
else {
|
|
1093
|
+
const { applied } = refreshed;
|
|
1094
|
+
process.stdout.write([
|
|
1095
|
+
`Brought ${refreshed.from.slice(0, 8)}..${refreshed.to.slice(0, 8)} into the folder: ${applied.changed.length} changed, ${applied.added.length} new, ${applied.deleted.length} deleted.`,
|
|
1096
|
+
...(applied.merged.length === 0 ? [] : [`Merged into the folder's own edits:\n${list(applied.merged)}`]),
|
|
1097
|
+
...(applied.conflicts.length === 0
|
|
1098
|
+
? []
|
|
1099
|
+
: [
|
|
1100
|
+
`The folder's edits and the change overlap here; left as the folder had them, to merge by hand:\n${list(applied.conflicts)}\nMerge each by hand, then: carveout refresh ${dir} --resolved <path> (each), which takes the folder's text as resolved.`,
|
|
1101
|
+
]),
|
|
1102
|
+
...(applied.stubs.length === 0 ? [] : [`Stubs written:\n${list(applied.stubs.map((stub) => `${stub.path}: ${stub.detail}`))}`]),
|
|
1103
|
+
"",
|
|
1104
|
+
].join("\n"));
|
|
1105
|
+
if (applied.leaks.length > 0)
|
|
1106
|
+
process.stdout.write(leaksText(applied.leaks));
|
|
1107
|
+
if (applied.unmapped.length > 0)
|
|
1108
|
+
process.stdout.write(unmappedText(applied.unmapped));
|
|
1109
|
+
if (refreshed.copiedTaken !== undefined)
|
|
1110
|
+
process.stdout.write(`Copied files the folder had not edited, taken as the repository has them:\n${list(refreshed.copiedTaken)}\n`);
|
|
1111
|
+
if (refreshed.copiedLeft !== undefined)
|
|
1112
|
+
process.stdout.write(`Changed in the repository, in a folder the cut copied, and edited in the folder: not written over its copy. Bring each change by hand, then: carveout refresh ${dir} --resolved <path>:\n${list(refreshed.copiedLeft)}\n`);
|
|
1113
|
+
if (refreshed.overlapResolved !== undefined)
|
|
1114
|
+
process.stdout.write(`Taken as resolved where the repository changed the same lines: check the folder's text keeps what it needs of that change (the next patch reverts what it leaves out):\n${list(refreshed.overlapResolved)}\n`);
|
|
1115
|
+
if (refreshed.droppedUpstream !== undefined)
|
|
1116
|
+
process.stdout.write(`Taken as resolved, though the folder's text leaves out some of the repository's change to it (the next patch reverts those lines; merge them in if that is not meant):\n${list(refreshed.droppedUpstream)}\n`);
|
|
1117
|
+
}
|
|
1118
|
+
if (refreshed !== null && (refreshed.applied.leaks.length > 0 || refreshed.applied.unmapped.length > 0))
|
|
1119
|
+
process.exitCode = 2;
|
|
1120
|
+
// An edited copy the repository changed is an overlap too: merged by hand.
|
|
1121
|
+
if (refreshed !== null && (refreshed.applied.conflicts.length > 0 || refreshed.copiedLeft !== undefined))
|
|
1122
|
+
process.exitCode = 7;
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
case "widen": {
|
|
1126
|
+
const [dir] = positionals;
|
|
1127
|
+
if (dir === undefined || values.entry === undefined || values.entry.length === 0)
|
|
1128
|
+
fail("widen needs the folder and at least one --entry <path> to add.");
|
|
1129
|
+
const widened = await widenProject(dir, {
|
|
1130
|
+
entries: values.entry,
|
|
1131
|
+
...(values.out === undefined ? {} : { out: values.out }),
|
|
1132
|
+
...(values.depth === undefined ? {} : { maxDepth: integer(values.depth, "depth", 0) }),
|
|
1133
|
+
...(values["max-files"] === undefined ? {} : { maxFiles: integer(values["max-files"], "max-files", 0) }),
|
|
1134
|
+
...(values.repo === undefined ? {} : { repo }),
|
|
1135
|
+
...(values.allow === undefined ? {} : { allow: values.allow }),
|
|
1136
|
+
...receiptOption,
|
|
1137
|
+
log: (line) => {
|
|
1138
|
+
if (values.json !== true)
|
|
1139
|
+
process.stderr.write(`${line}\n`);
|
|
1140
|
+
},
|
|
1141
|
+
});
|
|
1142
|
+
if (values.json === true)
|
|
1143
|
+
print({
|
|
1144
|
+
out: widened.out,
|
|
1145
|
+
ready: widened.cut.record.ready,
|
|
1146
|
+
receipt: widened.cut.receipt,
|
|
1147
|
+
carried: widened.applied === null ? [] : [...widened.applied.changed, ...widened.applied.added],
|
|
1148
|
+
outside: widened.applied?.outside ?? [],
|
|
1149
|
+
blockers: widened.cut.record.blockers,
|
|
1150
|
+
});
|
|
1151
|
+
else
|
|
1152
|
+
process.stdout.write([
|
|
1153
|
+
`Widened: ${widened.out}`,
|
|
1154
|
+
` Entries: ${widened.cut.record.request.entryPoints.join(", ")}.`,
|
|
1155
|
+
widened.applied === null
|
|
1156
|
+
? " The folder held no work to carry."
|
|
1157
|
+
: ` The work carried into it: ${[...widened.applied.changed, ...widened.applied.added].length} file(s)${widened.applied.outside.length === 0 ? "" : `; outside it still: ${widened.applied.outside.join(", ")}`}.`,
|
|
1158
|
+
...(widened.cut.receipt === null ? [] : [`Receipt: ${widened.cut.receipt}`]),
|
|
1159
|
+
`Next: carveout check ${widened.out}, then hand it over in place of the old one.`,
|
|
1160
|
+
"",
|
|
1161
|
+
].join("\n"));
|
|
1162
|
+
if (!widened.cut.record.ready)
|
|
1163
|
+
process.exitCode = 2;
|
|
551
1164
|
return;
|
|
552
1165
|
}
|
|
553
1166
|
case "merge": {
|
|
@@ -556,18 +1169,32 @@ async function main(argv) {
|
|
|
556
1169
|
fail("merge needs the folder a cut wrote.");
|
|
557
1170
|
if (values.into === undefined)
|
|
558
1171
|
fail("merge needs --into <checkout>: the checkout to apply the edits in.");
|
|
1172
|
+
// The verify command runs the folder's code: in a container unless
|
|
1173
|
+
// told otherwise, checked before anything is applied.
|
|
1174
|
+
let verifySandbox;
|
|
1175
|
+
if (values.verify !== undefined && values.sandbox !== false) {
|
|
1176
|
+
verifySandbox = defaultSandbox(values["sandbox-image"]);
|
|
1177
|
+
const why = sandboxUnavailable(verifySandbox);
|
|
1178
|
+
if (why !== null)
|
|
1179
|
+
fail(`merge --verify runs the folder's code only in a container, on a copy of the checkout with no network but for the install; nothing was applied. ${why} Or pass --no-sandbox to run it in the checkout as you, with your files and network.`);
|
|
1180
|
+
}
|
|
559
1181
|
const merge = await mergeProject(dir, {
|
|
560
1182
|
into: values.into,
|
|
561
1183
|
...(values.verify === undefined ? {} : { verify: values.verify }),
|
|
562
1184
|
...(values.repo === undefined ? {} : { repo }),
|
|
563
1185
|
...(values.allow === undefined ? {} : { allow: values.allow }),
|
|
564
1186
|
...(values["verify-env"] === undefined ? {} : { verifyEnv: values["verify-env"] }),
|
|
1187
|
+
...(verifySandbox === undefined ? {} : { sandbox: verifySandbox }),
|
|
1188
|
+
...(values["verify-setup"] === undefined ? {} : { verifySetup: values["verify-setup"] }),
|
|
1189
|
+
...receiptOption,
|
|
565
1190
|
});
|
|
566
1191
|
if (values.json === true)
|
|
567
1192
|
print(merge);
|
|
568
1193
|
else {
|
|
569
1194
|
for (const edit of merge.patch.boundary)
|
|
570
1195
|
process.stderr.write(`carveout: boundary: ${edit.detail}\n`);
|
|
1196
|
+
for (const { from, to } of merge.patch.renamed ?? [])
|
|
1197
|
+
process.stderr.write(`carveout: ${from} is a new test where the repository already holds a file; applied as ${to}.\n`);
|
|
571
1198
|
for (const step of merge.steps) {
|
|
572
1199
|
process.stdout.write(`${step.ok ? "ok " : "FAIL"} ${step.step}: ${step.ok && step.step === "verify" ? "" : step.output}\n`);
|
|
573
1200
|
}
|