tickmarkr 2.5.9 → 2.6.1
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/dist/adapters/claude-code.js +19 -6
- package/dist/adapters/prompt.d.ts +2 -1
- package/dist/adapters/prompt.js +11 -1
- package/dist/adapters/types.d.ts +4 -0
- package/dist/adapters/types.js +10 -0
- package/dist/cli/commands/fleet.js +26 -1
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/plan.js +7 -3
- package/dist/cli/commands/report.js +11 -1
- package/dist/cli/commands/status.js +13 -3
- package/dist/cli/commands/verify.js +2 -0
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +2 -0
- package/dist/compile/native.js +39 -4
- package/dist/drivers/orca.d.ts +1 -1
- package/dist/drivers/orca.js +27 -5
- package/dist/gates/baseline.d.ts +2 -0
- package/dist/gates/baseline.js +9 -1
- package/dist/gates/cache.d.ts +11 -1
- package/dist/gates/cache.js +35 -15
- package/dist/gates/llm.js +4 -1
- package/dist/gates/review.d.ts +2 -3
- package/dist/gates/review.js +28 -35
- package/dist/gates/run-gates.d.ts +4 -0
- package/dist/gates/run-gates.js +17 -4
- package/dist/gates/test-manifest.d.ts +17 -0
- package/dist/gates/test-manifest.js +109 -9
- package/dist/gates/test-reporter.js +4 -0
- package/dist/graph/graph.d.ts +7 -3
- package/dist/graph/graph.js +21 -5
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +2 -0
- package/dist/route/preference.d.ts +1 -1
- package/dist/route/preference.js +10 -39
- package/dist/route/role-pick.d.ts +16 -0
- package/dist/route/role-pick.js +15 -0
- package/dist/route/router.d.ts +14 -0
- package/dist/route/router.js +9 -1
- package/dist/run/consult.js +5 -9
- package/dist/run/daemon.d.ts +16 -1
- package/dist/run/daemon.js +554 -77
- package/dist/run/git.d.ts +46 -1
- package/dist/run/git.js +138 -6
- package/dist/run/host-health.d.ts +20 -0
- package/dist/run/host-health.js +64 -0
- package/dist/run/journal.d.ts +1 -1
- package/dist/run/journal.js +51 -6
- package/dist/run/merge.d.ts +1 -1
- package/dist/run/merge.js +30 -6
- package/dist/run/operator-state.d.ts +24 -2
- package/dist/run/operator-state.js +41 -5
- package/dist/run/stall.d.ts +38 -2
- package/dist/run/stall.js +276 -6
- package/dist/tui/cockpit/board.d.ts +1 -1
- package/dist/tui/cockpit/board.js +27 -19
- package/dist/tui/cockpit/derive.d.ts +2 -0
- package/dist/tui/cockpit/derive.js +4 -0
- package/dist/tui/cockpit/live-runtime.d.ts +4 -0
- package/dist/tui/cockpit/live-runtime.js +37 -3
- package/dist/tui/cockpit/live-store.d.ts +3 -0
- package/dist/tui/cockpit/live-store.js +31 -8
- package/dist/tui/cockpit/run-cockpit.js +2 -1
- package/dist/tui/cockpit/run-view.d.ts +2 -4
- package/dist/tui/cockpit/run-view.js +9 -8
- package/package.json +1 -1
- package/schema/rungraph.schema.json +7 -0
- package/skills/tickmarkr-overseer/SKILL.md +76 -14
package/dist/run/git.d.ts
CHANGED
|
@@ -4,6 +4,18 @@ import { ROUTING_ENV_SEAMS } from "../route/router.js";
|
|
|
4
4
|
export { ROUTING_ENV_SEAMS };
|
|
5
5
|
export declare const FORK_CAP_ENV = "VITEST_MAX_FORKS";
|
|
6
6
|
export declare const SUITE_PARENT_ENV = "TICKMARKR_SUITE_PARENT";
|
|
7
|
+
/** OBS-1173 add.2: only ps's clean "no such process" (exit 1, nothing printed) means gone (""). A
|
|
8
|
+
* timeout, a signal, a spawn error or any other exit is a FAILED probe (null) that says nothing. */
|
|
9
|
+
export declare function classifyIdentityProbe(error: {
|
|
10
|
+
code?: number | string | null;
|
|
11
|
+
killed?: boolean;
|
|
12
|
+
signal?: string | null;
|
|
13
|
+
} | null, stdout: string): string | null;
|
|
14
|
+
/** `undefined` = the pid is gone; `null` = the probe failed and proves nothing (never gone, never a match). */
|
|
15
|
+
export declare function processIdentity(pid: number): Promise<string | null | undefined>;
|
|
16
|
+
export declare const activeShellIdentities: () => ReadonlyMap<number, string | undefined>;
|
|
17
|
+
/** A process that started after its receipt is a pid reuse, never that receipt's child. */
|
|
18
|
+
export declare function resolveActiveShellIdentities(): Promise<void>;
|
|
7
19
|
export declare const DEFAULT_FORK_CAP = "6";
|
|
8
20
|
/** Verification uses command leases; workers keep their concurrency budget. */
|
|
9
21
|
export declare const runWithVerificationBudget: <T>(capacity: RunCapacity, fn: () => Promise<T>) => Promise<T>;
|
|
@@ -174,6 +186,23 @@ export declare function cleanupRunWorktrees(repo: string, branch: string, opts:
|
|
|
174
186
|
removeTaskIds: string[];
|
|
175
187
|
}): Promise<void>;
|
|
176
188
|
export declare function resolveIntegrationBranch(_repo: string, branch: string): Promise<string>;
|
|
189
|
+
/**
|
|
190
|
+
* The attempt whose worker last wrote in a checkout, or explicit "unknown" when no worker launch is
|
|
191
|
+
* on record for the current tree (gate-only recreations, standalone verify, legacy callers). The
|
|
192
|
+
* label is what the preserve commit trailer and every `worktree-preserved`/refusal row carry, so a
|
|
193
|
+
* preserved engine commit is never credited to whichever seat is dispatched next.
|
|
194
|
+
*/
|
|
195
|
+
export type PreserveProducer = {
|
|
196
|
+
channel: string;
|
|
197
|
+
attempt: number;
|
|
198
|
+
} | "unknown";
|
|
199
|
+
export declare const PRESERVE_PRODUCER_TRAILER = "Tickmarkr-Producer";
|
|
200
|
+
/** Subject of every engine preserve commit; only such commits are owned by their preserve row. */
|
|
201
|
+
export declare const PRESERVE_COMMIT_SUBJECT = "tickmarkr: preserve uncommitted worktree";
|
|
202
|
+
export declare const producerFields: (producer: PreserveProducer | undefined) => {
|
|
203
|
+
producer: string;
|
|
204
|
+
producerAttempt?: number;
|
|
205
|
+
};
|
|
177
206
|
/**
|
|
178
207
|
* Preserve the bytes an existing checkout holds before recreation removes it.
|
|
179
208
|
*
|
|
@@ -188,8 +217,24 @@ export declare function resolveIntegrationBranch(_repo: string, branch: string):
|
|
|
188
217
|
* neither a commit nor a ref, keeping meaningful deaths visible rather than minting one ref per
|
|
189
218
|
* ordinary dispatch.
|
|
190
219
|
*/
|
|
191
|
-
export declare function preserveWorktree(cwd: string): Promise<string | undefined>;
|
|
220
|
+
export declare function preserveWorktree(cwd: string, producer?: PreserveProducer): Promise<string | undefined>;
|
|
221
|
+
/**
|
|
222
|
+
* Identity of the physical checkout, including recreation at the same path. Unlike mtime/ctime,
|
|
223
|
+
* birthtime does not change when builds add outputs. Read from disk on every call so process
|
|
224
|
+
* restarts preserve reuse. Filesystems without birthtime cannot prove build-output continuity.
|
|
225
|
+
*/
|
|
226
|
+
export declare function checkoutIncarnation(worktree: string): string | undefined;
|
|
192
227
|
export declare function createWorktree(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
228
|
+
export interface DependencyLink {
|
|
229
|
+
/** Package link, relative to node_modules (including @scope/name). */
|
|
230
|
+
link: string;
|
|
231
|
+
target: string;
|
|
232
|
+
classification: "worktree" | "node_modules" | "outside";
|
|
233
|
+
}
|
|
234
|
+
/** Inventory only package-level links; never descend into installed packages. */
|
|
235
|
+
export declare function inventoryDependencyLinks(worktree: string): DependencyLink[];
|
|
236
|
+
/** Shared pre-command refusal; unreadable inventories cannot establish isolation either. */
|
|
237
|
+
export declare function dependencyLinkRefusal(worktree: string): string | undefined;
|
|
193
238
|
export declare function linkNodeModules(repo: string, dir: string, { force }?: {
|
|
194
239
|
force?: boolean | undefined;
|
|
195
240
|
}): boolean;
|
package/dist/run/git.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { executionSignal } from "./execution-budget.js";
|
|
2
2
|
import { commandLeaseEnvironment, withCommandLease } from "./lease.js";
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
4
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
5
5
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { availableParallelism, homedir, tmpdir } from "node:os";
|
|
7
|
-
import { join, resolve } from "node:path";
|
|
7
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
8
|
import { StringDecoder } from "node:string_decoder";
|
|
9
9
|
import { shq } from "../adapters/types.js";
|
|
10
10
|
import { tickmarkrDir } from "../graph/graph.js";
|
|
@@ -16,6 +16,52 @@ export { ROUTING_ENV_SEAMS };
|
|
|
16
16
|
// as argv (OBS-55) so child test oracles stay intact. The operator's own export wins.
|
|
17
17
|
export const FORK_CAP_ENV = "VITEST_MAX_FORKS";
|
|
18
18
|
export const SUITE_PARENT_ENV = "TICKMARKR_SUITE_PARENT";
|
|
19
|
+
/** A pid is an address, not an identity. Keep this evidence out of ShellReceipt. */
|
|
20
|
+
const linuxIdentity = (pid) => {
|
|
21
|
+
try {
|
|
22
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
23
|
+
const start = stat.slice(stat.lastIndexOf(")") + 2).split(/\s+/)[19];
|
|
24
|
+
return start && /^\d+$/.test(start) ? `${pid}:${start}` : undefined;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** OBS-1173 add.2: only ps's clean "no such process" (exit 1, nothing printed) means gone (""). A
|
|
31
|
+
* timeout, a signal, a spawn error or any other exit is a FAILED probe (null) that says nothing. */
|
|
32
|
+
export function classifyIdentityProbe(error, stdout) {
|
|
33
|
+
const start = stdout.trim().replace(/\s+/g, " ");
|
|
34
|
+
if (!error)
|
|
35
|
+
return start || null;
|
|
36
|
+
return !error.killed && !error.signal && error.code === 1 && !start ? "" : null;
|
|
37
|
+
}
|
|
38
|
+
/** `undefined` = the pid is gone; `null` = the probe failed and proves nothing (never gone, never a match). */
|
|
39
|
+
export async function processIdentity(pid) {
|
|
40
|
+
if (process.platform === "linux")
|
|
41
|
+
return linuxIdentity(pid);
|
|
42
|
+
// Async and bounded like every other ps probe: a loaded host is slow, not unreadable.
|
|
43
|
+
const start = await new Promise((done) => execFile("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
44
|
+
encoding: "utf8", timeout: 15_000, maxBuffer: 4096,
|
|
45
|
+
}, (error, stdout) => done(classifyIdentityProbe(error, stdout))));
|
|
46
|
+
if (start === null)
|
|
47
|
+
return null;
|
|
48
|
+
return start ? `${pid}:${start}` : undefined;
|
|
49
|
+
}
|
|
50
|
+
// Started receipts own these live pids; terminal receipts retire exactly that invocation. The
|
|
51
|
+
// shell hot path never probes: Linux reads /proc in place, other hosts resolve on demand below.
|
|
52
|
+
const activeShells = new Map();
|
|
53
|
+
export const activeShellIdentities = () => new Map([...activeShells].map(([pid, live]) => [pid, live.identity ?? undefined]));
|
|
54
|
+
/** A process that started after its receipt is a pid reuse, never that receipt's child. */
|
|
55
|
+
export async function resolveActiveShellIdentities() {
|
|
56
|
+
await Promise.all([...activeShells].filter(([, live]) => live.identity === undefined).map(async ([pid, live]) => {
|
|
57
|
+
const identity = await processIdentity(pid);
|
|
58
|
+
if (identity === null)
|
|
59
|
+
return; // a failed probe resolves nothing; the next resolve retries
|
|
60
|
+
const started = identity ? Date.parse(identity.slice(identity.indexOf(":") + 1)) : NaN;
|
|
61
|
+
if (live.identity === undefined)
|
|
62
|
+
live.identity = identity && started <= live.startedMs + 1_000 ? identity : null;
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
19
65
|
export const DEFAULT_FORK_CAP = "6";
|
|
20
66
|
/** Worker concurrency is resolved once per run and isolated from other async runs. */
|
|
21
67
|
const forkBudget = new AsyncLocalStorage();
|
|
@@ -217,6 +263,8 @@ export function shell(cmd, cwd, timeoutMs, login = false, options = {}) {
|
|
|
217
263
|
let attribution;
|
|
218
264
|
let confirmedStart = false;
|
|
219
265
|
let terminal = false;
|
|
266
|
+
let activePid;
|
|
267
|
+
let activeInvocation;
|
|
220
268
|
const begin = (invocation) => {
|
|
221
269
|
attribution = options.receiptAttribution?.(invocation);
|
|
222
270
|
confirmedStart = false;
|
|
@@ -225,10 +273,21 @@ export function shell(cmd, cwd, timeoutMs, login = false, options = {}) {
|
|
|
225
273
|
const emit = (receipt) => {
|
|
226
274
|
if (terminal)
|
|
227
275
|
return;
|
|
228
|
-
if (receipt.outcome === "started")
|
|
276
|
+
if (receipt.outcome === "started") {
|
|
229
277
|
confirmedStart = true;
|
|
230
|
-
|
|
278
|
+
activePid = receipt.pid;
|
|
279
|
+
if (activePid !== undefined) {
|
|
280
|
+
activeInvocation = Symbol("shell invocation");
|
|
281
|
+
activeShells.set(activePid, { startedMs: Date.now(), invocation: activeInvocation,
|
|
282
|
+
...(process.platform === "linux" ? { identity: linuxIdentity(activePid) ?? null } : {}) });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
231
286
|
terminal = true;
|
|
287
|
+
if (activePid !== undefined && activeShells.get(activePid)?.invocation === activeInvocation)
|
|
288
|
+
activeShells.delete(activePid);
|
|
289
|
+
activePid = undefined;
|
|
290
|
+
}
|
|
232
291
|
// Observational callbacks must not change process cleanup, retries or cancellation.
|
|
233
292
|
try {
|
|
234
293
|
options.onReceipt?.({ ...receipt, confirmedStart, ...(attribution ? { attribution: { ...attribution } } : {}) });
|
|
@@ -540,6 +599,14 @@ const resolveTaskBranch = async (repo, branch) => {
|
|
|
540
599
|
const integration = await resolveIntegrationBranch(repo, branch.slice(0, split));
|
|
541
600
|
return `${integration}${branch.slice(split)}`;
|
|
542
601
|
};
|
|
602
|
+
export const PRESERVE_PRODUCER_TRAILER = "Tickmarkr-Producer";
|
|
603
|
+
/** Subject of every engine preserve commit; only such commits are owned by their preserve row. */
|
|
604
|
+
export const PRESERVE_COMMIT_SUBJECT = "tickmarkr: preserve uncommitted worktree";
|
|
605
|
+
export const producerFields = (producer) => producer && producer !== "unknown" ? { producer: producer.channel, producerAttempt: producer.attempt } : { producer: "unknown" };
|
|
606
|
+
const producerLabel = (producer) => {
|
|
607
|
+
const f = producerFields(producer);
|
|
608
|
+
return f.producerAttempt === undefined ? f.producer : `${f.producer} attempt ${f.producerAttempt}`;
|
|
609
|
+
};
|
|
543
610
|
/**
|
|
544
611
|
* Preserve the bytes an existing checkout holds before recreation removes it.
|
|
545
612
|
*
|
|
@@ -554,7 +621,7 @@ const resolveTaskBranch = async (repo, branch) => {
|
|
|
554
621
|
* neither a commit nor a ref, keeping meaningful deaths visible rather than minting one ref per
|
|
555
622
|
* ordinary dispatch.
|
|
556
623
|
*/
|
|
557
|
-
export async function preserveWorktree(cwd) {
|
|
624
|
+
export async function preserveWorktree(cwd, producer = "unknown") {
|
|
558
625
|
if (!existsSync(cwd))
|
|
559
626
|
return undefined;
|
|
560
627
|
const scratch = mkdtempSync(join(tmpdir(), "tickmarkr-preserve-index-"));
|
|
@@ -571,7 +638,7 @@ export async function preserveWorktree(cwd) {
|
|
|
571
638
|
// not an authored project commit. Its HEAD parent makes the preserved tree directly inspectable.
|
|
572
639
|
const identity = "GIT_AUTHOR_NAME=tickmarkr GIT_AUTHOR_EMAIL=tickmarkr@localhost "
|
|
573
640
|
+ "GIT_COMMITTER_NAME=tickmarkr GIT_COMMITTER_EMAIL=tickmarkr@localhost";
|
|
574
|
-
const commit = (await shGitOk(`${identity} git commit-tree ${shq(tree)} -p HEAD -m ${shq(
|
|
641
|
+
const commit = (await shGitOk(`${identity} git commit-tree ${shq(tree)} -p HEAD -m ${shq(PRESERVE_COMMIT_SUBJECT)} -m ${shq(`${PRESERVE_PRODUCER_TRAILER}: ${producerLabel(producer)}`)}`, cwd)).trim();
|
|
575
642
|
const ref = `refs/tickmarkr/preserved/${commit}`;
|
|
576
643
|
await shGitOk(`git update-ref ${shq(ref)} ${shq(commit)}`, cwd);
|
|
577
644
|
return ref;
|
|
@@ -580,6 +647,23 @@ export async function preserveWorktree(cwd) {
|
|
|
580
647
|
rmSync(scratch, { recursive: true, force: true });
|
|
581
648
|
}
|
|
582
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Identity of the physical checkout, including recreation at the same path. Unlike mtime/ctime,
|
|
652
|
+
* birthtime does not change when builds add outputs. Read from disk on every call so process
|
|
653
|
+
* restarts preserve reuse. Filesystems without birthtime cannot prove build-output continuity.
|
|
654
|
+
*/
|
|
655
|
+
export function checkoutIncarnation(worktree) {
|
|
656
|
+
try {
|
|
657
|
+
const root = realpathSync(worktree);
|
|
658
|
+
const stat = statSync(root, { bigint: true });
|
|
659
|
+
if (!stat.isDirectory() || stat.birthtimeNs <= 0n)
|
|
660
|
+
return undefined;
|
|
661
|
+
return `${root}:${stat.dev}:${stat.ino}:${stat.birthtimeNs}`;
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
583
667
|
export async function createWorktree(repo, branch, baseRef) {
|
|
584
668
|
branch = await resolveTaskBranch(repo, branch);
|
|
585
669
|
const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
|
|
@@ -624,6 +708,54 @@ function excludeNodeModules(dir) {
|
|
|
624
708
|
}
|
|
625
709
|
catch { /* not a git checkout (bare tmpdir in tests) — never fail provisioning over the exclude */ }
|
|
626
710
|
}
|
|
711
|
+
/** Inventory only package-level links; never descend into installed packages. */
|
|
712
|
+
export function inventoryDependencyLinks(worktree) {
|
|
713
|
+
const dependencyPath = join(worktree, "node_modules");
|
|
714
|
+
try {
|
|
715
|
+
lstatSync(dependencyPath);
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
if (error.code === "ENOENT")
|
|
719
|
+
return [];
|
|
720
|
+
throw error;
|
|
721
|
+
}
|
|
722
|
+
const root = realpathSync(worktree);
|
|
723
|
+
const dependencies = realpathSync(dependencyPath);
|
|
724
|
+
const inside = (parent, target) => {
|
|
725
|
+
const rel = relative(parent, target);
|
|
726
|
+
return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`));
|
|
727
|
+
};
|
|
728
|
+
const links = [];
|
|
729
|
+
const inspect = (link) => {
|
|
730
|
+
const path = join(dependencies, link);
|
|
731
|
+
if (!lstatSync(path).isSymbolicLink())
|
|
732
|
+
return;
|
|
733
|
+
const target = realpathSync(path);
|
|
734
|
+
links.push({ link, target, classification: inside(root, target) ? "worktree"
|
|
735
|
+
: inside(dependencies, target) ? "node_modules" : "outside" });
|
|
736
|
+
};
|
|
737
|
+
for (const entry of readdirSync(dependencies).sort()) {
|
|
738
|
+
inspect(entry);
|
|
739
|
+
if (entry.startsWith("@") && statSync(join(dependencies, entry)).isDirectory()) {
|
|
740
|
+
for (const child of readdirSync(join(dependencies, entry)).sort())
|
|
741
|
+
inspect(join(entry, child));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return links;
|
|
745
|
+
}
|
|
746
|
+
/** Shared pre-command refusal; unreadable inventories cannot establish isolation either. */
|
|
747
|
+
export function dependencyLinkRefusal(worktree) {
|
|
748
|
+
try {
|
|
749
|
+
const outside = inventoryDependencyLinks(worktree).filter(link => link.classification === "outside");
|
|
750
|
+
if (!outside.length)
|
|
751
|
+
return undefined;
|
|
752
|
+
return "infra: OBS-1118 refusing verification: dependency links resolve outside the worktree and real node_modules:\n"
|
|
753
|
+
+ outside.map(({ link, target }) => `node_modules/${link} -> ${target}`).join("\n");
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
return `infra: OBS-1118 refusing verification: cannot inventory dependency links in ${worktree}: ${String(error)}`;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
627
759
|
export function linkNodeModules(repo, dir, { force = false } = {}) {
|
|
628
760
|
const src = join(repo, "node_modules");
|
|
629
761
|
const dest = join(dir, "node_modules");
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export declare const HOST_PROBE_SAMPLES = 3;
|
|
3
|
+
export declare const HOST_PROBE_SAMPLE_MS = 1000;
|
|
4
|
+
export declare const HOST_LATENCY_FLOOR_MS = 50;
|
|
5
|
+
export declare const HOST_LATENCY_RATIO = 3;
|
|
6
|
+
export declare class HostDegradedError extends Error {
|
|
7
|
+
}
|
|
8
|
+
export declare const setHostProbeSpawnForTests: (fn: typeof spawn) => void;
|
|
9
|
+
export declare const resetHostProbeSpawnForTests: () => void;
|
|
10
|
+
declare const execLatency: (signal: AbortSignal, timeoutMs: number) => Promise<number | null>;
|
|
11
|
+
export declare const setHostLatencySampleForTests: (fn: typeof execLatency) => void;
|
|
12
|
+
export declare const resetHostLatencySampleForTests: () => void;
|
|
13
|
+
export interface HostObservation {
|
|
14
|
+
samplesMs: Array<number | null>;
|
|
15
|
+
medianMs: number | null;
|
|
16
|
+
}
|
|
17
|
+
/** Three sequential samples, each bounded by 1s and the remaining observation budget. */
|
|
18
|
+
export declare function observeHost(signal: AbortSignal, budgetMs?: number): Promise<HostObservation>;
|
|
19
|
+
export declare const hostDegraded: (observation: HostObservation, referenceMs: number | undefined) => boolean;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { FILE_HANG_SLACK } from "../gates/test-manifest.js";
|
|
4
|
+
export const HOST_PROBE_SAMPLES = 3;
|
|
5
|
+
export const HOST_PROBE_SAMPLE_MS = 1_000;
|
|
6
|
+
export const HOST_LATENCY_FLOOR_MS = 50;
|
|
7
|
+
export const HOST_LATENCY_RATIO = FILE_HANG_SLACK;
|
|
8
|
+
export class HostDegradedError extends Error {
|
|
9
|
+
}
|
|
10
|
+
// A probe owns just this child: no shell, descendants, inherited Node options, or host-wide kills.
|
|
11
|
+
let probeSpawn = spawn;
|
|
12
|
+
export const setHostProbeSpawnForTests = (fn) => { probeSpawn = fn; };
|
|
13
|
+
export const resetHostProbeSpawnForTests = () => { probeSpawn = spawn; };
|
|
14
|
+
const execLatency = (signal, timeoutMs) => new Promise((resolve, reject) => {
|
|
15
|
+
signal.throwIfAborted();
|
|
16
|
+
const start = performance.now();
|
|
17
|
+
const env = { ...process.env };
|
|
18
|
+
delete env.NODE_OPTIONS;
|
|
19
|
+
let child;
|
|
20
|
+
try {
|
|
21
|
+
child = probeSpawn(process.execPath, ["-e", ""], { stdio: "ignore", env });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
resolve(null);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let unreadable = false;
|
|
28
|
+
const retire = () => { unreadable = true; child.kill("SIGKILL"); };
|
|
29
|
+
const timer = setTimeout(retire, timeoutMs);
|
|
30
|
+
signal.addEventListener("abort", retire, { once: true });
|
|
31
|
+
child.once("error", () => { unreadable = true; });
|
|
32
|
+
// Resolve only after close: cancellation and timeout must reap the owned child before returning.
|
|
33
|
+
child.once("close", code => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
signal.removeEventListener("abort", retire);
|
|
36
|
+
if (signal.aborted)
|
|
37
|
+
reject(signal.reason);
|
|
38
|
+
else {
|
|
39
|
+
const elapsed = performance.now() - start;
|
|
40
|
+
resolve(!unreadable && code === 0 && elapsed <= timeoutMs ? elapsed : null);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
if (signal.aborted)
|
|
44
|
+
retire();
|
|
45
|
+
});
|
|
46
|
+
let sample = execLatency;
|
|
47
|
+
export const setHostLatencySampleForTests = (fn) => { sample = fn; };
|
|
48
|
+
export const resetHostLatencySampleForTests = () => { sample = execLatency; };
|
|
49
|
+
/** Three sequential samples, each bounded by 1s and the remaining observation budget. */
|
|
50
|
+
export async function observeHost(signal, budgetMs = HOST_PROBE_SAMPLES * HOST_PROBE_SAMPLE_MS) {
|
|
51
|
+
const deadline = performance.now() + budgetMs;
|
|
52
|
+
const samplesMs = [];
|
|
53
|
+
for (let i = 0; i < HOST_PROBE_SAMPLES; i++) {
|
|
54
|
+
signal.throwIfAborted();
|
|
55
|
+
const remaining = deadline - performance.now();
|
|
56
|
+
const value = remaining > 0 ? await sample(signal, Math.min(HOST_PROBE_SAMPLE_MS, remaining)) : null;
|
|
57
|
+
samplesMs.push(value !== null && Number.isFinite(value) && value > 0 ? value : null);
|
|
58
|
+
}
|
|
59
|
+
const medianMs = samplesMs.every((n) => n !== null)
|
|
60
|
+
? [...samplesMs].sort((a, b) => a - b)[1] : null;
|
|
61
|
+
return { samplesMs, medianMs };
|
|
62
|
+
}
|
|
63
|
+
export const hostDegraded = (observation, referenceMs) => observation.medianMs === null || referenceMs === undefined
|
|
64
|
+
|| observation.medianMs >= Math.max(HOST_LATENCY_FLOOR_MS, referenceMs * HOST_LATENCY_RATIO);
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -437,7 +437,7 @@ export declare class Journal {
|
|
|
437
437
|
assignment?: Assignment;
|
|
438
438
|
} | null;
|
|
439
439
|
replayResumeState(): Map<string, ResumeState>;
|
|
440
|
-
replaySatisfiedGates(): Map<string, GateName>;
|
|
440
|
+
replaySatisfiedGates(currentSubjects?: ReadonlyMap<string, string>): Map<string, GateName>;
|
|
441
441
|
replayCurrentAttemptGateResults(): Map<string, CurrentAttemptGateReplay>;
|
|
442
442
|
replayExcludedChannels(): Set<string>;
|
|
443
443
|
telemetry(row: TelemetryRow): void;
|
package/dist/run/journal.js
CHANGED
|
@@ -1673,6 +1673,15 @@ export class Journal {
|
|
|
1673
1673
|
lastDispatch.delete(e.taskId);
|
|
1674
1674
|
}
|
|
1675
1675
|
}
|
|
1676
|
+
else if (e.event === "capacity-requeue") {
|
|
1677
|
+
// OBS-1161: a capacity requeue is free — the daemon re-dispatches the SAME attempt on the SAME
|
|
1678
|
+
// seat — so the dispatch it closes must not count, or a resume bills every busy-seat wait
|
|
1679
|
+
// against MAX_ATTEMPTS. Only the attempt rewinds: the seat stays tried and in force (busy, not
|
|
1680
|
+
// burned). Idempotent by the same rule as the scope rewind: only the OUTSTANDING dispatch.
|
|
1681
|
+
const st = m.get(e.taskId);
|
|
1682
|
+
if (st && lastDispatch.delete(e.taskId))
|
|
1683
|
+
st.attempts = Math.max(0, st.attempts - 1);
|
|
1684
|
+
}
|
|
1676
1685
|
else if (e.event === "consult-verdict" && e.data.action === "reroute") {
|
|
1677
1686
|
// A reroute bans the in-force channel; retry/decompose/human verdicts ban nothing (D-03).
|
|
1678
1687
|
pendingReroute.add(e.taskId);
|
|
@@ -1734,26 +1743,62 @@ export class Journal {
|
|
|
1734
1743
|
}
|
|
1735
1744
|
return m;
|
|
1736
1745
|
}
|
|
1737
|
-
// OBS-130/OBS-571:
|
|
1738
|
-
//
|
|
1739
|
-
//
|
|
1740
|
-
// before
|
|
1741
|
-
|
|
1742
|
-
replaySatisfiedGates() {
|
|
1746
|
+
// OBS-130/OBS-571: only an explicit approval grants gate authority; recreation consumes
|
|
1747
|
+
// its immediate enactment. OBS-1133: retain a review waiver's subject separately so a later
|
|
1748
|
+
// recheck may carry it. Tool waivers never survive recheck. The daemon supplies the recreated
|
|
1749
|
+
// subject before using a carried waiver; absent that, replay can only compare journaled subjects.
|
|
1750
|
+
replaySatisfiedGates(currentSubjects) {
|
|
1743
1751
|
const satisfied = new Map();
|
|
1752
|
+
const reviewSubjects = new Map();
|
|
1753
|
+
const subjects = new Map();
|
|
1754
|
+
const reviewWaivers = new Map();
|
|
1744
1755
|
for (const e of this.read()) {
|
|
1745
1756
|
if (!e.taskId)
|
|
1746
1757
|
continue;
|
|
1758
|
+
if (e.event === "task-dispatch") {
|
|
1759
|
+
reviewSubjects.delete(e.taskId);
|
|
1760
|
+
subjects.delete(e.taskId);
|
|
1761
|
+
reviewWaivers.delete(e.taskId);
|
|
1762
|
+
satisfied.delete(e.taskId);
|
|
1763
|
+
}
|
|
1764
|
+
if (e.event === "gate-result") {
|
|
1765
|
+
const commit = typeof e.data.commit === "string" && e.data.commit ? e.data.commit : undefined;
|
|
1766
|
+
if (commit)
|
|
1767
|
+
subjects.set(e.taskId, commit);
|
|
1768
|
+
else
|
|
1769
|
+
subjects.delete(e.taskId);
|
|
1770
|
+
if (reviewWaivers.has(e.taskId) && reviewWaivers.get(e.taskId) !== commit) {
|
|
1771
|
+
reviewWaivers.delete(e.taskId);
|
|
1772
|
+
}
|
|
1773
|
+
if (e.data.gate === "review") {
|
|
1774
|
+
if (commit && e.data.pass === false)
|
|
1775
|
+
reviewSubjects.set(e.taskId, commit);
|
|
1776
|
+
else
|
|
1777
|
+
reviewSubjects.delete(e.taskId);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1747
1780
|
if (e.event === "worktree-recreation") {
|
|
1748
1781
|
satisfied.delete(e.taskId);
|
|
1749
1782
|
continue;
|
|
1750
1783
|
}
|
|
1751
1784
|
if (e.event === "task-approved") {
|
|
1752
1785
|
satisfied.delete(e.taskId);
|
|
1786
|
+
if (e.data.release === RECHECK_RELEASE) {
|
|
1787
|
+
const waivedSubject = reviewWaivers.get(e.taskId);
|
|
1788
|
+
const subject = currentSubjects?.get(e.taskId) ?? subjects.get(e.taskId);
|
|
1789
|
+
if (waivedSubject && waivedSubject === subject)
|
|
1790
|
+
satisfied.set(e.taskId, "review");
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
reviewWaivers.delete(e.taskId);
|
|
1753
1794
|
if (e.data.release === GATE_SATISFIED_RELEASE
|
|
1754
1795
|
&& typeof e.data.gate === "string"
|
|
1755
1796
|
&& GATE_NAMES.includes(e.data.gate)) {
|
|
1756
1797
|
satisfied.set(e.taskId, e.data.gate);
|
|
1798
|
+
const subject = reviewSubjects.get(e.taskId);
|
|
1799
|
+
if (e.data.gate === "review" && subject && subject === subjects.get(e.taskId)) {
|
|
1800
|
+
reviewWaivers.set(e.taskId, subject);
|
|
1801
|
+
}
|
|
1757
1802
|
}
|
|
1758
1803
|
}
|
|
1759
1804
|
}
|
package/dist/run/merge.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface TipVerifyResult {
|
|
|
5
5
|
evidenceReceipt?: GateEvidenceReceipt;
|
|
6
6
|
evidenceReceipts?: GateEvidenceReceipt[];
|
|
7
7
|
evidenceAbsence?: "provenance-refused" | "not-started" | "historical-cache";
|
|
8
|
-
/**
|
|
8
|
+
/** Directory the evidence setup returned; receipt artifact paths resolve against it. */
|
|
9
9
|
originRunRoot?: string;
|
|
10
10
|
nonce?: string;
|
|
11
11
|
stdoutPath?: string;
|
package/dist/run/merge.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
import { shq } from "../adapters/types.js";
|
|
4
4
|
import { beginGateEvidence, ceilingKillResult, classifyFreshRunnerOutput, classifyRunnerOutput, fileCountDeficit, waitForCalmWindow, effectiveCeilingMs, fingerprint, freshFailures, } from "../gates/baseline.js";
|
|
5
5
|
import { evaluateManifestedTest, isVitestTestCommand } from "../gates/test-manifest.js";
|
|
6
6
|
import { computeVerificationIdentity, formatReusedDetails, getVerdictStore, getWorktreeTree, resolveStateDir, isInfraResult, } from "../gates/cache.js";
|
|
7
7
|
import { tickmarkrDir } from "../graph/graph.js";
|
|
8
|
-
import { describeCapacity, gitHead, linkNodeModules, resolveIntegrationBranch, resolvedCapacity, sameCapacity, sh, shGit, shGitOk, WORKTREES_DIR } from "./git.js";
|
|
8
|
+
import { dependencyLinkRefusal, describeCapacity, gitHead, linkNodeModules, resolveIntegrationBranch, resolvedCapacity, sameCapacity, sh, shGit, shGitOk, WORKTREES_DIR } from "./git.js";
|
|
9
9
|
import { executionSignal } from "./execution-budget.js";
|
|
10
10
|
// Diagnostic convenience logs are observational, just like receipt persistence.
|
|
11
11
|
function writeTipLog(path, text) {
|
|
@@ -128,6 +128,16 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
128
128
|
if (!commands.test && commands.tipTest) {
|
|
129
129
|
gatesToRun.push(["test", commands.tipTest]);
|
|
130
130
|
}
|
|
131
|
+
const dependencyRefusal = dependencyLinkRefusal(intWt);
|
|
132
|
+
if (dependencyRefusal) {
|
|
133
|
+
for (const [gate, cmd] of gatesToRun.length ? gatesToRun : [["build", ""]]) {
|
|
134
|
+
const artifact = join(runDir, `tip-verify-${gate}.log`);
|
|
135
|
+
writeTipLog(artifact, dependencyRefusal + "\n");
|
|
136
|
+
results.push({ gate, cmd, pass: false, exitCode: 1, fingerprints: [],
|
|
137
|
+
cause: "infra", details: dependencyRefusal, evidenceAbsence: "not-started", artifact });
|
|
138
|
+
}
|
|
139
|
+
return results;
|
|
140
|
+
}
|
|
131
141
|
if (journalFound && hasRunEvidenceOrMalformed && runStartCommands === undefined) {
|
|
132
142
|
for (const [gate, cmd] of gatesToRun) {
|
|
133
143
|
const artifact = join(runDir, `tip-verify-${gate}.log`);
|
|
@@ -151,6 +161,8 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
151
161
|
// Publish only after the entire verification completes. The daemon kills a cancelled
|
|
152
162
|
// verifier process, so completed early gates must remain in memory until then.
|
|
153
163
|
const pending = new Map();
|
|
164
|
+
// D-267 (1): each fresh receipt's origin is the root its own beginGateEvidence returned.
|
|
165
|
+
const evidenceRoots = new Map();
|
|
154
166
|
for (const [gate, cmd] of gatesToRun) {
|
|
155
167
|
if (runStartCommands !== undefined) {
|
|
156
168
|
const expectedCmd = gate === "test"
|
|
@@ -226,13 +238,20 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
226
238
|
// a detected vitest test command never reaches the stdout-count/file-count path below (this
|
|
227
239
|
// gate's own manifest is always the FULL suite; verify has no selected screen to hold). A
|
|
228
240
|
// scripted test command that is not the detected runner falls through unchanged.
|
|
241
|
+
const evidenceSetup = { artifactDir: runDir, runId: runDir, ...evidenceOptions };
|
|
229
242
|
if (gate === "test" && isVitestTestCommand(cmd, intWt)) {
|
|
243
|
+
// D-267 (1): stamp the root the evidence setup returned, then give that exact root to every
|
|
244
|
+
// manifested invocation. Do not reconstruct it from the caller's options: the receipt's
|
|
245
|
+
// artifact paths are relative to the root returned by beginGateEvidence.
|
|
246
|
+
const evidence = beginGateEvidence(intWt, gate, cmd, evidenceSetup);
|
|
247
|
+
const manifestedEvidence = { ...evidenceSetup, artifactDir: evidence.root };
|
|
248
|
+
evidenceRoots.set(gate, evidence.root);
|
|
230
249
|
const outcome = await evaluateManifestedTest(cmd, intWt, {
|
|
231
250
|
baselineDurations: entry?.fileDurations,
|
|
232
251
|
longestFile: entry?.longestFile,
|
|
233
252
|
overallCeilingMs: effectiveCeilingMs(entry),
|
|
234
253
|
artifactDir: runDir,
|
|
235
|
-
evidence:
|
|
254
|
+
evidence: manifestedEvidence,
|
|
236
255
|
});
|
|
237
256
|
const artifact = join(runDir, `tip-verify-${gate}.log`);
|
|
238
257
|
if (!outcome.pass)
|
|
@@ -264,7 +283,8 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
264
283
|
const ceilingMs = effectiveCeilingMs(entry);
|
|
265
284
|
const evidenceReceipts = [];
|
|
266
285
|
const execute = async () => {
|
|
267
|
-
const evidence = beginGateEvidence(intWt, gate, cmd,
|
|
286
|
+
const evidence = beginGateEvidence(intWt, gate, cmd, evidenceSetup);
|
|
287
|
+
evidenceRoots.set(gate, evidence.root);
|
|
268
288
|
try {
|
|
269
289
|
const result = await sh(cmd, intWt, ceilingMs, { env: evidenceOptions.env, onReceipt: receipt => evidence.observe(receipt) });
|
|
270
290
|
evidenceReceipts.push(...evidence.history, evidence.finish(result.stdout, result.stderr));
|
|
@@ -372,8 +392,12 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
372
392
|
// spawns in the daemon child, and a command that changed the tree invalidates reuse.
|
|
373
393
|
const completedTree = pending.size ? await getWorktreeTree(intWt) : undefined;
|
|
374
394
|
for (const result of results) {
|
|
375
|
-
if (!result.reused && result.evidenceReceipt)
|
|
376
|
-
|
|
395
|
+
if (!result.reused && result.evidenceReceipt) {
|
|
396
|
+
const evidenceRoot = evidenceRoots.get(result.gate);
|
|
397
|
+
if (evidenceRoot === undefined)
|
|
398
|
+
throw new Error(`tip verify receipt for ${result.gate} has no evidence root from its evidence setup`);
|
|
399
|
+
result.originRunRoot = evidenceRoot;
|
|
400
|
+
}
|
|
377
401
|
const id = pending.get(result.gate);
|
|
378
402
|
if (id && id.tree === completedTree && !isInfraResult(result)) {
|
|
379
403
|
store.set(id, { ...result, meta: {
|
|
@@ -11,11 +11,18 @@ export interface OperatorRecord extends EvidenceIdentity {
|
|
|
11
11
|
event: JournalEvent;
|
|
12
12
|
}
|
|
13
13
|
export type OperatorTaskState = "unknown" | "pending" | "running" | "completed" | "merged" | "failed" | "human" | "blocked";
|
|
14
|
-
export type OperatorGateState = "unknown" | "not-run" | "disabled" | "running" | "passed" | "failed";
|
|
14
|
+
export type OperatorGateState = "unknown" | "not-run" | "disabled" | "queued" | "running" | "passed" | "failed";
|
|
15
15
|
export interface OperatorGate {
|
|
16
16
|
state: OperatorGateState;
|
|
17
17
|
evidence?: EvidenceIdentity;
|
|
18
18
|
}
|
|
19
|
+
export interface GateActivity {
|
|
20
|
+
state: "queued" | "running";
|
|
21
|
+
gate?: string;
|
|
22
|
+
reason?: "suite-wait" | "host-degraded";
|
|
23
|
+
count?: number;
|
|
24
|
+
evidence: EvidenceIdentity;
|
|
25
|
+
}
|
|
19
26
|
export interface OperatorTask {
|
|
20
27
|
id: string;
|
|
21
28
|
title?: string;
|
|
@@ -30,10 +37,14 @@ export interface OperatorTask {
|
|
|
30
37
|
evidence?: EvidenceIdentity;
|
|
31
38
|
mergeEvidence?: EvidenceIdentity;
|
|
32
39
|
gates: Record<string, OperatorGate>;
|
|
40
|
+
gateActivity?: GateActivity;
|
|
33
41
|
/** BD-1: the board's per-task counters — latest dispatch channel, review verdicts drawn, human parks. */
|
|
34
42
|
channel?: string;
|
|
35
43
|
reviewRounds: number;
|
|
36
44
|
parks: number;
|
|
45
|
+
/** Distinct producing channels of the merged subject from task-done; "unknown" names unattributed preserved work.
|
|
46
|
+
* Kept apart from `channel`: the last-dispatched seat may have authored nothing. */
|
|
47
|
+
authors?: readonly string[];
|
|
37
48
|
}
|
|
38
49
|
export interface OperatorSnapshot {
|
|
39
50
|
sequence: number;
|
|
@@ -47,6 +58,10 @@ export interface OperatorSnapshot {
|
|
|
47
58
|
label: string;
|
|
48
59
|
green: boolean;
|
|
49
60
|
currentTip: "passed" | "not required" | "pending" | "failed" | "unknown";
|
|
61
|
+
graphAvailability?: {
|
|
62
|
+
status: "readable" | "absent" | "unreadable";
|
|
63
|
+
error?: string;
|
|
64
|
+
};
|
|
50
65
|
comparable: boolean;
|
|
51
66
|
comparison: "matching graph" | "not comparable";
|
|
52
67
|
merged: number;
|
|
@@ -60,6 +75,12 @@ export interface OperatorSnapshot {
|
|
|
60
75
|
latestRunEnd?: EvidenceIdentity;
|
|
61
76
|
approvedResumeRequired: boolean;
|
|
62
77
|
}
|
|
78
|
+
/** Author channels recorded on a task-done row. A row written before authors were recorded carries none and
|
|
79
|
+
* projects nothing (never the dispatch); unattributed preserved work arrives as an explicit "unknown" entry. */
|
|
80
|
+
export declare const doneAuthors: (data: Record<string, unknown>) => string[] | undefined;
|
|
81
|
+
/** The note a status row or board row shows beside the last-dispatched channel when the merged
|
|
82
|
+
* authors are not exactly that channel (a carry-only seat, a mixed subject, or legacy unknown). */
|
|
83
|
+
export declare const authorsNote: (authors: readonly string[] | undefined, channel: string | undefined) => string | undefined;
|
|
63
84
|
/** Incremental pure lifecycle fold. Retains facts per task/gate, never verdict bodies or event history. */
|
|
64
85
|
export declare class OperatorStateFold {
|
|
65
86
|
private tasks;
|
|
@@ -78,11 +99,12 @@ export declare class OperatorStateFold {
|
|
|
78
99
|
private total;
|
|
79
100
|
apply(record: OperatorRecord): void;
|
|
80
101
|
private task;
|
|
81
|
-
snapshot({ graph, sequence, observedAt, readable }?: {
|
|
102
|
+
snapshot({ graph, sequence, observedAt, readable, graphAvailability }?: {
|
|
82
103
|
graph?: RunGraph;
|
|
83
104
|
sequence?: number;
|
|
84
105
|
observedAt?: number;
|
|
85
106
|
readable?: boolean;
|
|
107
|
+
graphAvailability?: OperatorSnapshot["graphAvailability"];
|
|
86
108
|
}): OperatorSnapshot;
|
|
87
109
|
private comparableTo;
|
|
88
110
|
}
|