@williamthorsen/nmr 0.48.0 → 0.49.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/.meta/changelog.json +158 -0
- package/.readyup/kits/default.js +1527 -1460
- package/.readyup/manifest.json +96 -11
- package/CHANGELOG.md +41 -0
- package/README.md +36 -8
- package/agents/guidance/rulebooks/nmr.md +2 -2
- package/dist/esm/check-cache-build-output.d.ts +24 -0
- package/dist/esm/check-cache-build-output.js +49 -0
- package/dist/esm/check-cache-delegation.d.ts +27 -0
- package/dist/esm/check-cache-delegation.js +92 -0
- package/dist/esm/check-cache-entries.d.ts +109 -0
- package/dist/esm/check-cache-entries.js +87 -0
- package/dist/esm/check-cache-gate.d.ts +64 -0
- package/dist/esm/check-cache-gate.js +226 -0
- package/dist/esm/check-cache-key.d.ts +47 -0
- package/dist/esm/check-cache-key.js +77 -0
- package/dist/esm/check-cache-record.d.ts +69 -0
- package/dist/esm/check-cache-record.js +167 -0
- package/dist/esm/check-cache.d.ts +8 -176
- package/dist/esm/check-cache.js +9 -214
- package/dist/esm/child-env.d.ts +57 -0
- package/dist/esm/child-env.js +59 -0
- package/dist/esm/child-process.d.ts +55 -0
- package/dist/esm/child-process.js +112 -0
- package/dist/esm/commands/build.d.ts +0 -16
- package/dist/esm/commands/build.js +9 -214
- package/dist/esm/commands/clean.js +3 -2
- package/dist/esm/commands/compiler.d.ts +18 -0
- package/dist/esm/commands/compiler.js +49 -0
- package/dist/esm/commands/fmt.d.ts +0 -39
- package/dist/esm/commands/fmt.js +4 -81
- package/dist/esm/commands/format-targets.d.ts +39 -0
- package/dist/esm/commands/format-targets.js +83 -0
- package/dist/esm/commands/rewrite-specifiers.d.ts +17 -0
- package/dist/esm/commands/rewrite-specifiers.js +122 -0
- package/dist/esm/commands/tsconfig-chain.d.ts +8 -0
- package/dist/esm/commands/tsconfig-chain.js +63 -0
- package/dist/esm/config-check-cache.d.ts +17 -0
- package/dist/esm/config-check-cache.js +63 -0
- package/dist/esm/config-validation.d.ts +20 -0
- package/dist/esm/config-validation.js +236 -0
- package/dist/esm/config.js +4 -286
- package/dist/esm/crossing-diagnostics.d.ts +39 -0
- package/dist/esm/crossing-diagnostics.js +132 -0
- package/dist/esm/default-scripts.js +1 -1
- package/dist/esm/delegate-host.d.ts +43 -0
- package/dist/esm/delegate-host.js +178 -0
- package/dist/esm/delegation-target.d.ts +52 -0
- package/dist/esm/delegation-target.js +71 -0
- package/dist/esm/fan-out.d.ts +57 -0
- package/dist/esm/fan-out.js +146 -0
- package/dist/esm/glyphs.d.ts +1 -1
- package/dist/esm/help.js +9 -3
- package/dist/esm/helpers/deriveExcerpt.d.ts +9 -0
- package/dist/esm/helpers/deriveExcerpt.js +1 -1
- package/dist/esm/helpers/renderQuotedList.d.ts +2 -0
- package/dist/esm/helpers/renderQuotedList.js +9 -0
- package/dist/esm/hooks.d.ts +17 -0
- package/dist/esm/hooks.js +23 -0
- package/dist/esm/invocation-args.d.ts +40 -0
- package/dist/esm/invocation-args.js +178 -0
- package/dist/esm/invocation.d.ts +67 -0
- package/dist/esm/invocation.js +3 -0
- package/dist/esm/machine-lock.d.ts +2 -0
- package/dist/esm/machine-lock.js +1 -1
- package/dist/esm/output-channel.d.ts +23 -0
- package/dist/esm/output-channel.js +14 -0
- package/dist/esm/pass-manifest.d.ts +53 -0
- package/dist/esm/pass-manifest.js +89 -0
- package/dist/esm/presentation.d.ts +47 -0
- package/dist/esm/presentation.js +32 -0
- package/dist/esm/recording.d.ts +1 -1
- package/dist/esm/recording.js +2 -2
- package/dist/esm/remote-host-session.d.ts +62 -0
- package/dist/esm/remote-host-session.js +277 -0
- package/dist/esm/remote-probe-host.d.ts +22 -0
- package/dist/esm/remote-probe-host.js +50 -0
- package/dist/esm/remote-probe.d.ts +31 -0
- package/dist/esm/remote-probe.js +78 -0
- package/dist/esm/remote-run.d.ts +28 -73
- package/dist/esm/remote-run.js +103 -165
- package/dist/esm/remote-session.d.ts +40 -0
- package/dist/esm/remote-session.js +151 -0
- package/dist/esm/remote-snapshot.d.ts +21 -0
- package/dist/esm/remote-snapshot.js +80 -0
- package/dist/esm/replay-assembly.d.ts +2 -2
- package/dist/esm/replay-assembly.js +1 -1
- package/dist/esm/run-chain.d.ts +21 -0
- package/dist/esm/run-chain.js +108 -0
- package/dist/esm/runCli.d.ts +1 -9
- package/dist/esm/runCli.js +39 -1002
- package/dist/esm/runner.d.ts +1 -54
- package/dist/esm/runner.js +2 -125
- package/dist/esm/shell-syntax.d.ts +30 -0
- package/dist/esm/shell-syntax.js +120 -0
- package/dist/esm/steps.d.ts +0 -2
- package/dist/esm/steps.js +3 -119
- package/dist/esm/types.d.ts +5 -0
- package/dist/esm/user-config.d.ts +34 -0
- package/dist/esm/user-config.js +93 -0
- package/dist/esm/verbosity.d.ts +6 -0
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +1 -1
- package/dist/esm/verdict.js +1 -1
- package/docs/check-cache.md +28 -12
- package/docs/delegate.md +96 -16
- package/docs/reporting.md +6 -1
- package/docs/scripts.md +2 -1
- package/package.json +3 -3
- /package/dist/esm/{prettier-default-config.d.ts → prettier-default.config.d.ts} +0 -0
- /package/dist/esm/{prettier-default-config.js → prettier-default.config.js} +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
|
+
import { isCacheableCommand, resolveTreeSnapshot, writeDebugNote } from "./check-cache.js";
|
|
4
|
+
import { findStaleBuildOutput, readBuildOutputState } from "./check-cache-build-output.js";
|
|
5
|
+
import { readCheckCacheEntry, recordTranscript, writeCheckCacheEntry, } from "./check-cache-entries.js";
|
|
6
|
+
import { CURRENT_RUNTIME } from "./check-cache-key.js";
|
|
7
|
+
import { deriveExcerpt } from "./helpers/deriveExcerpt.js";
|
|
8
|
+
import { composeTranscript } from "./helpers/transcript.js";
|
|
9
|
+
import { RUN_IF_PRESENT_ENV_VAR, VERSION } from "./invocation.js";
|
|
10
|
+
import { writePassManifestEntry } from "./pass-manifest.js";
|
|
11
|
+
import { renderRecording, renderRefusal, resolveRecording } from "./recording.js";
|
|
12
|
+
import { assembleReplay } from "./replay-assembly.js";
|
|
13
|
+
export async function recordHostPass(options) {
|
|
14
|
+
const { env, host, key, ref, retention, snapshot, stderr } = options;
|
|
15
|
+
const recorded = await readCheckCacheEntry(ref);
|
|
16
|
+
if (recorded !== undefined && recorded.host === undefined && recorded.key === key) {
|
|
17
|
+
const output = await readBuildOutputState(options.monorepoRoot, options.config);
|
|
18
|
+
if (output.missingPackages.length === 0 &&
|
|
19
|
+
findStaleBuildOutput(recorded.buildDigests, output.digests) === undefined) {
|
|
20
|
+
writeDebugNote(`not recording ${ref.command} on ${host}: a pass earned here covers this tree`, env, stderr);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
await writePass({
|
|
25
|
+
entry: {
|
|
26
|
+
key,
|
|
27
|
+
treeHash: snapshot.treeId,
|
|
28
|
+
headSha: snapshot.headSha,
|
|
29
|
+
commandString: options.commandString,
|
|
30
|
+
nmrVersion: VERSION,
|
|
31
|
+
nodeVersion: CURRENT_RUNTIME.nodeVersion,
|
|
32
|
+
durationMs: options.durationMs,
|
|
33
|
+
recordedAt: new Date().toISOString(),
|
|
34
|
+
buildDigests: {},
|
|
35
|
+
host,
|
|
36
|
+
...(retention !== undefined && { retention }),
|
|
37
|
+
},
|
|
38
|
+
env,
|
|
39
|
+
ref,
|
|
40
|
+
retainedOutput: options.retainedOutput,
|
|
41
|
+
stderr,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export async function recordPass(options) {
|
|
45
|
+
const { anchorDir, command, env, monorepoRoot, snapshot, stderr } = options;
|
|
46
|
+
const currentRead = resolveTreeSnapshot({ monorepoRoot, env: {} });
|
|
47
|
+
if (!currentRead.ok) {
|
|
48
|
+
writeDebugNote(`not recording ${command}: ${currentRead.reason}`, env, stderr);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (currentRead.snapshot.treeId !== snapshot.treeId) {
|
|
52
|
+
writeDebugNote(`not recording ${command}: the tree changed while it ran`, env, stderr);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const ref = { anchorDir, command, monorepoRoot };
|
|
56
|
+
const output = await readBuildOutputState(monorepoRoot, options.config);
|
|
57
|
+
const [missingPackage] = output.missingPackages;
|
|
58
|
+
if (missingPackage !== undefined) {
|
|
59
|
+
writeDebugNote(`not recording ${command}: ${missingPackage} does not have build output`, env, stderr);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const changedPackage = findStaleBuildOutput(options.buildOutputBefore.digests, output.digests);
|
|
63
|
+
if (changedPackage !== undefined) {
|
|
64
|
+
writeDebugNote(`not recording ${command}: ${changedPackage}'s build output changed while it ran`, env, stderr);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const retention = await composeRetention({
|
|
68
|
+
anchorDir,
|
|
69
|
+
command,
|
|
70
|
+
key: options.retentionKey,
|
|
71
|
+
monorepoRoot,
|
|
72
|
+
retainedOutput: options.retainedOutput,
|
|
73
|
+
runId: options.runId,
|
|
74
|
+
steps: options.ownSteps,
|
|
75
|
+
treeHash: snapshot.treeId,
|
|
76
|
+
});
|
|
77
|
+
const isWritten = await writePass({
|
|
78
|
+
entry: {
|
|
79
|
+
key: options.key,
|
|
80
|
+
treeHash: snapshot.treeId,
|
|
81
|
+
headSha: snapshot.headSha,
|
|
82
|
+
commandString: options.commandString,
|
|
83
|
+
nmrVersion: VERSION,
|
|
84
|
+
nodeVersion: CURRENT_RUNTIME.nodeVersion,
|
|
85
|
+
durationMs: options.durationMs,
|
|
86
|
+
recordedAt: new Date().toISOString(),
|
|
87
|
+
buildDigests: output.digests,
|
|
88
|
+
...(retention !== undefined && { retention }),
|
|
89
|
+
},
|
|
90
|
+
env,
|
|
91
|
+
ref,
|
|
92
|
+
retainedOutput: options.retainedOutput,
|
|
93
|
+
stderr,
|
|
94
|
+
});
|
|
95
|
+
if (isWritten) {
|
|
96
|
+
await writePassManifestEntry({
|
|
97
|
+
anchorDir,
|
|
98
|
+
command,
|
|
99
|
+
commandString: options.commandString,
|
|
100
|
+
durationMs: options.durationMs,
|
|
101
|
+
env,
|
|
102
|
+
monorepoRoot,
|
|
103
|
+
replay: retention?.replay,
|
|
104
|
+
stderr,
|
|
105
|
+
treeHash: snapshot.treeId,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function reportRecording(plan) {
|
|
110
|
+
const { anchorDir, command, context, env, scope, stderr, stdout, styles } = plan.invocation;
|
|
111
|
+
const { config, monorepoRoot } = context;
|
|
112
|
+
const lookup = await resolveRecording({
|
|
113
|
+
anchorDir,
|
|
114
|
+
command,
|
|
115
|
+
currentIdentity: {
|
|
116
|
+
commandString: plan.commandString,
|
|
117
|
+
nmrVersion: VERSION,
|
|
118
|
+
nodeVersion: CURRENT_RUNTIME.nodeVersion,
|
|
119
|
+
treeHash: plan.snapshot?.treeId,
|
|
120
|
+
},
|
|
121
|
+
isCacheable: isCacheableCommand(config.checkCache, command),
|
|
122
|
+
key: plan.key,
|
|
123
|
+
monorepoRoot,
|
|
124
|
+
});
|
|
125
|
+
if (!lookup.ok) {
|
|
126
|
+
stderr.write(`${renderRefusal({ command, refusal: lookup.refusal, scope, style: styles.stderr })}\n`);
|
|
127
|
+
return { exitCode: env[RUN_IF_PRESENT_ENV_VAR] === '1' ? 0 : 1 };
|
|
128
|
+
}
|
|
129
|
+
stdout.write(renderRecording({ command, recording: lookup.recording, scope, style: styles.stdout }));
|
|
130
|
+
return { exitCode: 0 };
|
|
131
|
+
}
|
|
132
|
+
async function composeRetention(options) {
|
|
133
|
+
const { anchorDir, command, key, retainedOutput, runId } = options;
|
|
134
|
+
if (retainedOutput !== undefined) {
|
|
135
|
+
const excerpt = deriveExcerpt(retainedOutput.stdout.toString('utf8')) ?? deriveExcerpt(retainedOutput.stderr.toString('utf8'));
|
|
136
|
+
if (excerpt === undefined) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
return { key, replay: [{ command, excerpt, scope: path.basename(anchorDir) }], runId };
|
|
140
|
+
}
|
|
141
|
+
const replay = await assembleReplay({
|
|
142
|
+
anchorDir,
|
|
143
|
+
monorepoRoot: options.monorepoRoot,
|
|
144
|
+
runId,
|
|
145
|
+
steps: options.steps,
|
|
146
|
+
treeHash: options.treeHash,
|
|
147
|
+
});
|
|
148
|
+
return replay.length === 0 ? undefined : { key, replay, runId };
|
|
149
|
+
}
|
|
150
|
+
async function writePass(options) {
|
|
151
|
+
const { entry, env, ref, stderr } = options;
|
|
152
|
+
const transcript = options.retainedOutput === undefined ? undefined : composeTranscript(options.retainedOutput);
|
|
153
|
+
try {
|
|
154
|
+
await recordTranscript(ref, transcript);
|
|
155
|
+
await writeCheckCacheEntry({ ...ref, entry });
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
try {
|
|
160
|
+
await recordTranscript(ref, undefined);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
}
|
|
164
|
+
writeDebugNote(`could not record ${ref.command}: ${describeError(error)}`, env, stderr);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -1,75 +1,11 @@
|
|
|
1
1
|
import type { Writable } from 'node:stream';
|
|
2
2
|
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
3
|
-
import type {
|
|
4
|
-
import type { CheckCacheConfig, NmrConfig } from './types.js';
|
|
5
|
-
/** A recorded pass: what ran, on which tree, and how long it took. */
|
|
6
|
-
export interface CheckCacheEntry {
|
|
7
|
-
/** The full cache key under which the pass was recorded; a hit is this key matching the key computed now. */
|
|
8
|
-
key: string;
|
|
9
|
-
treeHash: string;
|
|
10
|
-
headSha: string;
|
|
11
|
-
commandString: string;
|
|
12
|
-
nmrVersion: string;
|
|
13
|
-
nodeVersion: string;
|
|
14
|
-
durationMs: number;
|
|
15
|
-
/** ISO-8601 instant the pass completed. */
|
|
16
|
-
recordedAt: string;
|
|
17
|
-
/**
|
|
18
|
-
* The build digest that each covered package's output had when the pass was recorded. Build output is
|
|
19
|
-
* git-ignored, so the tree hash cannot describe it; comparing these separates output built from this tree from
|
|
20
|
-
* output that another tree left behind.
|
|
21
|
-
*/
|
|
22
|
-
buildDigests: Record<string, string>;
|
|
23
|
-
/** What a skip replays in place of the run that it recalls, absent on a pass that retained nothing. */
|
|
24
|
-
retention?: Retention;
|
|
25
|
-
}
|
|
26
|
-
/** What locates one command's recorded pass at one scope, and the transcript beside it. */
|
|
27
|
-
export interface EntryRef {
|
|
28
|
-
anchorDir: string;
|
|
29
|
-
command: string;
|
|
30
|
-
monorepoRoot: string;
|
|
31
|
-
}
|
|
32
|
-
/** One command's excerpt, attributed to the scope and the command that produced it. */
|
|
33
|
-
export interface ReplayLine {
|
|
34
|
-
command: string;
|
|
35
|
-
excerpt: string;
|
|
36
|
-
scope: string;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* What a recalled pass replays, the key certifying the excerpts describe this environment's output, and the
|
|
40
|
-
* run that last vouched for them.
|
|
41
|
-
*
|
|
42
|
-
* A list rather than one excerpt: a composite's entry contains its constituents' lines, and a nested one's are
|
|
43
|
-
* spliced into its parent's, after which the attribution cannot be re-derived from the entry that contains them.
|
|
44
|
-
*
|
|
45
|
-
* The witness admits a constituent's line into the assembly that its parent records: a run writes it when it
|
|
46
|
-
* records an excerpt, and restamps it when it recalls one and replays it.
|
|
47
|
-
*/
|
|
48
|
-
export interface Retention {
|
|
49
|
-
key: string;
|
|
50
|
-
replay: ReplayLine[];
|
|
51
|
-
runId: string;
|
|
52
|
-
}
|
|
53
|
-
/** What nmr's own build has left on disk across the workspace. */
|
|
54
|
-
export interface BuildOutputState {
|
|
55
|
-
/** Covered packages whose output is absent. */
|
|
56
|
-
missingPackages: string[];
|
|
57
|
-
/** The digest of the inputs from which each covered package's output was built, keyed by package name. */
|
|
58
|
-
digests: Record<string, string>;
|
|
59
|
-
}
|
|
60
|
-
/** The parts of the running interpreter that can change what a check concludes. */
|
|
61
|
-
export interface RuntimeIdentity {
|
|
62
|
-
arch: string;
|
|
63
|
-
nodeVersion: string;
|
|
64
|
-
platform: string;
|
|
65
|
-
}
|
|
3
|
+
import type { CheckCacheConfig } from './types.js';
|
|
66
4
|
/** One working tree, observed once per top-level invocation and shared with every process below it. */
|
|
67
5
|
export interface TreeSnapshot {
|
|
68
|
-
|
|
6
|
+
treeId: string;
|
|
69
7
|
headSha: string;
|
|
70
8
|
}
|
|
71
|
-
/** The interpreter on which this process is running, folded into every key and recorded alongside every pass. */
|
|
72
|
-
export declare const CURRENT_RUNTIME: RuntimeIdentity;
|
|
73
9
|
/** Set to `1` to hear why the gate did not skip, or why it is disabled. */
|
|
74
10
|
export declare const DEBUG_ENV_VAR = "NMR_DEBUG";
|
|
75
11
|
/**
|
|
@@ -85,76 +21,17 @@ export declare const DEBUG_ENV_VAR = "NMR_DEBUG";
|
|
|
85
21
|
export declare const DEFAULT_CACHEABLE_COMMANDS: string[];
|
|
86
22
|
/** Set to `1` for the standing equivalent of `--no-cache`: skip the lookup, still record on success. */
|
|
87
23
|
export declare const NO_CACHE_ENV_VAR = "NMR_NO_CACHE";
|
|
24
|
+
/**
|
|
25
|
+
* Names the directory into which every nmr process of a delegated run reports the passes that it records or recalls.
|
|
26
|
+
* Only the host session sets it, so that the delegating machine can learn a composite's constituent passes.
|
|
27
|
+
*/
|
|
28
|
+
export declare const PASS_MANIFEST_DIR_ENV_VAR = "NMR_PASS_MANIFEST_DIR";
|
|
88
29
|
/** Passes one run's identity down the spawned chain, so that an entry can name the run that vouched for it. */
|
|
89
30
|
export declare const RUN_ID_ENV_VAR = "NMR_RUN_ID";
|
|
90
31
|
/** Passes the top-level tree snapshot down the spawned chain, to make one invocation hash the tree only once. */
|
|
91
32
|
export declare const TREE_SNAPSHOT_ENV_VAR = "NMR_TREE_SNAPSHOT";
|
|
92
|
-
/**
|
|
93
|
-
* Restamps a recalled entry's retention with the run that is replaying it, so that an excerpt certified by this
|
|
94
|
-
* run can join the assembly recorded by a composite above it.
|
|
95
|
-
*
|
|
96
|
-
* A recall certifies as surely as a recording does. Because the pass key matched, the excerpt describes this
|
|
97
|
-
* tree; because the caller calls this only when the retention key matched too, the excerpt also describes this
|
|
98
|
-
* presentation environment. Everything else the entry records stands -- the instant and the duration belong to
|
|
99
|
-
* the run that earned the pass, and a recall must not make it read as later or longer than it was.
|
|
100
|
-
*
|
|
101
|
-
* A cache that cannot be written is not worth failing a green run over: `certifyRetention` reports that failure
|
|
102
|
-
* in the debug note.
|
|
103
|
-
*/
|
|
104
|
-
export declare function certifyRetention(options: {
|
|
105
|
-
anchorDir: string;
|
|
106
|
-
command: string;
|
|
107
|
-
entry: CheckCacheEntry;
|
|
108
|
-
env: NodeJS.ProcessEnv;
|
|
109
|
-
monorepoRoot: string;
|
|
110
|
-
runId: string;
|
|
111
|
-
stderr: Writable;
|
|
112
|
-
}): Promise<void>;
|
|
113
|
-
/**
|
|
114
|
-
* Folds everything that can change what a command concludes into one key: the tree's content, the command
|
|
115
|
-
* string that would run, the scope in which it would run, nmr's own version, the interpreter, what is installed,
|
|
116
|
-
* and the environment variables that a check can read. A hit is this key matching a recorded one, so an ingredient left
|
|
117
|
-
* out here is an ingredient that could change while the cache still claims a pass.
|
|
118
|
-
*
|
|
119
|
-
* Reports a reason instead of a key when the install fingerprint cannot be read, which disables the gate.
|
|
120
|
-
*/
|
|
121
|
-
export declare function computeCacheKey(options: {
|
|
122
|
-
anchorDir: string;
|
|
123
|
-
command: string;
|
|
124
|
-
commandString: string;
|
|
125
|
-
env: NodeJS.ProcessEnv;
|
|
126
|
-
monorepoRoot: string;
|
|
127
|
-
nmrVersion: string;
|
|
128
|
-
runtime?: RuntimeIdentity;
|
|
129
|
-
snapshot: TreeSnapshot;
|
|
130
|
-
}): {
|
|
131
|
-
ok: true;
|
|
132
|
-
key: string;
|
|
133
|
-
} | {
|
|
134
|
-
ok: false;
|
|
135
|
-
reason: string;
|
|
136
|
-
};
|
|
137
|
-
/**
|
|
138
|
-
* Folds what changes a transcript without changing a conclusion onto the pass key: the channel on which each of
|
|
139
|
-
* the command's output streams ran, and the environment variables through which a tool presents itself.
|
|
140
|
-
*
|
|
141
|
-
* The channel kind keeps a run at a terminal from replaying a piped recording. It is the channel's kind and not
|
|
142
|
-
* whether a terminal is attached: under quiet mode the child writes to pipes at a terminal, and its transcript
|
|
143
|
-
* is reproducible.
|
|
144
|
-
*/
|
|
145
|
-
export declare function computeRetentionKey(options: {
|
|
146
|
-
channels: OutputChannels;
|
|
147
|
-
env: NodeJS.ProcessEnv;
|
|
148
|
-
passKey: string;
|
|
149
|
-
}): string;
|
|
150
33
|
/** Renders a snapshot for the environment of every process below this one. */
|
|
151
34
|
export declare function encodeTreeSnapshot(snapshot: TreeSnapshot): string;
|
|
152
|
-
/**
|
|
153
|
-
* Names a covered package whose output differs between two observations of it, or `undefined` when every
|
|
154
|
-
* package agrees. A package that has appeared or disappeared between them counts as a disagreement, because
|
|
155
|
-
* the output that the earlier observation describes is not the output that the later one found.
|
|
156
|
-
*/
|
|
157
|
-
export declare function findStaleBuildOutput(earlierDigests: Record<string, string>, laterDigests: Record<string, string>): string | undefined;
|
|
158
35
|
/**
|
|
159
36
|
* Renders the warning for a `--no-cache` that appears after the command name, which makes it an argument to
|
|
160
37
|
* the command rather than a flag to nmr. nmr passes the argument on unchanged, so the warning is the only sign
|
|
@@ -169,44 +46,6 @@ export declare function formatMisplacedNoCacheWarning(command: string, style: Ou
|
|
|
169
46
|
* excluding it here keeps the gate and every reader of its entries in agreement.
|
|
170
47
|
*/
|
|
171
48
|
export declare function isCacheableCommand(checkCache: CheckCacheConfig | undefined, command: string): boolean;
|
|
172
|
-
/**
|
|
173
|
-
* Reads the state of the build output that nmr's own build covers. Build output is git-ignored, so the tree hash
|
|
174
|
-
* says nothing about it: a `ci` whose `build` constituent is cached would otherwise skip on a tree whose `dist`
|
|
175
|
-
* had been deleted, or whose `dist` was compiled from a different tree, and hand back a green exit over a
|
|
176
|
-
* repository that cannot run.
|
|
177
|
-
*
|
|
178
|
-
* Because a package whose `build` or `compile` is overridden emits to a location that this function does not
|
|
179
|
-
* know about, it is left out rather than made a permanent miss.
|
|
180
|
-
*/
|
|
181
|
-
export declare function readBuildOutputState(monorepoRoot: string, config: NmrConfig): Promise<BuildOutputState>;
|
|
182
|
-
/**
|
|
183
|
-
* Reads the entry recorded for one command at one scope, or `undefined` when there is none to trust.
|
|
184
|
-
*
|
|
185
|
-
* Retention is vouched for separately from the pass that contains it: an excerpt of a shape that this function
|
|
186
|
-
* cannot read is dropped, leaving a pass that skips cleanly and reports its verdict alone.
|
|
187
|
-
*/
|
|
188
|
-
export declare function readCheckCacheEntry(options: {
|
|
189
|
-
anchorDir: string;
|
|
190
|
-
command: string;
|
|
191
|
-
monorepoRoot: string;
|
|
192
|
-
}): Promise<CheckCacheEntry | undefined>;
|
|
193
|
-
/**
|
|
194
|
-
* Reads the whole output that one recorded pass retained, or `undefined` when it retained none.
|
|
195
|
-
*
|
|
196
|
-
* Held to nothing on its own: the entry beside it says which tree the bytes describe, and a caller
|
|
197
|
-
* that has not matched the entry's key is reading a transcript of some other tree.
|
|
198
|
-
*/
|
|
199
|
-
export declare function readTranscript(ref: EntryRef): Promise<string | undefined>;
|
|
200
|
-
/**
|
|
201
|
-
* Makes the transcript beside one entry be exactly what this pass retained, removing what an earlier pass
|
|
202
|
-
* left when this one retained nothing.
|
|
203
|
-
*
|
|
204
|
-
* A composite retains nothing of its own, so without the removal a leaf's transcript would remain beside an
|
|
205
|
-
* entry that never produced it, and `--log` would date another run's bytes by this one's instant.
|
|
206
|
-
*/
|
|
207
|
-
export declare function recordTranscript(ref: EntryRef, transcript: string | undefined): Promise<void>;
|
|
208
|
-
/** Removes every recorded pass for a monorepo, or for a standalone package outside one. */
|
|
209
|
-
export declare function removeCheckCache(scopeDir: string): Promise<void>;
|
|
210
49
|
/**
|
|
211
50
|
* Merges a repo's `checkCache` configuration into the default set: `extraCommands` adds to the defaults, and
|
|
212
51
|
* `excludeCommands` removes a command whose chain does more than report an exit status.
|
|
@@ -217,7 +56,7 @@ export declare function resolveCacheableCommands(checkCache: CheckCacheConfig |
|
|
|
217
56
|
* down, and otherwise a fresh one, because the run starts at this invocation.
|
|
218
57
|
*
|
|
219
58
|
* Unbounded, whereas the tree snapshot is bounded by a HEAD comparison: a process that outlives its run hands a
|
|
220
|
-
* stale identity to the invocations that it later makes, and the tree
|
|
59
|
+
* stale identity to the invocations that it later makes, and the tree id keeps that harmless, because every
|
|
221
60
|
* constituent entry is held to it before its excerpt joins an assembly.
|
|
222
61
|
*/
|
|
223
62
|
export declare function resolveRunId(env: NodeJS.ProcessEnv): string;
|
|
@@ -240,13 +79,6 @@ export declare function resolveTreeSnapshot(options: {
|
|
|
240
79
|
ok: false;
|
|
241
80
|
reason: string;
|
|
242
81
|
};
|
|
243
|
-
/** Records a pass, replacing whatever this command last recorded at this scope. */
|
|
244
|
-
export declare function writeCheckCacheEntry(options: {
|
|
245
|
-
anchorDir: string;
|
|
246
|
-
command: string;
|
|
247
|
-
entry: CheckCacheEntry;
|
|
248
|
-
monorepoRoot: string;
|
|
249
|
-
}): Promise<void>;
|
|
250
82
|
/**
|
|
251
83
|
* Writes a note explaining a gate decision, but only when `NMR_DEBUG=1`. The gate is silent by default: a
|
|
252
84
|
* reason to run is not news, and a line per invocation explaining why nothing was skipped would bury the
|
package/dist/esm/check-cache.js
CHANGED
|
@@ -1,22 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import process from 'node:process';
|
|
5
|
-
import { hashWorkingTree, readCacheEntry, readHeadSha, readJsonCacheEntry, removeCacheDir, removeCacheEntry, resolveCacheEntryPath, STATUS_GLYPHS, writeCacheEntry, } from '@williamthorsen/nmr-core';
|
|
6
|
-
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
7
|
-
import { hasBuildOutput, readBuildDigest } from "./commands/build-output.js";
|
|
8
|
-
import { loadWorkspaceConfig } from "./config.js";
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { hashWorkingTree, readHeadSha, STATUS_GLYPHS } from '@williamthorsen/nmr-core';
|
|
9
4
|
import { isHookName } from "./helpers/hook-name.js";
|
|
10
|
-
import { isObject, isStringRecord } from "./helpers/type-guards.js";
|
|
11
|
-
import { getDefaultWorkspaceScripts } from "./resolve-scripts.js";
|
|
12
|
-
import { buildWorkspaceRegistry, resolveScript } from "./resolver.js";
|
|
13
|
-
import { renderChain } from "./steps.js";
|
|
14
|
-
import { getWorkspacePackageDirs } from "./workspace.js";
|
|
15
|
-
export const CURRENT_RUNTIME = {
|
|
16
|
-
arch: process.arch,
|
|
17
|
-
nodeVersion: process.version,
|
|
18
|
-
platform: process.platform,
|
|
19
|
-
};
|
|
20
5
|
export const DEBUG_ENV_VAR = 'NMR_DEBUG';
|
|
21
6
|
export const DEFAULT_CACHEABLE_COMMANDS = [
|
|
22
7
|
'check',
|
|
@@ -40,72 +25,11 @@ export const DEFAULT_CACHEABLE_COMMANDS = [
|
|
|
40
25
|
'typecheck',
|
|
41
26
|
];
|
|
42
27
|
export const NO_CACHE_ENV_VAR = 'NMR_NO_CACHE';
|
|
28
|
+
export const PASS_MANIFEST_DIR_ENV_VAR = 'NMR_PASS_MANIFEST_DIR';
|
|
43
29
|
export const RUN_ID_ENV_VAR = 'NMR_RUN_ID';
|
|
44
30
|
export const TREE_SNAPSHOT_ENV_VAR = 'NMR_TREE_SNAPSHOT';
|
|
45
|
-
const BUILT_IN_COMPILE = 'nmr-compile';
|
|
46
|
-
const CACHE_TOOL = 'nmr-check';
|
|
47
|
-
const INSTALL_FINGERPRINT_FILES = [
|
|
48
|
-
path.join('node_modules', '.modules.yaml'),
|
|
49
|
-
path.join('node_modules', '.pnpm', 'lock.yaml'),
|
|
50
|
-
];
|
|
51
|
-
const KEY_FORMAT = 'nmr-check-cache-v1';
|
|
52
|
-
const KEYED_ENV_VARS = ['LANG', 'LC_ALL', 'NODE_OPTIONS', 'TZ'];
|
|
53
|
-
const RETENTION_KEY_FORMAT = 'nmr-retention-v1';
|
|
54
|
-
const RETENTION_KEYED_ENV_VARS = ['CI', 'COLUMNS', 'FORCE_COLOR', 'NO_COLOR', 'TERM'];
|
|
55
|
-
const UNSAFE_SLUG_CHARACTERS = /[^\w.-]+/g;
|
|
56
|
-
export async function certifyRetention(options) {
|
|
57
|
-
const { entry, runId } = options;
|
|
58
|
-
if (entry.retention === undefined || entry.retention.runId === runId) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
await writeCheckCacheEntry({
|
|
63
|
-
anchorDir: options.anchorDir,
|
|
64
|
-
command: options.command,
|
|
65
|
-
monorepoRoot: options.monorepoRoot,
|
|
66
|
-
entry: { ...entry, retention: { ...entry.retention, runId } },
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
writeDebugNote(`could not certify ${options.command}: ${describeError(error)}`, options.env, options.stderr);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
export function computeCacheKey(options) {
|
|
74
|
-
const fingerprint = resolveInstallFingerprint(options.monorepoRoot);
|
|
75
|
-
if (!fingerprint.ok) {
|
|
76
|
-
return fingerprint;
|
|
77
|
-
}
|
|
78
|
-
const runtime = options.runtime ?? CURRENT_RUNTIME;
|
|
79
|
-
const scope = path.relative(options.monorepoRoot, options.anchorDir);
|
|
80
|
-
const parts = [
|
|
81
|
-
KEY_FORMAT,
|
|
82
|
-
options.snapshot.hash,
|
|
83
|
-
scope === '' ? '.' : scope,
|
|
84
|
-
options.command,
|
|
85
|
-
options.commandString,
|
|
86
|
-
options.nmrVersion,
|
|
87
|
-
runtime.nodeVersion,
|
|
88
|
-
runtime.platform,
|
|
89
|
-
runtime.arch,
|
|
90
|
-
fingerprint.fingerprint,
|
|
91
|
-
];
|
|
92
|
-
return { ok: true, key: digestParts([...parts, ...composeEnvParts(KEYED_ENV_VARS, options.env)]) };
|
|
93
|
-
}
|
|
94
|
-
export function computeRetentionKey(options) {
|
|
95
|
-
const parts = [
|
|
96
|
-
RETENTION_KEY_FORMAT,
|
|
97
|
-
options.passKey,
|
|
98
|
-
describeChannel(options.channels.stdout),
|
|
99
|
-
describeChannel(options.channels.stderr),
|
|
100
|
-
];
|
|
101
|
-
return digestParts([...parts, ...composeEnvParts(RETENTION_KEYED_ENV_VARS, options.env)]);
|
|
102
|
-
}
|
|
103
31
|
export function encodeTreeSnapshot(snapshot) {
|
|
104
|
-
return `${snapshot.
|
|
105
|
-
}
|
|
106
|
-
export function findStaleBuildOutput(earlierDigests, laterDigests) {
|
|
107
|
-
const names = [...new Set([...Object.keys(earlierDigests), ...Object.keys(laterDigests)])].toSorted();
|
|
108
|
-
return names.find((name) => earlierDigests[name] !== laterDigests[name]);
|
|
32
|
+
return `${snapshot.treeId} ${snapshot.headSha}`;
|
|
109
33
|
}
|
|
110
34
|
export function formatMisplacedNoCacheWarning(command, style) {
|
|
111
35
|
return (`${STATUS_GLYPHS[style].warning.text} --no-cache after the command name is passed to \`${command}\`, ` +
|
|
@@ -114,57 +38,6 @@ export function formatMisplacedNoCacheWarning(command, style) {
|
|
|
114
38
|
export function isCacheableCommand(checkCache, command) {
|
|
115
39
|
return !isHookName(command) && checkCache?.enabled !== false && resolveCacheableCommands(checkCache).has(command);
|
|
116
40
|
}
|
|
117
|
-
export async function readBuildOutputState(monorepoRoot, config) {
|
|
118
|
-
const state = { missingPackages: [], digests: {} };
|
|
119
|
-
let packageDirs;
|
|
120
|
-
try {
|
|
121
|
-
packageDirs = getWorkspacePackageDirs(monorepoRoot);
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
return state;
|
|
125
|
-
}
|
|
126
|
-
const registry = buildWorkspaceRegistry(config);
|
|
127
|
-
if (JSON.stringify(registry['build']) !== JSON.stringify(getDefaultWorkspaceScripts()['build'])) {
|
|
128
|
-
return state;
|
|
129
|
-
}
|
|
130
|
-
for (const packageDir of packageDirs) {
|
|
131
|
-
if (!isProbeSubject(packageDir, registry)) {
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
const { build } = await loadWorkspaceConfig(packageDir);
|
|
135
|
-
const options = build?.extraIgnorePatterns === undefined ? {} : { extraIgnorePatterns: build.extraIgnorePatterns };
|
|
136
|
-
const name = path.relative(monorepoRoot, packageDir);
|
|
137
|
-
if (await hasBuildOutput(packageDir, options)) {
|
|
138
|
-
state.digests[name] = (await readBuildDigest(packageDir)) ?? '';
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
state.missingPackages.push(name);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return state;
|
|
145
|
-
}
|
|
146
|
-
export async function readCheckCacheEntry(options) {
|
|
147
|
-
const parsedEntry = await readJsonCacheEntry(resolveEntryPath(options), isParsedCheckCacheEntry);
|
|
148
|
-
if (parsedEntry === undefined) {
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
const { retention, ...pass } = parsedEntry;
|
|
152
|
-
return isRetention(retention) ? { ...pass, retention } : pass;
|
|
153
|
-
}
|
|
154
|
-
export async function readTranscript(ref) {
|
|
155
|
-
return readCacheEntry(resolveEntryPath(ref, '.log'));
|
|
156
|
-
}
|
|
157
|
-
export async function recordTranscript(ref, transcript) {
|
|
158
|
-
const entryPath = resolveEntryPath(ref, '.log');
|
|
159
|
-
if (transcript === undefined) {
|
|
160
|
-
await removeCacheEntry(entryPath);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
await writeCacheEntry(entryPath, transcript);
|
|
164
|
-
}
|
|
165
|
-
export async function removeCheckCache(scopeDir) {
|
|
166
|
-
await removeCacheDir({ tool: CACHE_TOOL, scopeDir });
|
|
167
|
-
}
|
|
168
41
|
export function resolveCacheableCommands(checkCache) {
|
|
169
42
|
const commands = new Set([...DEFAULT_CACHEABLE_COMMANDS, ...(checkCache?.extraCommands ?? [])]);
|
|
170
43
|
const excludedCommands = checkCache?.excludeCommands ?? [];
|
|
@@ -196,98 +69,20 @@ export function resolveTreeSnapshot(options) {
|
|
|
196
69
|
if (hashedTree.toplevel !== physicalRoot) {
|
|
197
70
|
return { ok: false, reason: `the monorepo root is not the git toplevel (${hashedTree.toplevel})` };
|
|
198
71
|
}
|
|
199
|
-
return { ok: true, snapshot: {
|
|
200
|
-
}
|
|
201
|
-
export async function writeCheckCacheEntry(options) {
|
|
202
|
-
await writeCacheEntry(resolveEntryPath(options), JSON.stringify(options.entry, undefined, 2));
|
|
72
|
+
return { ok: true, snapshot: { treeId: hashedTree.treeId, headSha: hashedTree.headSha } };
|
|
203
73
|
}
|
|
204
74
|
export function writeDebugNote(message, env, stderr) {
|
|
205
75
|
if (env[DEBUG_ENV_VAR] === '1') {
|
|
206
76
|
stderr.write(`nmr check-cache: ${message}\n`);
|
|
207
77
|
}
|
|
208
78
|
}
|
|
209
|
-
function composeEnvParts(names, env) {
|
|
210
|
-
return names.flatMap((name) => {
|
|
211
|
-
const value = env[name];
|
|
212
|
-
return [name, value === undefined ? 'unset' : 'set', value ?? ''];
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
79
|
function decodeTreeSnapshot(encodedSnapshot) {
|
|
216
80
|
if (encodedSnapshot === undefined) {
|
|
217
81
|
return undefined;
|
|
218
82
|
}
|
|
219
|
-
const [
|
|
220
|
-
if (
|
|
83
|
+
const [treeId, headSha] = encodedSnapshot.split(' ', 2);
|
|
84
|
+
if (treeId === undefined || headSha === undefined || treeId === '' || headSha === '') {
|
|
221
85
|
return undefined;
|
|
222
86
|
}
|
|
223
|
-
return {
|
|
224
|
-
}
|
|
225
|
-
function describeChannel(channel) {
|
|
226
|
-
return channel === 'pipe' ? 'pipe' : 'descriptor';
|
|
227
|
-
}
|
|
228
|
-
function digestParts(parts) {
|
|
229
|
-
const hash = createHash('sha256');
|
|
230
|
-
for (const part of parts) {
|
|
231
|
-
hash.update(part);
|
|
232
|
-
hash.update('\0');
|
|
233
|
-
}
|
|
234
|
-
return hash.digest('hex');
|
|
235
|
-
}
|
|
236
|
-
function isParsedCheckCacheEntry(value) {
|
|
237
|
-
if (!isObject(value)) {
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
const stringFields = ['key', 'treeHash', 'headSha', 'commandString', 'nmrVersion', 'nodeVersion', 'recordedAt'];
|
|
241
|
-
if (stringFields.some((field) => typeof value[field] !== 'string')) {
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
return (typeof value['durationMs'] === 'number' &&
|
|
245
|
-
Number.isFinite(value['durationMs']) &&
|
|
246
|
-
!Number.isNaN(Date.parse(String(value['recordedAt']))) &&
|
|
247
|
-
isStringRecord(value['buildDigests']));
|
|
248
|
-
}
|
|
249
|
-
function isProbeSubject(packageDir, registry) {
|
|
250
|
-
const build = resolveScript('build', registry, packageDir, false);
|
|
251
|
-
if (build === undefined || build.origin.tier === 'package') {
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
const compile = resolveScript('compile', registry, packageDir, false);
|
|
255
|
-
return compile !== undefined && renderChain(compile.steps) === BUILT_IN_COMPILE;
|
|
256
|
-
}
|
|
257
|
-
function isReplayLine(value) {
|
|
258
|
-
return (isObject(value) &&
|
|
259
|
-
typeof value['command'] === 'string' &&
|
|
260
|
-
typeof value['excerpt'] === 'string' &&
|
|
261
|
-
typeof value['scope'] === 'string');
|
|
262
|
-
}
|
|
263
|
-
function isRetention(value) {
|
|
264
|
-
return (isObject(value) &&
|
|
265
|
-
typeof value['key'] === 'string' &&
|
|
266
|
-
typeof value['runId'] === 'string' &&
|
|
267
|
-
Array.isArray(value['replay']) &&
|
|
268
|
-
value['replay'].every((line) => isReplayLine(line)));
|
|
269
|
-
}
|
|
270
|
-
function resolveEntryPath(options, extension = '.json') {
|
|
271
|
-
const anchorDir = path.resolve(options.anchorDir);
|
|
272
|
-
const ref = {
|
|
273
|
-
tool: CACHE_TOOL,
|
|
274
|
-
scopeDir: options.monorepoRoot,
|
|
275
|
-
slug: `${path.basename(anchorDir)}-${options.command.replaceAll(UNSAFE_SLUG_CHARACTERS, '-')}`,
|
|
276
|
-
extension,
|
|
277
|
-
discriminators: [anchorDir, options.command],
|
|
278
|
-
};
|
|
279
|
-
return resolveCacheEntryPath(ref);
|
|
280
|
-
}
|
|
281
|
-
function resolveInstallFingerprint(monorepoRoot) {
|
|
282
|
-
const hash = createHash('sha256');
|
|
283
|
-
for (const relativePath of INSTALL_FINGERPRINT_FILES) {
|
|
284
|
-
try {
|
|
285
|
-
hash.update(readFileSync(path.join(monorepoRoot, relativePath)));
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
return { ok: false, reason: `no install fingerprint: ${relativePath} is unreadable` };
|
|
289
|
-
}
|
|
290
|
-
hash.update('\0');
|
|
291
|
-
}
|
|
292
|
-
return { ok: true, fingerprint: hash.digest('hex') };
|
|
87
|
+
return { treeId, headSha };
|
|
293
88
|
}
|