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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/dist/benchproto/types.d.ts +32 -0
  4. package/dist/benchproto/types.js +28 -0
  5. package/dist/benchproto/types.js.map +1 -0
  6. package/dist/benchrun/child.d.ts +1 -0
  7. package/dist/benchrun/child.js +222 -0
  8. package/dist/benchrun/child.js.map +1 -0
  9. package/dist/benchrun/invoke.d.ts +61 -0
  10. package/dist/benchrun/invoke.js +152 -0
  11. package/dist/benchrun/invoke.js.map +1 -0
  12. package/dist/cli/cmd-baseline.d.ts +15 -0
  13. package/dist/cli/cmd-baseline.js +292 -0
  14. package/dist/cli/cmd-baseline.js.map +1 -0
  15. package/dist/cli/cmd-doctor.d.ts +11 -0
  16. package/dist/cli/cmd-doctor.js +26 -0
  17. package/dist/cli/cmd-doctor.js.map +1 -0
  18. package/dist/cli/cmd-eval.d.ts +30 -0
  19. package/dist/cli/cmd-eval.js +173 -0
  20. package/dist/cli/cmd-eval.js.map +1 -0
  21. package/dist/cli/cmd-init.d.ts +16 -0
  22. package/dist/cli/cmd-init.js +343 -0
  23. package/dist/cli/cmd-init.js.map +1 -0
  24. package/dist/cli/cmd-profile.d.ts +11 -0
  25. package/dist/cli/cmd-profile.js +140 -0
  26. package/dist/cli/cmd-profile.js.map +1 -0
  27. package/dist/cli/cmd-report.d.ts +15 -0
  28. package/dist/cli/cmd-report.js +70 -0
  29. package/dist/cli/cmd-report.js.map +1 -0
  30. package/dist/cli/cmd-status.d.ts +11 -0
  31. package/dist/cli/cmd-status.js +143 -0
  32. package/dist/cli/cmd-status.js.map +1 -0
  33. package/dist/cli/cmd-stop.d.ts +15 -0
  34. package/dist/cli/cmd-stop.js +143 -0
  35. package/dist/cli/cmd-stop.js.map +1 -0
  36. package/dist/cli/main.d.ts +31 -0
  37. package/dist/cli/main.js +133 -0
  38. package/dist/cli/main.js.map +1 -0
  39. package/dist/cli/runctx.d.ts +29 -0
  40. package/dist/cli/runctx.js +49 -0
  41. package/dist/cli/runctx.js.map +1 -0
  42. package/dist/cli/speedup.d.ts +14 -0
  43. package/dist/cli/speedup.js +25 -0
  44. package/dist/cli/speedup.js.map +1 -0
  45. package/dist/config/load.d.ts +6 -0
  46. package/dist/config/load.js +172 -0
  47. package/dist/config/load.js.map +1 -0
  48. package/dist/config/schema.d.ts +42 -0
  49. package/dist/config/schema.js +25 -0
  50. package/dist/config/schema.js.map +1 -0
  51. package/dist/discover/benchmarks.d.ts +26 -0
  52. package/dist/discover/benchmarks.js +88 -0
  53. package/dist/discover/benchmarks.js.map +1 -0
  54. package/dist/discover/files.d.ts +23 -0
  55. package/dist/discover/files.js +82 -0
  56. package/dist/discover/files.js.map +1 -0
  57. package/dist/doctor/doctor.d.ts +56 -0
  58. package/dist/doctor/doctor.js +255 -0
  59. package/dist/doctor/doctor.js.map +1 -0
  60. package/dist/freeze/freeze.d.ts +23 -0
  61. package/dist/freeze/freeze.js +107 -0
  62. package/dist/freeze/freeze.js.map +1 -0
  63. package/dist/freeze/manifest.d.ts +6 -0
  64. package/dist/freeze/manifest.js +9 -0
  65. package/dist/freeze/manifest.js.map +1 -0
  66. package/dist/freeze/safepath.d.ts +8 -0
  67. package/dist/freeze/safepath.js +22 -0
  68. package/dist/freeze/safepath.js.map +1 -0
  69. package/dist/gitx/git.d.ts +87 -0
  70. package/dist/gitx/git.js +235 -0
  71. package/dist/gitx/git.js.map +1 -0
  72. package/dist/measure/interleave.d.ts +32 -0
  73. package/dist/measure/interleave.js +42 -0
  74. package/dist/measure/interleave.js.map +1 -0
  75. package/dist/pipeline/eval.d.ts +67 -0
  76. package/dist/pipeline/eval.js +495 -0
  77. package/dist/pipeline/eval.js.map +1 -0
  78. package/dist/pm/detect.d.ts +17 -0
  79. package/dist/pm/detect.js +131 -0
  80. package/dist/pm/detect.js.map +1 -0
  81. package/dist/profile/profile.d.ts +53 -0
  82. package/dist/profile/profile.js +105 -0
  83. package/dist/profile/profile.js.map +1 -0
  84. package/dist/results/results.d.ts +65 -0
  85. package/dist/results/results.js +214 -0
  86. package/dist/results/results.js.map +1 -0
  87. package/dist/runner/exec.d.ts +69 -0
  88. package/dist/runner/exec.js +185 -0
  89. package/dist/runner/exec.js.map +1 -0
  90. package/dist/scope/scope.d.ts +20 -0
  91. package/dist/scope/scope.js +70 -0
  92. package/dist/scope/scope.js.map +1 -0
  93. package/dist/state/baseline.d.ts +35 -0
  94. package/dist/state/baseline.js +38 -0
  95. package/dist/state/baseline.js.map +1 -0
  96. package/dist/state/home.d.ts +9 -0
  97. package/dist/state/home.js +72 -0
  98. package/dist/state/home.js.map +1 -0
  99. package/dist/state/lock.d.ts +11 -0
  100. package/dist/state/lock.js +89 -0
  101. package/dist/state/lock.js.map +1 -0
  102. package/dist/state/runnaming.d.ts +27 -0
  103. package/dist/state/runnaming.js +47 -0
  104. package/dist/state/runnaming.js.map +1 -0
  105. package/dist/state/stop.d.ts +28 -0
  106. package/dist/state/stop.js +59 -0
  107. package/dist/state/stop.js.map +1 -0
  108. package/dist/stats/delta.d.ts +29 -0
  109. package/dist/stats/delta.js +41 -0
  110. package/dist/stats/delta.js.map +1 -0
  111. package/dist/stats/geomean.d.ts +4 -0
  112. package/dist/stats/geomean.js +21 -0
  113. package/dist/stats/geomean.js.map +1 -0
  114. package/dist/stats/mannwhitney.d.ts +27 -0
  115. package/dist/stats/mannwhitney.js +158 -0
  116. package/dist/stats/mannwhitney.js.map +1 -0
  117. package/dist/verdict/verdict.d.ts +46 -0
  118. package/dist/verdict/verdict.js +102 -0
  119. package/dist/verdict/verdict.js.map +1 -0
  120. package/dist/version.d.ts +2 -0
  121. package/dist/version.js +3 -0
  122. package/dist/version.js.map +1 -0
  123. package/package.json +35 -0
  124. 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":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjE,OAAO,IAAI,MAAM,WAAW,CAAA;AAO5B,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAA;AACpC,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,IAAI,CAAC;QACH,wEAAwE;QACxE,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACpB,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,wEAAwE;QACxE,sEAAsE;QACtE,mDAAmD;QACnD,OAAQ,GAA6B,CAAC,IAAI,KAAK,OAAO,CAAA;IACxD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAW;IAC5C,IAAI,IAAY,CAAA;IAChB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAA;IAC9C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAA;QACjE,mEAAmE;QACnE,yEAAyE;QACzE,uEAAuE;QACvE,wEAAwE;QACxE,gEAAgE;QAChE,MAAM,GAAG,CAAA;IACX,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAiB,CAAA;QAC/C,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAA;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;QACpE,qEAAqE;QACrE,oEAAoE;QACpE,eAAe;QACf,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAW;IAC/C,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACrC,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAA;IAC1B,MAAM,MAAM,GAAiB,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAA;IAEtF,SAAS,CAAC;QACR,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;YAC7D,MAAK;QACP,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,GAAG,CAAA;YAC/D,mEAAmE;YACnE,uEAAuE;YACvE,mEAAmE;YACnE,kBAAkB;YAClB,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,CAAA;YACxC,IAAI,QAAQ,KAAK,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC/C,MAAM,IAAI,KAAK,CACb,oCAAoC,QAAQ,CAAC,GAAG,kCAAkC;oBAChF,2BAA2B,CAC9B,CAAA;YACH,CAAC;YACD,sEAAsE;YACtE,wEAAwE;YACxE,qEAAqE;YACrE,mEAAmE;YACnE,iBAAiB;YACjB,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACjC,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,GAAG,KAAK,CAAA;IACpB,OAAO,KAAK,IAAI,EAAE;QAChB,IAAI,QAAQ;YAAE,OAAM;QACpB,QAAQ,GAAG,IAAI,CAAA;QACf,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IACjC,CAAC,CAAA;AACH,CAAC"}