autor3search-typescript 0.1.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/LICENSE +21 -0
- package/README.md +210 -0
- package/dist/benchproto/types.d.ts +32 -0
- package/dist/benchproto/types.js +28 -0
- package/dist/benchproto/types.js.map +1 -0
- package/dist/benchrun/child.d.ts +1 -0
- package/dist/benchrun/child.js +222 -0
- package/dist/benchrun/child.js.map +1 -0
- package/dist/benchrun/invoke.d.ts +61 -0
- package/dist/benchrun/invoke.js +152 -0
- package/dist/benchrun/invoke.js.map +1 -0
- package/dist/cli/cmd-baseline.d.ts +15 -0
- package/dist/cli/cmd-baseline.js +292 -0
- package/dist/cli/cmd-baseline.js.map +1 -0
- package/dist/cli/cmd-doctor.d.ts +11 -0
- package/dist/cli/cmd-doctor.js +26 -0
- package/dist/cli/cmd-doctor.js.map +1 -0
- package/dist/cli/cmd-eval.d.ts +30 -0
- package/dist/cli/cmd-eval.js +173 -0
- package/dist/cli/cmd-eval.js.map +1 -0
- package/dist/cli/cmd-init.d.ts +16 -0
- package/dist/cli/cmd-init.js +343 -0
- package/dist/cli/cmd-init.js.map +1 -0
- package/dist/cli/cmd-profile.d.ts +11 -0
- package/dist/cli/cmd-profile.js +140 -0
- package/dist/cli/cmd-profile.js.map +1 -0
- package/dist/cli/cmd-report.d.ts +15 -0
- package/dist/cli/cmd-report.js +70 -0
- package/dist/cli/cmd-report.js.map +1 -0
- package/dist/cli/cmd-status.d.ts +11 -0
- package/dist/cli/cmd-status.js +143 -0
- package/dist/cli/cmd-status.js.map +1 -0
- package/dist/cli/cmd-stop.d.ts +15 -0
- package/dist/cli/cmd-stop.js +143 -0
- package/dist/cli/cmd-stop.js.map +1 -0
- package/dist/cli/main.d.ts +31 -0
- package/dist/cli/main.js +133 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/runctx.d.ts +29 -0
- package/dist/cli/runctx.js +49 -0
- package/dist/cli/runctx.js.map +1 -0
- package/dist/cli/speedup.d.ts +14 -0
- package/dist/cli/speedup.js +25 -0
- package/dist/cli/speedup.js.map +1 -0
- package/dist/config/load.d.ts +6 -0
- package/dist/config/load.js +172 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/schema.d.ts +42 -0
- package/dist/config/schema.js +25 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/discover/benchmarks.d.ts +26 -0
- package/dist/discover/benchmarks.js +88 -0
- package/dist/discover/benchmarks.js.map +1 -0
- package/dist/discover/files.d.ts +23 -0
- package/dist/discover/files.js +82 -0
- package/dist/discover/files.js.map +1 -0
- package/dist/doctor/doctor.d.ts +56 -0
- package/dist/doctor/doctor.js +255 -0
- package/dist/doctor/doctor.js.map +1 -0
- package/dist/freeze/freeze.d.ts +23 -0
- package/dist/freeze/freeze.js +107 -0
- package/dist/freeze/freeze.js.map +1 -0
- package/dist/freeze/manifest.d.ts +6 -0
- package/dist/freeze/manifest.js +9 -0
- package/dist/freeze/manifest.js.map +1 -0
- package/dist/freeze/safepath.d.ts +8 -0
- package/dist/freeze/safepath.js +22 -0
- package/dist/freeze/safepath.js.map +1 -0
- package/dist/gitx/git.d.ts +87 -0
- package/dist/gitx/git.js +235 -0
- package/dist/gitx/git.js.map +1 -0
- package/dist/measure/interleave.d.ts +32 -0
- package/dist/measure/interleave.js +42 -0
- package/dist/measure/interleave.js.map +1 -0
- package/dist/pipeline/eval.d.ts +67 -0
- package/dist/pipeline/eval.js +495 -0
- package/dist/pipeline/eval.js.map +1 -0
- package/dist/pm/detect.d.ts +17 -0
- package/dist/pm/detect.js +131 -0
- package/dist/pm/detect.js.map +1 -0
- package/dist/profile/profile.d.ts +53 -0
- package/dist/profile/profile.js +105 -0
- package/dist/profile/profile.js.map +1 -0
- package/dist/results/results.d.ts +65 -0
- package/dist/results/results.js +214 -0
- package/dist/results/results.js.map +1 -0
- package/dist/runner/exec.d.ts +69 -0
- package/dist/runner/exec.js +185 -0
- package/dist/runner/exec.js.map +1 -0
- package/dist/scope/scope.d.ts +20 -0
- package/dist/scope/scope.js +70 -0
- package/dist/scope/scope.js.map +1 -0
- package/dist/state/baseline.d.ts +35 -0
- package/dist/state/baseline.js +38 -0
- package/dist/state/baseline.js.map +1 -0
- package/dist/state/home.d.ts +9 -0
- package/dist/state/home.js +72 -0
- package/dist/state/home.js.map +1 -0
- package/dist/state/lock.d.ts +11 -0
- package/dist/state/lock.js +89 -0
- package/dist/state/lock.js.map +1 -0
- package/dist/state/runnaming.d.ts +27 -0
- package/dist/state/runnaming.js +47 -0
- package/dist/state/runnaming.js.map +1 -0
- package/dist/state/stop.d.ts +28 -0
- package/dist/state/stop.js +59 -0
- package/dist/state/stop.js.map +1 -0
- package/dist/stats/delta.d.ts +29 -0
- package/dist/stats/delta.js +41 -0
- package/dist/stats/delta.js.map +1 -0
- package/dist/stats/geomean.d.ts +4 -0
- package/dist/stats/geomean.js +21 -0
- package/dist/stats/geomean.js.map +1 -0
- package/dist/stats/mannwhitney.d.ts +27 -0
- package/dist/stats/mannwhitney.js +158 -0
- package/dist/stats/mannwhitney.js.map +1 -0
- package/dist/verdict/verdict.d.ts +46 -0
- package/dist/verdict/verdict.js +102 -0
- package/dist/verdict/verdict.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +35 -0
- package/templates/program.md +131 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Largest output we retain per stream, in characters (UTF-16 code units),
|
|
3
|
+
* not bytes. The cap exists to bound retained *memory*, and these are
|
|
4
|
+
* JavaScript strings living in V8 — which stores them as one-byte (latin1)
|
|
5
|
+
* or two-byte sequences, so retained memory tracks `.length` (code units),
|
|
6
|
+
* not UTF-8 byte length. Measuring in UTF-8 bytes would make the name
|
|
7
|
+
* accurate but the bound wrong: for a two-byte-represented string, the
|
|
8
|
+
* UTF-8 length can be smaller than the memory actually held, undercounting
|
|
9
|
+
* exactly where the bound matters most. Beyond this many characters we keep
|
|
10
|
+
* the tail.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_CAPTURED_CHARS: number;
|
|
13
|
+
export interface ExecOptions {
|
|
14
|
+
cwd: string;
|
|
15
|
+
timeoutMs: number;
|
|
16
|
+
env?: NodeJS.ProcessEnv;
|
|
17
|
+
/** Receives output as it arrives, for run.log. */
|
|
18
|
+
log?: (chunk: string) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface ExecResult {
|
|
21
|
+
exitCode: number;
|
|
22
|
+
signal: NodeJS.Signals | null;
|
|
23
|
+
stdout: string;
|
|
24
|
+
stderr: string;
|
|
25
|
+
timedOut: boolean;
|
|
26
|
+
truncated: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** A result is OK only if it exited zero, was not signalled, and did not time out. */
|
|
29
|
+
export declare function ok(r: ExecResult): boolean;
|
|
30
|
+
/** The last `lines` lines of `s`. */
|
|
31
|
+
export declare function tail(s: string, lines: number): string;
|
|
32
|
+
/**
|
|
33
|
+
* Accumulates output while keeping only the tail once the cap is exceeded.
|
|
34
|
+
*
|
|
35
|
+
* Exported for testing: the eviction and single-chunk-trim logic below is
|
|
36
|
+
* only reliably exercised by calling `push` directly — a real subprocess's
|
|
37
|
+
* output arrives from the OS pipe in fragments (observed ~64 KiB on this
|
|
38
|
+
* machine for a 4 MiB+ write), so driving it through a spawned process
|
|
39
|
+
* cannot be trusted to ever deliver a single chunk larger than the cap.
|
|
40
|
+
*/
|
|
41
|
+
export declare class Capture {
|
|
42
|
+
private chunks;
|
|
43
|
+
private size;
|
|
44
|
+
truncated: boolean;
|
|
45
|
+
push(s: string): void;
|
|
46
|
+
toString(): string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Signals a process group, refusing to signal pid 1's group.
|
|
50
|
+
*
|
|
51
|
+
* On Linux a process whose group leader has exited can report ppid 1, and
|
|
52
|
+
* `kill(-1, ...)` means "every process the user may signal" — which in a
|
|
53
|
+
* container is everything. Refusing is strictly better than the alternative.
|
|
54
|
+
*
|
|
55
|
+
* Exported: `cli/cmd-stop.ts`'s `stop -force` needs this exact same refusal
|
|
56
|
+
* to signal the pid recorded in `eval.lock`. That is the same function, not
|
|
57
|
+
* two views of one enumeration (contrast the lockfile `switch` in
|
|
58
|
+
* `pm/detect.ts`, which genuinely cannot be imported across a `switch`) --
|
|
59
|
+
* so the duplication is eliminated by importing this rather than guarded by
|
|
60
|
+
* a second copy plus a comment. A future edit to the pid-1 refusal (e.g.
|
|
61
|
+
* hardening it for a new edge case) now cannot diverge between the two call
|
|
62
|
+
* sites by simply being forgotten in one of them.
|
|
63
|
+
*/
|
|
64
|
+
export declare function killGroup(pid: number | undefined, signal: NodeJS.Signals): void;
|
|
65
|
+
/** Signals every currently-active child's process group, best effort. */
|
|
66
|
+
export declare function killActiveChildren(signal: NodeJS.Signals): void;
|
|
67
|
+
export declare function run(cmd: string, args: string[], opts: ExecOptions): Promise<ExecResult>;
|
|
68
|
+
/** Runs a full command line through the platform shell. */
|
|
69
|
+
export declare function runShell(command: string, opts: ExecOptions): Promise<ExecResult>;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Largest output we retain per stream, in characters (UTF-16 code units),
|
|
4
|
+
* not bytes. The cap exists to bound retained *memory*, and these are
|
|
5
|
+
* JavaScript strings living in V8 — which stores them as one-byte (latin1)
|
|
6
|
+
* or two-byte sequences, so retained memory tracks `.length` (code units),
|
|
7
|
+
* not UTF-8 byte length. Measuring in UTF-8 bytes would make the name
|
|
8
|
+
* accurate but the bound wrong: for a two-byte-represented string, the
|
|
9
|
+
* UTF-8 length can be smaller than the memory actually held, undercounting
|
|
10
|
+
* exactly where the bound matters most. Beyond this many characters we keep
|
|
11
|
+
* the tail.
|
|
12
|
+
*/
|
|
13
|
+
export const MAX_CAPTURED_CHARS = 4 * 1024 * 1024;
|
|
14
|
+
/** A result is OK only if it exited zero, was not signalled, and did not time out. */
|
|
15
|
+
export function ok(r) {
|
|
16
|
+
return r.exitCode === 0 && r.signal === null && !r.timedOut;
|
|
17
|
+
}
|
|
18
|
+
/** The last `lines` lines of `s`. */
|
|
19
|
+
export function tail(s, lines) {
|
|
20
|
+
const parts = s.split('\n');
|
|
21
|
+
return parts.slice(Math.max(0, parts.length - lines)).join('\n');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Accumulates output while keeping only the tail once the cap is exceeded.
|
|
25
|
+
*
|
|
26
|
+
* Exported for testing: the eviction and single-chunk-trim logic below is
|
|
27
|
+
* only reliably exercised by calling `push` directly — a real subprocess's
|
|
28
|
+
* output arrives from the OS pipe in fragments (observed ~64 KiB on this
|
|
29
|
+
* machine for a 4 MiB+ write), so driving it through a spawned process
|
|
30
|
+
* cannot be trusted to ever deliver a single chunk larger than the cap.
|
|
31
|
+
*/
|
|
32
|
+
export class Capture {
|
|
33
|
+
chunks = [];
|
|
34
|
+
size = 0;
|
|
35
|
+
truncated = false;
|
|
36
|
+
push(s) {
|
|
37
|
+
if (s.length > MAX_CAPTURED_CHARS) {
|
|
38
|
+
// A single incoming chunk can itself exceed the whole cap (a chatty
|
|
39
|
+
// process writing in one huge call, or a pipe that happens to hand us
|
|
40
|
+
// a large read). Chunk-granularity eviction below can't bound this —
|
|
41
|
+
// with only one chunk held there is nothing to evict — so trim this
|
|
42
|
+
// string's own tail directly. Its last MAX_CAPTURED_CHARS characters
|
|
43
|
+
// are also the most recent MAX_CAPTURED_CHARS characters of the whole
|
|
44
|
+
// stream, so anything held from before this chunk is now stale and is
|
|
45
|
+
// discarded along with the front of this one.
|
|
46
|
+
this.chunks = [s.slice(s.length - MAX_CAPTURED_CHARS)];
|
|
47
|
+
this.size = MAX_CAPTURED_CHARS;
|
|
48
|
+
this.truncated = true;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.chunks.push(s);
|
|
52
|
+
this.size += s.length;
|
|
53
|
+
while (this.size > MAX_CAPTURED_CHARS && this.chunks.length > 1) {
|
|
54
|
+
this.size -= this.chunks.shift().length;
|
|
55
|
+
this.truncated = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
toString() {
|
|
59
|
+
return this.chunks.join('');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Signals a process group, refusing to signal pid 1's group.
|
|
64
|
+
*
|
|
65
|
+
* On Linux a process whose group leader has exited can report ppid 1, and
|
|
66
|
+
* `kill(-1, ...)` means "every process the user may signal" — which in a
|
|
67
|
+
* container is everything. Refusing is strictly better than the alternative.
|
|
68
|
+
*
|
|
69
|
+
* Exported: `cli/cmd-stop.ts`'s `stop -force` needs this exact same refusal
|
|
70
|
+
* to signal the pid recorded in `eval.lock`. That is the same function, not
|
|
71
|
+
* two views of one enumeration (contrast the lockfile `switch` in
|
|
72
|
+
* `pm/detect.ts`, which genuinely cannot be imported across a `switch`) --
|
|
73
|
+
* so the duplication is eliminated by importing this rather than guarded by
|
|
74
|
+
* a second copy plus a comment. A future edit to the pid-1 refusal (e.g.
|
|
75
|
+
* hardening it for a new edge case) now cannot diverge between the two call
|
|
76
|
+
* sites by simply being forgotten in one of them.
|
|
77
|
+
*/
|
|
78
|
+
export function killGroup(pid, signal) {
|
|
79
|
+
if (pid === undefined || pid <= 1)
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
process.kill(-pid, signal);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// The group is already gone; fall back to the single process.
|
|
86
|
+
try {
|
|
87
|
+
process.kill(pid, signal);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* already dead */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Every measurement/typecheck/build/test child currently spawned by THIS
|
|
96
|
+
* process, so a signal handler can reach them without any pipeline-layer
|
|
97
|
+
* plumbing. Load-bearing for `stop -force`: `eval.ts` spawns each
|
|
98
|
+
* measurement child `detached: true` so it is its own process-group leader
|
|
99
|
+
* (see `exec` below), which means it is NOT killed for free when `eval`
|
|
100
|
+
* itself receives SIGTERM. Left unhandled, `stop -force`'s SIGTERM to the
|
|
101
|
+
* eval process kills node immediately (SIGTERM's default disposition) with
|
|
102
|
+
* the benchmark child still running and the eval lock still held -- the
|
|
103
|
+
* exact orphaned-process hazard spec section 2 item 4 names explicitly.
|
|
104
|
+
*/
|
|
105
|
+
const activeChildPids = new Set();
|
|
106
|
+
/** Signals every currently-active child's process group, best effort. */
|
|
107
|
+
export function killActiveChildren(signal) {
|
|
108
|
+
for (const pid of activeChildPids)
|
|
109
|
+
killGroup(pid, signal);
|
|
110
|
+
}
|
|
111
|
+
function exec(cmd, args, opts, useShell) {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
const stdout = new Capture();
|
|
114
|
+
const stderr = new Capture();
|
|
115
|
+
let timedOut = false;
|
|
116
|
+
let settled = false;
|
|
117
|
+
const child = spawn(cmd, args, {
|
|
118
|
+
cwd: opts.cwd,
|
|
119
|
+
env: opts.env ?? process.env,
|
|
120
|
+
shell: useShell,
|
|
121
|
+
// A new process group is what makes killGroup able to reach
|
|
122
|
+
// grandchildren. Without it, a benchmark spawned by a test runner
|
|
123
|
+
// survives the timeout and corrupts every later measurement.
|
|
124
|
+
detached: process.platform !== 'win32',
|
|
125
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
126
|
+
});
|
|
127
|
+
if (child.pid !== undefined)
|
|
128
|
+
activeChildPids.add(child.pid);
|
|
129
|
+
// A throwing `log` must not take down an unattended overnight run —
|
|
130
|
+
// losing a log line is fine, losing the whole exec because a log
|
|
131
|
+
// stream closed or a disk filled is not.
|
|
132
|
+
const safeLog = (c) => {
|
|
133
|
+
try {
|
|
134
|
+
opts.log?.(c);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* a broken logger must not break the run it is only observing */
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
child.stdout?.setEncoding('utf8');
|
|
141
|
+
child.stderr?.setEncoding('utf8');
|
|
142
|
+
child.stdout?.on('data', (c) => {
|
|
143
|
+
stdout.push(c);
|
|
144
|
+
safeLog(c);
|
|
145
|
+
});
|
|
146
|
+
child.stderr?.on('data', (c) => {
|
|
147
|
+
stderr.push(c);
|
|
148
|
+
safeLog(c);
|
|
149
|
+
});
|
|
150
|
+
const timer = setTimeout(() => {
|
|
151
|
+
timedOut = true;
|
|
152
|
+
killGroup(child.pid, 'SIGKILL');
|
|
153
|
+
}, opts.timeoutMs);
|
|
154
|
+
const finish = (exitCode, signal) => {
|
|
155
|
+
if (settled)
|
|
156
|
+
return;
|
|
157
|
+
settled = true;
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
if (child.pid !== undefined)
|
|
160
|
+
activeChildPids.delete(child.pid);
|
|
161
|
+
resolve({
|
|
162
|
+
exitCode,
|
|
163
|
+
signal,
|
|
164
|
+
stdout: stdout.toString(),
|
|
165
|
+
stderr: stderr.toString(),
|
|
166
|
+
timedOut,
|
|
167
|
+
truncated: stdout.truncated || stderr.truncated,
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
// A missing binary is a result the caller reasons about, not a crash.
|
|
171
|
+
child.on('error', (e) => {
|
|
172
|
+
stderr.push(e.message);
|
|
173
|
+
finish(127, null);
|
|
174
|
+
});
|
|
175
|
+
child.on('close', (code, signal) => finish(code ?? 0, signal));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
export function run(cmd, args, opts) {
|
|
179
|
+
return exec(cmd, args, opts, false);
|
|
180
|
+
}
|
|
181
|
+
/** Runs a full command line through the platform shell. */
|
|
182
|
+
export function runShell(command, opts) {
|
|
183
|
+
return exec(command, [], opts, true);
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=exec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/runner/exec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ScopeViolation {
|
|
2
|
+
file: string;
|
|
3
|
+
reason: 'immutable' | 'out-of-scope';
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Compiles a glob to an anchored regexp.
|
|
7
|
+
*
|
|
8
|
+
* `**` crosses directory separators, `*` and `?` do not. Everything else is
|
|
9
|
+
* escaped: a pattern is data from a config file, and a stray `.` or `+`
|
|
10
|
+
* silently widening the scope is exactly the failure this gate must not have.
|
|
11
|
+
*/
|
|
12
|
+
export declare function matchGlob(pattern: string, p: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Every reason the changed set is not allowed.
|
|
15
|
+
*
|
|
16
|
+
* Immutability is checked first and independently of `scope`: a dependency or
|
|
17
|
+
* compiler-configuration change is a human decision, and no scope setting may
|
|
18
|
+
* grant it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function checkScope(changed: string[], scope: string[]): ScopeViolation[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CONFIG_PATH, IMMUTABLE_FILES } from '../config/schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* Compiles a glob to an anchored regexp.
|
|
4
|
+
*
|
|
5
|
+
* `**` crosses directory separators, `*` and `?` do not. Everything else is
|
|
6
|
+
* escaped: a pattern is data from a config file, and a stray `.` or `+`
|
|
7
|
+
* silently widening the scope is exactly the failure this gate must not have.
|
|
8
|
+
*/
|
|
9
|
+
export function matchGlob(pattern, p) {
|
|
10
|
+
let re = '';
|
|
11
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
12
|
+
const c = pattern[i];
|
|
13
|
+
if (c === '*') {
|
|
14
|
+
if (pattern[i + 1] === '*') {
|
|
15
|
+
// `**/` also matches zero directories, so `src/**` matches `src/a.ts`.
|
|
16
|
+
if (pattern[i + 2] === '/') {
|
|
17
|
+
re += '(?:.*/)?';
|
|
18
|
+
i += 2;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
re += '.*';
|
|
22
|
+
i += 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
re += '[^/]*';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (c === '?') {
|
|
30
|
+
re += '[^/]';
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
re += c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return new RegExp(`^${re}$`).test(p);
|
|
37
|
+
}
|
|
38
|
+
const IMMUTABLE = new Set(IMMUTABLE_FILES);
|
|
39
|
+
/**
|
|
40
|
+
* Every reason the changed set is not allowed.
|
|
41
|
+
*
|
|
42
|
+
* Immutability is checked first and independently of `scope`: a dependency or
|
|
43
|
+
* compiler-configuration change is a human decision, and no scope setting may
|
|
44
|
+
* grant it.
|
|
45
|
+
*/
|
|
46
|
+
export function checkScope(changed, scope) {
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const file of changed) {
|
|
49
|
+
// `.autor3search/config.yaml` is now a tracked, committed file (spec
|
|
50
|
+
// section 13), so any edit to it is visible to `changedFiles` like any
|
|
51
|
+
// other file -- but it already has its own dedicated, more specific
|
|
52
|
+
// protection: `pipeline/eval.ts` gate 2 hashes it against what baseline
|
|
53
|
+
// recorded and FAILs with a diagnosis naming the actual problem
|
|
54
|
+
// ("the run configuration is frozen"), not a generic scope violation.
|
|
55
|
+
// Exempting it here just lets that more specific gate be the one that
|
|
56
|
+
// actually reports it, rather than racing scope to the same conclusion
|
|
57
|
+
// with a less useful message.
|
|
58
|
+
if (file === CONFIG_PATH)
|
|
59
|
+
continue;
|
|
60
|
+
if (IMMUTABLE.has(file)) {
|
|
61
|
+
out.push({ file, reason: 'immutable' });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!scope.some((pat) => matchGlob(pat, file))) {
|
|
65
|
+
out.push({ file, reason: 'out-of-scope' });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope.js","sourceRoot":"","sources":["../../src/scope/scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAOlE;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,OAAe,EAAE,CAAS;IAClD,IAAI,EAAE,GAAG,EAAE,CAAA;IACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAE,CAAA;QACrB,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACd,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC3B,uEAAuE;gBACvE,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBAC3B,EAAE,IAAI,UAAU,CAAA;oBAChB,CAAC,IAAI,CAAC,CAAA;gBACR,CAAC;qBAAM,CAAC;oBACN,EAAE,IAAI,IAAI,CAAA;oBACV,CAAC,IAAI,CAAC,CAAA;gBACR,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,EAAE,IAAI,OAAO,CAAA;YACf,CAAC;QACH,CAAC;aAAM,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACrB,EAAE,IAAI,MAAM,CAAA;QACd,CAAC;aAAM,CAAC;YACN,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAA;QAChD,CAAC;IACH,CAAC;IACD,OAAO,IAAI,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACtC,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,eAAe,CAAC,CAAA;AAE1C;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,OAAiB,EAAE,KAAe;IAC3D,MAAM,GAAG,GAAqB,EAAE,CAAA;IAChC,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,qEAAqE;QACrE,uEAAuE;QACvE,oEAAoE;QACpE,wEAAwE;QACxE,gEAAgE;QAChE,sEAAsE;QACtE,sEAAsE;QACtE,uEAAuE;QACvE,8BAA8B;QAC9B,IAAI,IAAI,KAAK,WAAW;YAAE,SAAQ;QAClC,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAA;YACvC,SAAQ;QACV,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC;YAC/C,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAA;QAC5C,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Manifest } from '../freeze/manifest.js';
|
|
2
|
+
export interface BaselineRecord {
|
|
3
|
+
tag: string;
|
|
4
|
+
/**
|
|
5
|
+
* The commit the frozen tests and the scope gate always compare against.
|
|
6
|
+
* NEVER advances once the baseline is created. Moving it would move the
|
|
7
|
+
* success criteria mid-run — this is the field that, in the Go predecessor,
|
|
8
|
+
* got collapsed into a single "commit" and let a tampered experiment KEEP.
|
|
9
|
+
*/
|
|
10
|
+
frozenCommit: string;
|
|
11
|
+
/**
|
|
12
|
+
* The commit base timings are measured against. Advances to the candidate
|
|
13
|
+
* commit after every KEEP, so each subsequent score reflects that
|
|
14
|
+
* experiment's own contribution rather than re-measuring an earlier win.
|
|
15
|
+
* Advancing this field must never change frozenCommit.
|
|
16
|
+
*/
|
|
17
|
+
measureCommit: string;
|
|
18
|
+
configHash: string;
|
|
19
|
+
lockfileHash: string;
|
|
20
|
+
lockfileName: string;
|
|
21
|
+
benchmarks: string[];
|
|
22
|
+
manifest: Manifest;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function baselinePath(dir: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Writes via a temp file plus rename rather than truncating in place.
|
|
28
|
+
* baseline.json is read by every subsequent eval; a crash mid-truncate would
|
|
29
|
+
* leave a half-written (or empty) file that every future eval trips over. A
|
|
30
|
+
* rename within the same directory is atomic on the filesystems this tool
|
|
31
|
+
* targets, so a reader always sees either the old complete record or the new
|
|
32
|
+
* one, never a partial write.
|
|
33
|
+
*/
|
|
34
|
+
export declare function writeBaseline(dir: string, rec: BaselineRecord): Promise<void>;
|
|
35
|
+
export declare function readBaseline(dir: string): Promise<BaselineRecord>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export function baselinePath(dir) {
|
|
5
|
+
return path.join(dir, 'baseline.json');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Writes via a temp file plus rename rather than truncating in place.
|
|
9
|
+
* baseline.json is read by every subsequent eval; a crash mid-truncate would
|
|
10
|
+
* leave a half-written (or empty) file that every future eval trips over. A
|
|
11
|
+
* rename within the same directory is atomic on the filesystems this tool
|
|
12
|
+
* targets, so a reader always sees either the old complete record or the new
|
|
13
|
+
* one, never a partial write.
|
|
14
|
+
*/
|
|
15
|
+
export async function writeBaseline(dir, rec) {
|
|
16
|
+
await mkdir(dir, { recursive: true });
|
|
17
|
+
const dest = baselinePath(dir);
|
|
18
|
+
const tmpPath = path.join(dir, `.baseline.json.${process.pid}.${randomUUID()}.tmp`);
|
|
19
|
+
await writeFile(tmpPath, `${JSON.stringify(rec, null, 2)}\n`);
|
|
20
|
+
try {
|
|
21
|
+
await rename(tmpPath, dest);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
await rm(tmpPath, { force: true });
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function readBaseline(dir) {
|
|
29
|
+
let text;
|
|
30
|
+
try {
|
|
31
|
+
text = await readFile(baselinePath(dir), 'utf8');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error(`no baseline found at ${baselinePath(dir)}. Run "autor3search-typescript baseline -tag <tag>" first.`);
|
|
35
|
+
}
|
|
36
|
+
return JSON.parse(text);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=baseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../src/state/baseline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACzE,OAAO,IAAI,MAAM,WAAW,CAAA;AA2B5B,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAA;AACxC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAW,EAAE,GAAmB;IAClE,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACrC,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAA;IAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAkB,OAAO,CAAC,GAAG,IAAI,UAAU,EAAE,MAAM,CAAC,CAAA;IACnF,MAAM,SAAS,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;IAC7D,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IAC7B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAClC,MAAM,GAAG,CAAA;IACX,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAW;IAC5C,IAAI,IAAY,CAAA;IAChB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAA;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,wBAAwB,YAAY,CAAC,GAAG,CAAC,4DAA4D,CACtG,CAAA;IACH,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAmB,CAAA;AAC3C,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const STATE_HOME_ENV = "AUTOR3SEARCH_TYPESCRIPT_STATE_HOME";
|
|
2
|
+
/**
|
|
3
|
+
* The root directory everything the metric depends on lives under, deliberately
|
|
4
|
+
* outside the repository the agent controls: baselines, eval locks and stop
|
|
5
|
+
* requests. If the agent could write any of this, it could grade its own work.
|
|
6
|
+
*/
|
|
7
|
+
export declare function stateHome(env?: NodeJS.ProcessEnv): string;
|
|
8
|
+
/** The per-repository, per-tag state directory. */
|
|
9
|
+
export declare function runDir(repoRoot: string, tag: string, env?: NodeJS.ProcessEnv): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { hashString } from '../freeze/manifest.js';
|
|
5
|
+
export const STATE_HOME_ENV = 'AUTOR3SEARCH_TYPESCRIPT_STATE_HOME';
|
|
6
|
+
const APP = 'autor3search-typescript';
|
|
7
|
+
/** Tags become directory names, so they must be a single safe segment. */
|
|
8
|
+
const TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
9
|
+
/**
|
|
10
|
+
* The root directory everything the metric depends on lives under, deliberately
|
|
11
|
+
* outside the repository the agent controls: baselines, eval locks and stop
|
|
12
|
+
* requests. If the agent could write any of this, it could grade its own work.
|
|
13
|
+
*/
|
|
14
|
+
export function stateHome(env = process.env) {
|
|
15
|
+
const override = env[STATE_HOME_ENV];
|
|
16
|
+
if (override !== undefined && override !== '') {
|
|
17
|
+
if (!path.isAbsolute(override)) {
|
|
18
|
+
// A relative value resolves against whatever directory the current command
|
|
19
|
+
// happens to run from. `eval` invoked from a subdirectory and `stop` invoked
|
|
20
|
+
// from the repo root would then address different state for the same run,
|
|
21
|
+
// and the stop would silently appear to do nothing.
|
|
22
|
+
throw new Error(`${STATE_HOME_ENV} must be an absolute path, got ${JSON.stringify(override)}: a relative ` +
|
|
23
|
+
"value resolves against each command's working directory, so eval and stop could " +
|
|
24
|
+
'address different state for the same run');
|
|
25
|
+
}
|
|
26
|
+
return override;
|
|
27
|
+
}
|
|
28
|
+
if (process.platform === 'darwin')
|
|
29
|
+
return path.join(homedir(), 'Library', 'Caches');
|
|
30
|
+
if (process.platform === 'win32') {
|
|
31
|
+
return env['LOCALAPPDATA'] ?? path.join(homedir(), 'AppData', 'Local');
|
|
32
|
+
}
|
|
33
|
+
return env['XDG_CACHE_HOME'] ?? path.join(homedir(), '.cache');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Canonicalizes a repo path so the same repository keys identically no matter
|
|
37
|
+
* how it was reached: a trailing slash, a relative path, or a symlinked parent
|
|
38
|
+
* directory. `realpath` requires the path to exist; when it doesn't (as in
|
|
39
|
+
* tests that key against a repo root that was never created on disk) we fall
|
|
40
|
+
* back to the syntactic resolution rather than failing.
|
|
41
|
+
*
|
|
42
|
+
* The fallback is ONLY for ENOENT. Any other realpath failure (EACCES on a
|
|
43
|
+
* symlinked ancestor, ELOOP, ESTALE, ...) must propagate rather than silently
|
|
44
|
+
* fall back to the syntactic path: `baseline` might run as the repo's owner
|
|
45
|
+
* and key on the true canonical path, while `eval` later runs under a
|
|
46
|
+
* different uid or a sandbox with restricted traversal on that same ancestor,
|
|
47
|
+
* hits EACCES, and -- if we swallowed it -- would fall back to a differently-
|
|
48
|
+
* hashed syntactic path and report "no baseline" for a run that exists. That
|
|
49
|
+
* is the exact same stranded-state failure the canonicalization exists to
|
|
50
|
+
* prevent, reintroduced through its own fallback.
|
|
51
|
+
*/
|
|
52
|
+
function canonicalRepoPath(repoRoot) {
|
|
53
|
+
const abs = path.resolve(repoRoot);
|
|
54
|
+
try {
|
|
55
|
+
return realpathSync(abs);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err.code === 'ENOENT')
|
|
59
|
+
return abs;
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** The per-repository, per-tag state directory. */
|
|
64
|
+
export function runDir(repoRoot, tag, env = process.env) {
|
|
65
|
+
if (!TAG_RE.test(tag)) {
|
|
66
|
+
throw new Error(`invalid tag ${JSON.stringify(tag)}: use letters, digits, dot, dash or underscore only ` +
|
|
67
|
+
'(a tag becomes a directory name)');
|
|
68
|
+
}
|
|
69
|
+
const key = hashString(canonicalRepoPath(repoRoot)).slice(0, 16);
|
|
70
|
+
return path.join(stateHome(env), APP, key, tag);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=home.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"home.js","sourceRoot":"","sources":["../../src/state/home.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAElD,MAAM,CAAC,MAAM,cAAc,GAAG,oCAAoC,CAAA;AAElE,MAAM,GAAG,GAAG,yBAAyB,CAAA;AAErC,0EAA0E;AAC1E,MAAM,MAAM,GAAG,mCAAmC,CAAA;AAElD;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC5D,MAAM,QAAQ,GAAG,GAAG,CAAC,cAAc,CAAC,CAAA;IACpC,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/B,2EAA2E;YAC3E,6EAA6E;YAC7E,0EAA0E;YAC1E,oDAAoD;YACpD,MAAM,IAAI,KAAK,CACb,GAAG,cAAc,kCAAkC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,eAAe;gBACxF,kFAAkF;gBAClF,0CAA0C,CAC7C,CAAA;QACH,CAAC;QACD,OAAO,QAAQ,CAAA;IACjB,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAA;IACnF,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,OAAO,GAAG,CAAC,cAAc,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAA;IACxE,CAAC;IACD,OAAO,GAAG,CAAC,gBAAgB,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAA;AAChE,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,SAAS,iBAAiB,CAAC,QAAgB;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IAClC,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,GAAG,CAAA;QAChE,MAAM,GAAG,CAAA;IACX,CAAC;AACH,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,MAAM,CACpB,QAAgB,EAChB,GAAW,EACX,MAAyB,OAAO,CAAC,GAAG;IAEpC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,eAAe,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,sDAAsD;YACtF,kCAAkC,CACrC,CAAA;IACH,CAAC;IACD,MAAM,GAAG,GAAG,UAAU,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAChE,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;AACjD,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function lockPath(dir: string): string;
|
|
2
|
+
export declare function readEvalLock(dir: string): Promise<{
|
|
3
|
+
pid: number;
|
|
4
|
+
} | null>;
|
|
5
|
+
/**
|
|
6
|
+
* Acquires the per-run eval lock, refusing a concurrent eval for the same
|
|
7
|
+
* repo and tag. A crashed eval leaves its lock file behind with no process
|
|
8
|
+
* to clean it up; if the recorded pid is no longer alive the lock is stale
|
|
9
|
+
* and is reclaimed, so a crash never permanently blocks future evals.
|
|
10
|
+
*/
|
|
11
|
+
export declare function acquireEvalLock(dir: string): Promise<() => Promise<void>>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function lockPath(dir) {
|
|
4
|
+
return path.join(dir, 'eval.lock');
|
|
5
|
+
}
|
|
6
|
+
function isAlive(pid) {
|
|
7
|
+
try {
|
|
8
|
+
// Signal 0 sends nothing; it only probes whether the pid is signalable.
|
|
9
|
+
process.kill(pid, 0);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
// EPERM means the process exists but we lack permission to signal it --
|
|
14
|
+
// that is still "alive" and definitely not ours to reclaim. Any other
|
|
15
|
+
// error (ESRCH: no such process) means it is dead.
|
|
16
|
+
return err.code === 'EPERM';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function readEvalLock(dir) {
|
|
20
|
+
let text;
|
|
21
|
+
try {
|
|
22
|
+
text = await readFile(lockPath(dir), 'utf8');
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (err.code === 'ENOENT')
|
|
26
|
+
return null;
|
|
27
|
+
// Any other read failure (EACCES, a different uid owns the file, a
|
|
28
|
+
// transient I/O error, ...) does NOT mean the lock is absent -- it means
|
|
29
|
+
// we could not check. Collapsing that into "absent" is how a genuinely
|
|
30
|
+
// held, live lock gets stolen out from under its owner. Propagate it so
|
|
31
|
+
// the caller never treats "couldn't read" as "safe to reclaim".
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(text);
|
|
36
|
+
return { pid: parsed.pid };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// The read itself succeeded; the content is corrupt or half-written
|
|
40
|
+
// (e.g. a crash mid-write). That is genuinely indistinguishable from
|
|
41
|
+
// absent, so it is treated as reclaimable rather than wedging every
|
|
42
|
+
// future eval.
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Acquires the per-run eval lock, refusing a concurrent eval for the same
|
|
48
|
+
* repo and tag. A crashed eval leaves its lock file behind with no process
|
|
49
|
+
* to clean it up; if the recorded pid is no longer alive the lock is stale
|
|
50
|
+
* and is reclaimed, so a crash never permanently blocks future evals.
|
|
51
|
+
*/
|
|
52
|
+
export async function acquireEvalLock(dir) {
|
|
53
|
+
await mkdir(dir, { recursive: true });
|
|
54
|
+
const file = lockPath(dir);
|
|
55
|
+
const record = { pid: process.pid, startedAt: new Date().toISOString() };
|
|
56
|
+
for (;;) {
|
|
57
|
+
try {
|
|
58
|
+
await writeFile(file, JSON.stringify(record), { flag: 'wx' });
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (err.code !== 'EEXIST')
|
|
63
|
+
throw err;
|
|
64
|
+
// readEvalLock throws on anything other than "confirmed absent" or
|
|
65
|
+
// "confirmed corrupt" (see its doc comment); that throw propagates out
|
|
66
|
+
// of this function untouched, so a lock we failed to read is never
|
|
67
|
+
// reclaimed here.
|
|
68
|
+
const existing = await readEvalLock(dir);
|
|
69
|
+
if (existing !== null && isAlive(existing.pid)) {
|
|
70
|
+
throw new Error(`eval lock is already held by pid ${existing.pid}; wait for it to finish, or run ` +
|
|
71
|
+
'"stop" to request it stop');
|
|
72
|
+
}
|
|
73
|
+
// existing is null here only because the file was confirmed absent (a
|
|
74
|
+
// race: it existed for writeFile's wx check but was gone by the time we
|
|
75
|
+
// read it) or its content was corrupt/half-written. Either way it is
|
|
76
|
+
// safe to reclaim. A dead-pid lock falls through the isAlive check
|
|
77
|
+
// above instead.
|
|
78
|
+
await rm(file, { force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let released = false;
|
|
82
|
+
return async () => {
|
|
83
|
+
if (released)
|
|
84
|
+
return;
|
|
85
|
+
released = true;
|
|
86
|
+
await rm(file, { force: true });
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/state/lock.ts"],"names":[],"mappings":"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"}
|