tickmarkr 2.6.1 → 2.6.3
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/README.md +16 -3
- package/dist/adapters/catalog-remote.js +89 -47
- package/dist/adapters/claude-code.js +9 -6
- package/dist/adapters/codex.js +7 -4
- package/dist/adapters/prompt.d.ts +1 -0
- package/dist/adapters/prompt.js +14 -6
- package/dist/adapters/registry.js +3 -3
- package/dist/adapters/types.d.ts +12 -4
- package/dist/adapters/types.js +6 -0
- package/dist/cli/commands/approve.d.ts +11 -4
- package/dist/cli/commands/approve.js +82 -27
- package/dist/cli/commands/compile.js +13 -3
- package/dist/cli/commands/doctor.d.ts +8 -2
- package/dist/cli/commands/doctor.js +11 -3
- package/dist/cli/commands/fleet.js +87 -11
- package/dist/cli/commands/plan.js +13 -8
- package/dist/cli/commands/report.d.ts +2 -1
- package/dist/cli/commands/report.js +74 -8
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/status.js +43 -20
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +9 -2
- package/dist/compile/native.js +7 -0
- package/dist/config/config.d.ts +35 -2
- package/dist/config/config.js +86 -10
- package/dist/config/fleet-overlay.d.ts +13 -2
- package/dist/config/fleet-overlay.js +60 -0
- package/dist/drivers/herdr.d.ts +12 -0
- package/dist/drivers/herdr.js +51 -0
- package/dist/drivers/orca.d.ts +35 -2
- package/dist/drivers/orca.js +222 -67
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.js +2 -2
- package/dist/eval/canary.d.ts +2 -1
- package/dist/eval/canary.js +2 -2
- package/dist/eval/dispatch.js +1 -0
- package/dist/gates/acceptance.d.ts +9 -1
- package/dist/gates/acceptance.js +31 -4
- package/dist/gates/baseline.d.ts +32 -2
- package/dist/gates/baseline.js +111 -24
- package/dist/gates/cache.d.ts +8 -0
- package/dist/gates/cache.js +12 -2
- package/dist/gates/llm.d.ts +11 -4
- package/dist/gates/llm.js +40 -21
- package/dist/gates/review.d.ts +14 -1
- package/dist/gates/review.js +160 -34
- package/dist/gates/run-gates.d.ts +56 -4
- package/dist/gates/run-gates.js +358 -58
- package/dist/gates/test-manifest.d.ts +45 -1
- package/dist/gates/test-manifest.js +78 -12
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +2 -0
- package/dist/plan/scope.js +2 -2
- package/dist/route/preference.d.ts +20 -2
- package/dist/route/preference.js +48 -13
- package/dist/route/router.d.ts +12 -1
- package/dist/route/router.js +56 -24
- package/dist/run/consult.d.ts +15 -1
- package/dist/run/consult.js +18 -7
- package/dist/run/daemon.d.ts +38 -2
- package/dist/run/daemon.js +895 -192
- package/dist/run/git.d.ts +8 -0
- package/dist/run/git.js +14 -0
- package/dist/run/interactive-seed.d.ts +4 -0
- package/dist/run/interactive-seed.js +35 -9
- package/dist/run/journal.d.ts +152 -3
- package/dist/run/journal.js +551 -50
- package/dist/run/lease.d.ts +13 -0
- package/dist/run/lease.js +45 -0
- package/dist/run/merge.d.ts +3 -1
- package/dist/run/merge.js +3 -2
- package/dist/run/operator-summary.d.ts +3 -0
- package/dist/run/operator-summary.js +3 -1
- package/dist/run/protocol.d.ts +46 -1
- package/dist/run/protocol.js +14 -2
- package/dist/run/receipt-resolver.d.ts +22 -0
- package/dist/run/receipt-resolver.js +40 -1
- package/dist/run/repair-selection.d.ts +11 -1
- package/dist/run/repair-selection.js +17 -9
- package/dist/run/supervision.d.ts +7 -1
- package/dist/run/supervision.js +5 -2
- package/dist/run/wall-budget.d.ts +48 -0
- package/dist/run/wall-budget.js +280 -0
- package/dist/tui/cockpit/board.js +3 -3
- package/dist/tui/cockpit/decision-actions.d.ts +8 -5
- package/dist/tui/cockpit/decision-actions.js +55 -32
- package/dist/tui/cockpit/derive.js +13 -2
- package/dist/tui/cockpit/live-runtime.d.ts +10 -0
- package/dist/tui/cockpit/live-runtime.js +50 -3
- package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
- package/dist/tui/cockpit/run-cockpit.js +27 -2
- package/dist/tui/cockpit/run-view.d.ts +9 -2
- package/dist/tui/cockpit/run-view.js +66 -9
- package/dist/tui/cockpit/setup-cockpit.d.ts +6 -0
- package/dist/tui/cockpit/setup-cockpit.js +10 -3
- package/dist/tui/ink/fleet-app.d.ts +15 -3
- package/dist/tui/ink/fleet-app.js +91 -22
- package/package.json +3 -1
- package/schema/config.schema.json +825 -0
- package/skills/tickmarkr-loop/SKILL.md +15 -3
- package/skills/tickmarkr-overseer/SKILL.md +42 -0
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +91 -0
- package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
package/dist/run/lease.d.ts
CHANGED
|
@@ -33,7 +33,20 @@ export interface RepositoryLeaseOptions {
|
|
|
33
33
|
signal?: AbortSignal;
|
|
34
34
|
/** Called once per distinct holder the waiter is queued behind. */
|
|
35
35
|
onWait?: (holder: RepositoryLeaseHolder) => void;
|
|
36
|
+
/** The token a descendant inherited; defaults to REPOSITORY_LEASE_TOKEN_ENV. */
|
|
37
|
+
inherited?: string;
|
|
36
38
|
}
|
|
39
|
+
/** OBS-880/1071: the file lease's token, exported to the holder's descendants for the span it is held
|
|
40
|
+
* so a nested runner (a gate's suite under `tickmarkr verify`, a mutation child inside a leased suite)
|
|
41
|
+
* reenters instead of waiting on its own ancestor. Deliberately NOT COMMAND_LEASE_TOKEN_ENV: the
|
|
42
|
+
* installed daemon clears and rewrites that in-process token, and a command-lease token never names
|
|
43
|
+
* this file. Environment text alone proves nothing: see `reentrantHolder`. */
|
|
44
|
+
export declare const REPOSITORY_LEASE_TOKEN_ENV = "TICKMARKR_REPOSITORY_LEASE_TOKEN";
|
|
45
|
+
/** Reentry is identity-bound: the inherited token must name the record at THIS repository's lease path
|
|
46
|
+
* right now, its holder must be alive, and that holder must be a strict ancestor of this process. A
|
|
47
|
+
* forged, stale (released or superseded), foreign-repository or command-lease token matches no record;
|
|
48
|
+
* an in-process sibling is not a descendant — each of those waits like any other runner. */
|
|
49
|
+
export declare function reentrantHolder(path: string, inherited: string | undefined): Promise<RepositoryLeaseHolder | undefined>;
|
|
37
50
|
export declare function repositoryLeasePath(cwd: string): Promise<string>;
|
|
38
51
|
/** OBS-1057: dev+ino alone is not an identity across unlink/recreate — Linux hands the freed inode
|
|
39
52
|
* number straight to the next file, so a stale observation of a dead or corrupt reservation matched
|
package/dist/run/lease.js
CHANGED
|
@@ -91,6 +91,38 @@ export class CommandLeases {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
/** OBS-880/1071: the file lease's token, exported to the holder's descendants for the span it is held
|
|
95
|
+
* so a nested runner (a gate's suite under `tickmarkr verify`, a mutation child inside a leased suite)
|
|
96
|
+
* reenters instead of waiting on its own ancestor. Deliberately NOT COMMAND_LEASE_TOKEN_ENV: the
|
|
97
|
+
* installed daemon clears and rewrites that in-process token, and a command-lease token never names
|
|
98
|
+
* this file. Environment text alone proves nothing: see `reentrantHolder`. */
|
|
99
|
+
export const REPOSITORY_LEASE_TOKEN_ENV = "TICKMARKR_REPOSITORY_LEASE_TOKEN";
|
|
100
|
+
/** The strict ancestors of this process, from one `ps` snapshot; empty when unreadable (fail closed). */
|
|
101
|
+
const ancestorPids = () => new Promise((ok) => execFile("ps", ["-A", "-o", "pid=,ppid="], { encoding: "utf8" }, (error, stdout) => {
|
|
102
|
+
const parent = new Map();
|
|
103
|
+
if (!error)
|
|
104
|
+
for (const line of stdout.split("\n")) {
|
|
105
|
+
const [pid, ppid] = line.trim().split(/\s+/).map(Number);
|
|
106
|
+
if (Number.isInteger(pid) && Number.isInteger(ppid))
|
|
107
|
+
parent.set(pid, ppid);
|
|
108
|
+
}
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
for (let pid = error ? 0 : process.ppid; pid > 1 && !seen.has(pid); pid = parent.get(pid) ?? 0)
|
|
111
|
+
seen.add(pid);
|
|
112
|
+
ok(seen);
|
|
113
|
+
}));
|
|
114
|
+
/** Reentry is identity-bound: the inherited token must name the record at THIS repository's lease path
|
|
115
|
+
* right now, its holder must be alive, and that holder must be a strict ancestor of this process. A
|
|
116
|
+
* forged, stale (released or superseded), foreign-repository or command-lease token matches no record;
|
|
117
|
+
* an in-process sibling is not a descendant — each of those waits like any other runner. */
|
|
118
|
+
export async function reentrantHolder(path, inherited) {
|
|
119
|
+
if (!inherited)
|
|
120
|
+
return undefined;
|
|
121
|
+
const holder = readHolder(path);
|
|
122
|
+
if (holder?.token !== inherited || holder.pid === process.pid || !alive(holder.pid))
|
|
123
|
+
return undefined;
|
|
124
|
+
return (await ancestorPids()).has(holder.pid) ? holder : undefined;
|
|
125
|
+
}
|
|
94
126
|
const LEASE_FILE = "tickmarkr-runner.lease";
|
|
95
127
|
export async function repositoryLeasePath(cwd) {
|
|
96
128
|
const commonDir = await new Promise((ok, fail) => execFile("git", ["rev-parse", "--git-common-dir"], { cwd, encoding: "utf8" }, (error, stdout) => error ? fail(error) : ok(stdout.trim())));
|
|
@@ -283,6 +315,9 @@ const inspectAndReserve = (path, mine) => {
|
|
|
283
315
|
};
|
|
284
316
|
export async function withRepositoryLease(cwd, run, opts = {}) {
|
|
285
317
|
const path = await repositoryLeasePath(cwd);
|
|
318
|
+
// A descendant of the live holder runs inside its ancestor's reservation and releases nothing.
|
|
319
|
+
if (await reentrantHolder(path, opts.inherited ?? process.env[REPOSITORY_LEASE_TOKEN_ENV]))
|
|
320
|
+
return run();
|
|
286
321
|
const pollMs = opts.pollMs ?? 1_000;
|
|
287
322
|
const mine = { pid: process.pid, cwd, at: Date.now(), token: randomUUID() };
|
|
288
323
|
let waitingOn;
|
|
@@ -297,10 +332,20 @@ export async function withRepositoryLease(cwd, run, opts = {}) {
|
|
|
297
332
|
}
|
|
298
333
|
await new Promise((wake) => setTimeout(wake, pollMs));
|
|
299
334
|
}
|
|
335
|
+
// Exported before run() so every descendant forked or spawned during the hold can reenter; restored
|
|
336
|
+
// at release unless another reservation in this process has since exported its own.
|
|
337
|
+
const exported = process.env[REPOSITORY_LEASE_TOKEN_ENV];
|
|
338
|
+
process.env[REPOSITORY_LEASE_TOKEN_ENV] = mine.token;
|
|
300
339
|
try {
|
|
301
340
|
return await run();
|
|
302
341
|
}
|
|
303
342
|
finally {
|
|
343
|
+
if (process.env[REPOSITORY_LEASE_TOKEN_ENV] === mine.token) {
|
|
344
|
+
if (exported === undefined)
|
|
345
|
+
delete process.env[REPOSITORY_LEASE_TOKEN_ENV];
|
|
346
|
+
else
|
|
347
|
+
process.env[REPOSITORY_LEASE_TOKEN_ENV] = exported;
|
|
348
|
+
}
|
|
304
349
|
// Release is serialized with reclamation/acquisition and removes only this generation.
|
|
305
350
|
await withMutationLock(path, pollMs, undefined, () => {
|
|
306
351
|
if (readHolder(path)?.token === mine.token)
|
package/dist/run/merge.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TickmarkrConfig } from "../config/config.js";
|
|
2
|
-
import { type Baseline, type GateEvidenceOptions, type FailureClassification } from "../gates/baseline.js";
|
|
2
|
+
import { type Baseline, type BaselineProvenance, type GateEvidenceOptions, type FailureClassification } from "../gates/baseline.js";
|
|
3
3
|
import type { GateEvidenceReceipt } from "./protocol.js";
|
|
4
4
|
export interface TipVerifyResult {
|
|
5
5
|
evidenceReceipt?: GateEvidenceReceipt;
|
|
@@ -21,6 +21,8 @@ export interface TipVerifyResult {
|
|
|
21
21
|
spawnedCommand?: string;
|
|
22
22
|
/** Q121s: nonzero exit whose failures are ALL baseline-recorded — forgiven exactly as the battery forgives. */
|
|
23
23
|
forgiven?: boolean;
|
|
24
|
+
/** OBS-1123: the capture that recorded the forgiven fingerprints; absent for a legacy baseline that never said. */
|
|
25
|
+
baselineProvenance?: BaselineProvenance;
|
|
24
26
|
/** D-131: carried from a persisted per-gate verdict — this cycle did NOT execute the command. */
|
|
25
27
|
reused?: true;
|
|
26
28
|
/**
|
package/dist/run/merge.js
CHANGED
|
@@ -225,6 +225,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
225
225
|
exitCode: hit.exitCode ?? 0,
|
|
226
226
|
fingerprints: hit.meta?.fingerprints ?? [],
|
|
227
227
|
...(hit.meta?.forgiven ? { forgiven: true } : {}),
|
|
228
|
+
...(hit.meta?.forgiven && hit.meta.baselineProvenance ? { baselineProvenance: hit.meta.baselineProvenance } : {}),
|
|
228
229
|
details: formatReusedDetails(hit.details, id),
|
|
229
230
|
...(hit.meta?.reportPath ? { reportPath: hit.meta.reportPath } : {}),
|
|
230
231
|
...(hit.meta?.spawnedCommand ? { spawnedCommand: hit.meta.spawnedCommand } : {}),
|
|
@@ -382,7 +383,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
382
383
|
+ `${describeCapacity(entry?.capacity)} and this verification ran under ${describeCapacity(r.capacity)} `
|
|
383
384
|
+ `— forgiveness across a changed capacity is not evidence`
|
|
384
385
|
: `exit ${r.code}`)),
|
|
385
|
-
...(forgiven && !deficit ? { forgiven: true } : {}),
|
|
386
|
+
...(forgiven && !deficit ? { forgiven: true, ...(baseline?.provenance ? { baselineProvenance: baseline.provenance } : {}) } : {}),
|
|
386
387
|
...(cause ? { cause } : {}),
|
|
387
388
|
...(pass ? {} : { artifact }),
|
|
388
389
|
};
|
|
@@ -401,7 +402,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
401
402
|
const id = pending.get(result.gate);
|
|
402
403
|
if (id && id.tree === completedTree && !isInfraResult(result)) {
|
|
403
404
|
store.set(id, { ...result, meta: {
|
|
404
|
-
fingerprints: result.fingerprints, forgiven: result.forgiven,
|
|
405
|
+
fingerprints: result.fingerprints, forgiven: result.forgiven, baselineProvenance: result.baselineProvenance,
|
|
405
406
|
reportPath: result.reportPath, spawnedCommand: result.spawnedCommand,
|
|
406
407
|
artifact: result.artifact,
|
|
407
408
|
evidenceReceipt: result.evidenceReceipt, evidenceReceipts: result.evidenceReceipts,
|
|
@@ -13,6 +13,9 @@ export interface OperatorDecisionSnapshot {
|
|
|
13
13
|
readonly kind: string | undefined;
|
|
14
14
|
readonly tombstone: boolean;
|
|
15
15
|
readonly reason?: string;
|
|
16
|
+
/** OBS-1178: the park row's physical line and timestamp — the token a decision binds to. */
|
|
17
|
+
readonly line?: number;
|
|
18
|
+
readonly ts?: string;
|
|
16
19
|
};
|
|
17
20
|
readonly verbs: readonly string[];
|
|
18
21
|
readonly diagnostic?: string;
|
|
@@ -25,7 +25,9 @@ export function projectOperatorSummary(tasks, decisions) {
|
|
|
25
25
|
decisionRequired: verbs.length > 0,
|
|
26
26
|
...(diagnostic !== undefined || park.reason !== undefined
|
|
27
27
|
? { diagnostic: diagnostic ?? park.reason } : {}),
|
|
28
|
-
nextAction: verbs.length > 0
|
|
28
|
+
nextAction: verbs.length > 0
|
|
29
|
+
? `Choose a decision: ${verbs.join(", ")}${park.line !== undefined && park.ts !== undefined ? ` (--park ${park.line}@${park.ts})` : ""}`
|
|
30
|
+
: null,
|
|
29
31
|
};
|
|
30
32
|
if (!park.tombstone && park.kind === "infra" && task.missingCapability) {
|
|
31
33
|
const { capability, diagnostic: capabilityDiagnostic } = task.missingCapability;
|
package/dist/run/protocol.d.ts
CHANGED
|
@@ -53,6 +53,15 @@ export declare const EvidenceArtifactSchema: z.ZodObject<{
|
|
|
53
53
|
droppedBytes: z.ZodNumber;
|
|
54
54
|
truncated: z.ZodBoolean;
|
|
55
55
|
}, z.core.$strict>;
|
|
56
|
+
export declare const RedactionSchema: z.ZodObject<{
|
|
57
|
+
material: z.ZodBoolean;
|
|
58
|
+
counts: z.ZodOptional<z.ZodObject<{
|
|
59
|
+
token: z.ZodNumber;
|
|
60
|
+
assignment: z.ZodNumber;
|
|
61
|
+
secretEnv: z.ZodNumber;
|
|
62
|
+
benignEnv: z.ZodNumber;
|
|
63
|
+
}, z.core.$strict>>;
|
|
64
|
+
}, z.core.$strict>;
|
|
56
65
|
export declare const GateEvidenceReceiptSchema: z.ZodObject<{
|
|
57
66
|
invocationId: z.ZodString;
|
|
58
67
|
nonce: z.ZodOptional<z.ZodString>;
|
|
@@ -66,10 +75,10 @@ export declare const GateEvidenceReceiptSchema: z.ZodObject<{
|
|
|
66
75
|
termination: z.ZodObject<{
|
|
67
76
|
kind: z.ZodEnum<{
|
|
68
77
|
unknown: "unknown";
|
|
78
|
+
timeout: "timeout";
|
|
69
79
|
signal: "signal";
|
|
70
80
|
"not-started": "not-started";
|
|
71
81
|
exit: "exit";
|
|
72
|
-
timeout: "timeout";
|
|
73
82
|
}>;
|
|
74
83
|
exitCode: z.ZodNullable<z.ZodNumber>;
|
|
75
84
|
signal: z.ZodNullable<z.ZodString>;
|
|
@@ -85,6 +94,12 @@ export declare const GateEvidenceReceiptSchema: z.ZodObject<{
|
|
|
85
94
|
}>;
|
|
86
95
|
redaction: z.ZodObject<{
|
|
87
96
|
material: z.ZodBoolean;
|
|
97
|
+
counts: z.ZodOptional<z.ZodObject<{
|
|
98
|
+
token: z.ZodNumber;
|
|
99
|
+
assignment: z.ZodNumber;
|
|
100
|
+
secretEnv: z.ZodNumber;
|
|
101
|
+
benignEnv: z.ZodNumber;
|
|
102
|
+
}, z.core.$strict>>;
|
|
88
103
|
}, z.core.$strict>;
|
|
89
104
|
stdout: z.ZodObject<{
|
|
90
105
|
path: z.ZodString;
|
|
@@ -222,6 +237,11 @@ export declare const RoleInvocationStartEventSchema: z.ZodObject<{
|
|
|
222
237
|
adapter: z.ZodOptional<z.ZodString>;
|
|
223
238
|
model: z.ZodOptional<z.ZodString>;
|
|
224
239
|
vendor: z.ZodOptional<z.ZodString>;
|
|
240
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
241
|
+
low: "low";
|
|
242
|
+
medium: "medium";
|
|
243
|
+
high: "high";
|
|
244
|
+
}>>;
|
|
225
245
|
evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
226
246
|
}, z.core.$strict>;
|
|
227
247
|
}, z.core.$strict>;
|
|
@@ -240,6 +260,11 @@ export declare const RoleInvocationTerminalEventSchema: z.ZodObject<{
|
|
|
240
260
|
adapter: z.ZodOptional<z.ZodString>;
|
|
241
261
|
model: z.ZodOptional<z.ZodString>;
|
|
242
262
|
vendor: z.ZodOptional<z.ZodString>;
|
|
263
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
264
|
+
low: "low";
|
|
265
|
+
medium: "medium";
|
|
266
|
+
high: "high";
|
|
267
|
+
}>>;
|
|
243
268
|
evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
244
269
|
outcome: z.ZodEnum<{
|
|
245
270
|
failed: "failed";
|
|
@@ -352,6 +377,11 @@ export declare const DecisionEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
352
377
|
adapter: z.ZodOptional<z.ZodString>;
|
|
353
378
|
model: z.ZodOptional<z.ZodString>;
|
|
354
379
|
vendor: z.ZodOptional<z.ZodString>;
|
|
380
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
381
|
+
low: "low";
|
|
382
|
+
medium: "medium";
|
|
383
|
+
high: "high";
|
|
384
|
+
}>>;
|
|
355
385
|
evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
356
386
|
}, z.core.$strict>;
|
|
357
387
|
}, z.core.$strict>, z.ZodObject<{
|
|
@@ -369,6 +399,11 @@ export declare const DecisionEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
369
399
|
adapter: z.ZodOptional<z.ZodString>;
|
|
370
400
|
model: z.ZodOptional<z.ZodString>;
|
|
371
401
|
vendor: z.ZodOptional<z.ZodString>;
|
|
402
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
403
|
+
low: "low";
|
|
404
|
+
medium: "medium";
|
|
405
|
+
high: "high";
|
|
406
|
+
}>>;
|
|
372
407
|
evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
373
408
|
outcome: z.ZodEnum<{
|
|
374
409
|
failed: "failed";
|
|
@@ -473,6 +508,11 @@ export declare const DecisionEventWriteSchema: z.ZodDiscriminatedUnion<[z.ZodObj
|
|
|
473
508
|
adapter: z.ZodOptional<z.ZodString>;
|
|
474
509
|
model: z.ZodOptional<z.ZodString>;
|
|
475
510
|
vendor: z.ZodOptional<z.ZodString>;
|
|
511
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
512
|
+
low: "low";
|
|
513
|
+
medium: "medium";
|
|
514
|
+
high: "high";
|
|
515
|
+
}>>;
|
|
476
516
|
evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
477
517
|
}, z.core.$strict>;
|
|
478
518
|
event: z.ZodLiteral<"role-invocation-start">;
|
|
@@ -489,6 +529,11 @@ export declare const DecisionEventWriteSchema: z.ZodDiscriminatedUnion<[z.ZodObj
|
|
|
489
529
|
adapter: z.ZodOptional<z.ZodString>;
|
|
490
530
|
model: z.ZodOptional<z.ZodString>;
|
|
491
531
|
vendor: z.ZodOptional<z.ZodString>;
|
|
532
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
533
|
+
low: "low";
|
|
534
|
+
medium: "medium";
|
|
535
|
+
high: "high";
|
|
536
|
+
}>>;
|
|
492
537
|
evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
493
538
|
outcome: z.ZodEnum<{
|
|
494
539
|
failed: "failed";
|
package/dist/run/protocol.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { GATE_NAMES } from "../graph/schema.js";
|
|
2
|
+
import { EFFORTS, GATE_NAMES } from "../graph/schema.js";
|
|
3
3
|
import { normalizeGateOutcome } from "./outcome.js";
|
|
4
4
|
// T32 (F2): the decision protocol is deliberately smaller than the journal. Existing journals contain
|
|
5
5
|
// many observational event types and must remain readable; only the five event names below assert a
|
|
@@ -47,6 +47,16 @@ export const EvidenceArtifactSchema = z.object({
|
|
|
47
47
|
if (ref.availability === "available" && ref.sha256 === null)
|
|
48
48
|
ctx.addIssue({ code: "custom", message: "available bytes require a hash" });
|
|
49
49
|
});
|
|
50
|
+
// OBS-1139: one count per original span, never a value. A legacy receipt carries `material` alone and
|
|
51
|
+
// keeps it as recorded; a counted one must agree — benign HOME/TMPDIR substitutions are never material.
|
|
52
|
+
const RedactionCountsSchema = z.object({
|
|
53
|
+
token: z.number().int().nonnegative(),
|
|
54
|
+
assignment: z.number().int().nonnegative(),
|
|
55
|
+
secretEnv: z.number().int().nonnegative(),
|
|
56
|
+
benignEnv: z.number().int().nonnegative(),
|
|
57
|
+
}).strict();
|
|
58
|
+
export const RedactionSchema = z.object({ material: z.boolean(), counts: RedactionCountsSchema.optional() }).strict()
|
|
59
|
+
.refine(r => !r.counts || r.material === (r.counts.token + r.counts.assignment + r.counts.secretEnv > 0), { message: "material must agree with the token, assignment and secret-environment counts" });
|
|
50
60
|
export const GateEvidenceReceiptSchema = z.object({
|
|
51
61
|
invocationId: NonEmptyStringSchema,
|
|
52
62
|
nonce: NonEmptyStringSchema.optional(),
|
|
@@ -64,7 +74,7 @@ export const GateEvidenceReceiptSchema = z.object({
|
|
|
64
74
|
timedOut: z.boolean().nullable(),
|
|
65
75
|
}).strict(),
|
|
66
76
|
availability: z.enum(EVIDENCE_AVAILABILITIES),
|
|
67
|
-
redaction:
|
|
77
|
+
redaction: RedactionSchema,
|
|
68
78
|
stdout: EvidenceArtifactSchema,
|
|
69
79
|
stderr: EvidenceArtifactSchema,
|
|
70
80
|
}).strict();
|
|
@@ -133,6 +143,8 @@ const RoleInvocationIdentityDataSchema = z.object({
|
|
|
133
143
|
adapter: NonEmptyStringSchema.optional(),
|
|
134
144
|
model: NonEmptyStringSchema.optional(),
|
|
135
145
|
vendor: NonEmptyStringSchema.optional(),
|
|
146
|
+
// OBS-1182: the effort the seat actually launched at; absent = the CLI's own default, never a level.
|
|
147
|
+
effort: z.enum(EFFORTS).optional(),
|
|
136
148
|
evidence: EvidenceSchema.optional(),
|
|
137
149
|
});
|
|
138
150
|
const RoleInvocationStartDataSchema = RoleInvocationIdentityDataSchema.strict();
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { type GateEvidenceReceipt } from "./protocol.js";
|
|
2
2
|
export declare const RECEIPT_UNAVAILABLE_REASONS: readonly ["outside-root", "symlink", "hash-mismatch", "expired", "missing"];
|
|
3
3
|
export type ReceiptUnavailableReason = (typeof RECEIPT_UNAVAILABLE_REASONS)[number];
|
|
4
|
+
/** OBS-1140: at most this many newest eviction tombstones persist; an older eviction reads as missing. */
|
|
5
|
+
export declare const EVICTION_TOMBSTONE_LIMIT = 256;
|
|
6
|
+
/** Beside gate-evidence/, never inside it: tombstones are metadata, not retained artifact bytes. */
|
|
7
|
+
export declare const EVICTION_TOMBSTONES_FILE = "evidence-evictions.json";
|
|
8
|
+
/** Bound to the evicted bytes themselves, so a replacement artifact at the same path never inherits it. */
|
|
9
|
+
export interface EvictionTombstone {
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly sha256: string;
|
|
12
|
+
readonly retainedBytes: number;
|
|
13
|
+
}
|
|
14
|
+
/** Newest last, at most the limit; anything malformed is no tombstone at all (fail closed to missing). */
|
|
15
|
+
export declare function parseEvictionTombstones(bytes: Buffer): EvictionTombstone[];
|
|
4
16
|
export type ReceiptResolution = {
|
|
5
17
|
readonly ok: true;
|
|
6
18
|
readonly path: string;
|
|
@@ -12,6 +24,16 @@ export type ReceiptResolution = {
|
|
|
12
24
|
};
|
|
13
25
|
/** Resolve one artifact reference against the run root it was recorded under. Fails closed. */
|
|
14
26
|
export declare function resolveReceipt(reference: unknown, runRoot: string): ReceiptResolution;
|
|
27
|
+
export type ReceiptRedaction = {
|
|
28
|
+
readonly material: boolean;
|
|
29
|
+
readonly counts: GateEvidenceReceipt["redaction"]["counts"] | null;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* OBS-1139: what a receipt says was withheld. A counted receipt reports its per-span counts; a legacy
|
|
33
|
+
* one keeps its recorded `material` with no counts; anything unparseable is material (fail closed).
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveReceiptRedaction(receipt: unknown): ReceiptRedaction;
|
|
36
|
+
export declare function formatReceiptRedaction(r: ReceiptRedaction): string;
|
|
15
37
|
/** Both artifacts of a receipt, stdout first, each resolved independently. */
|
|
16
38
|
export declare function resolveReceiptArtifacts(receipt: GateEvidenceReceipt, runRoot: string): ReceiptResolution[];
|
|
17
39
|
/** One line per artifact for a human surface: the reference, then its verified availability. */
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { EvidenceArtifactSchema } from "./protocol.js";
|
|
4
|
+
import { EvidenceArtifactSchema, RedactionSchema } from "./protocol.js";
|
|
5
5
|
// OBS-1101 retrieval (D-174): the one production reader of an execution-evidence receipt. It answers
|
|
6
6
|
// from the bytes on disk under the run root the receipt was minted in and nothing else — no
|
|
7
7
|
// re-execution, no substitute chosen by name or timestamp, no symlink followed anywhere: not in the
|
|
8
8
|
// reference, not at the root, not above it. The caller hands a canonical root (its own trust anchor);
|
|
9
9
|
// a root reached through any link is refused outright.
|
|
10
10
|
export const RECEIPT_UNAVAILABLE_REASONS = ["outside-root", "symlink", "hash-mismatch", "expired", "missing"];
|
|
11
|
+
/** OBS-1140: at most this many newest eviction tombstones persist; an older eviction reads as missing. */
|
|
12
|
+
export const EVICTION_TOMBSTONE_LIMIT = 256;
|
|
13
|
+
/** Beside gate-evidence/, never inside it: tombstones are metadata, not retained artifact bytes. */
|
|
14
|
+
export const EVICTION_TOMBSTONES_FILE = "evidence-evictions.json";
|
|
15
|
+
/** Newest last, at most the limit; anything malformed is no tombstone at all (fail closed to missing). */
|
|
16
|
+
export function parseEvictionTombstones(bytes) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(bytes.toString("utf8"));
|
|
19
|
+
return Array.isArray(raw) ? raw.filter((t) => typeof t?.path === "string"
|
|
20
|
+
&& typeof t.sha256 === "string" && Number.isInteger(t.retainedBytes)).slice(-EVICTION_TOMBSTONE_LIMIT) : [];
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
11
26
|
/** Resolve one artifact reference against the run root it was recorded under. Fails closed. */
|
|
12
27
|
export function resolveReceipt(reference, runRoot) {
|
|
13
28
|
const parsed = EvidenceArtifactSchema.safeParse(reference);
|
|
@@ -27,6 +42,8 @@ export function resolveReceipt(reference, runRoot) {
|
|
|
27
42
|
if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
28
43
|
return { ok: false, path: ref.path, reason: "outside-root" };
|
|
29
44
|
const read = readUnderRoot(root, rel.split(sep));
|
|
45
|
+
if (read === "missing" && evictedByQuota(root, ref))
|
|
46
|
+
return { ok: false, path: ref.path, reason: "expired" };
|
|
30
47
|
if (typeof read === "string")
|
|
31
48
|
return { ok: false, path: ref.path, reason: read };
|
|
32
49
|
const sha256 = createHash("sha256").update(read).digest("hex");
|
|
@@ -34,6 +51,28 @@ export function resolveReceipt(reference, runRoot) {
|
|
|
34
51
|
return { ok: false, path: ref.path, reason: "hash-mismatch" };
|
|
35
52
|
return { ok: true, path: ref.path, sha256 };
|
|
36
53
|
}
|
|
54
|
+
/** A deletion is explained only by a tombstone naming this exact path, hash and length (OBS-1140). */
|
|
55
|
+
function evictedByQuota(root, ref) {
|
|
56
|
+
const bytes = readUnderRoot(root, [EVICTION_TOMBSTONES_FILE]);
|
|
57
|
+
return typeof bytes !== "string" && parseEvictionTombstones(bytes)
|
|
58
|
+
.some(t => t.path === ref.path && t.sha256 === ref.sha256 && t.retainedBytes === ref.retainedBytes);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* OBS-1139: what a receipt says was withheld. A counted receipt reports its per-span counts; a legacy
|
|
62
|
+
* one keeps its recorded `material` with no counts; anything unparseable is material (fail closed).
|
|
63
|
+
*/
|
|
64
|
+
export function resolveReceiptRedaction(receipt) {
|
|
65
|
+
const parsed = RedactionSchema.safeParse(receipt?.redaction);
|
|
66
|
+
if (!parsed.success)
|
|
67
|
+
return { material: true, counts: null };
|
|
68
|
+
return { material: parsed.data.material, counts: parsed.data.counts ?? null };
|
|
69
|
+
}
|
|
70
|
+
export function formatReceiptRedaction(r) {
|
|
71
|
+
const head = r.material ? "material" : "nonmaterial";
|
|
72
|
+
return r.counts
|
|
73
|
+
? `redaction ${head} token=${r.counts.token} assignment=${r.counts.assignment} secret-env=${r.counts.secretEnv} benign-env=${r.counts.benignEnv}`
|
|
74
|
+
: `redaction ${head} (uncounted)`;
|
|
75
|
+
}
|
|
37
76
|
const same = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
38
77
|
/** Every ancestor of the root, root first excluded — "/" itself cannot be a link. */
|
|
39
78
|
function ancestors(root) {
|
|
@@ -2,9 +2,19 @@ import type { JournalEvent } from "./journal.js";
|
|
|
2
2
|
export interface RepairSelectionDecision {
|
|
3
3
|
selectTests: boolean;
|
|
4
4
|
requiredFiles: string[];
|
|
5
|
-
reason: "no-test-failure" | "known-failing-files" | "legacy-test-failure" | "unattributed-test-failure"
|
|
5
|
+
reason: "no-test-failure" | "known-failing-files" | "legacy-test-failure" | "unattributed-test-failure";
|
|
6
6
|
}
|
|
7
7
|
type SelectionEvent = Pick<JournalEvent, "event" | "taskId" | "data">;
|
|
8
|
+
/** OBS-1199: repair selection is on unless a config says otherwise, independent of the optional
|
|
9
|
+
* execution budget (which stays subprocess-only). Either explicit `false` turns it off. */
|
|
10
|
+
export declare function repairSelectionEnabled(cfg?: {
|
|
11
|
+
executionPolicy?: {
|
|
12
|
+
repairSelection?: unknown;
|
|
13
|
+
};
|
|
14
|
+
gates?: {
|
|
15
|
+
repairSelection?: unknown;
|
|
16
|
+
};
|
|
17
|
+
}): boolean;
|
|
8
18
|
/** Diagnostic selection only: neither a narrow pass nor this decision can authorize integration.
|
|
9
19
|
* Distrust and known failing files survive later green results, approvals and resume. A historical
|
|
10
20
|
* ambiguous failure is never reinterpreted using a newly enabled policy. */
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** OBS-1199: repair selection is on unless a config says otherwise, independent of the optional
|
|
2
|
+
* execution budget (which stays subprocess-only). Either explicit `false` turns it off. */
|
|
3
|
+
export function repairSelectionEnabled(cfg) {
|
|
4
|
+
return cfg?.executionPolicy?.repairSelection !== false && cfg?.gates?.repairSelection !== false;
|
|
5
|
+
}
|
|
1
6
|
/** These are literal repository-relative identities. Existence and selection support are checked
|
|
2
7
|
* against the current worktree by the gate; this journal fold cannot establish either. */
|
|
3
8
|
function fileIdentities(value) {
|
|
@@ -32,16 +37,19 @@ export function repairSelectionDecision(events, taskId, enabled) {
|
|
|
32
37
|
distrust ??= "unattributed-test-failure";
|
|
33
38
|
continue;
|
|
34
39
|
}
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
const selected = fileIdentities(data.selectedTests);
|
|
40
|
+
// OBS-1199: a full-suite red counts only when it positively says it was full — the replacement
|
|
41
|
+
// after a screen (`fullSuite`), or an ordinary full run whose recorded selection decision says
|
|
42
|
+
// so — and names its failing files. Those files join every later screen even when no selector
|
|
43
|
+
// reaches them; the merge candidate still runs the complete suite. No attribution means full.
|
|
42
44
|
const failing = fileIdentities(data.failingFiles);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
const selected = fileIdentities(data.selectedTests);
|
|
46
|
+
const decision = data.selectionDecision;
|
|
47
|
+
const replacement = data.fullSuite === true;
|
|
48
|
+
const ordinaryFull = data.fullSuite === undefined && data.selectedTests === undefined
|
|
49
|
+
&& typeof decision === "object" && decision !== null && decision.scope === "full";
|
|
50
|
+
const screen = (data.fullSuite === undefined || data.fullSuite === false) && selected !== undefined
|
|
51
|
+
&& failing !== undefined && failing.every((file) => selected.includes(file));
|
|
52
|
+
if (!failing || !(replacement || ordinaryFull || screen)) {
|
|
45
53
|
distrust ??= "unattributed-test-failure";
|
|
46
54
|
continue;
|
|
47
55
|
}
|
|
@@ -48,6 +48,7 @@ export interface SupervisionArm {
|
|
|
48
48
|
export declare const supervisionArmPath: (repoRoot: string, tier: SupervisionTier) => string;
|
|
49
49
|
/** Explicit arming acknowledges the current marker without deleting it. */
|
|
50
50
|
export declare function newSupervisionArm(repoRoot: string, tier: SupervisionTier, armId?: string, initial?: boolean): SupervisionArm;
|
|
51
|
+
export declare const supervisionPresencePath: (repoRoot: string, tier: SupervisionTier, id: string) => string;
|
|
51
52
|
export declare function beatSupervision(repoRoot: string, tier: SupervisionTier, seat?: string, observation?: SupervisionBeatObservation, options?: {
|
|
52
53
|
arm?: SupervisionArm;
|
|
53
54
|
armId?: string;
|
|
@@ -69,7 +70,10 @@ export declare function supervisionStatus(repoRoot: string, tier: SupervisionTie
|
|
|
69
70
|
export declare const readSupervision: (repoRoot: string, now?: number) => TierLiveness[];
|
|
70
71
|
/** One line, one word per tier. Shared by both status surfaces so neither can render a state twice. */
|
|
71
72
|
export declare const supervisionText: (tiers: readonly TierLiveness[], divider?: string) => string;
|
|
72
|
-
/** A board's durable identity is independent of its short title and of other observers.
|
|
73
|
+
/** A board's durable identity is independent of its short title and of other observers.
|
|
74
|
+
* `handle` is the terminal the claiming observer reports running in, when its host exports one: the
|
|
75
|
+
* one fact that names the board's pane when the placing split's receipt never arrived (OBS-1172).
|
|
76
|
+
* `retired` marks a tombstone a driver keeps in the record's place; it is never answered again. */
|
|
73
77
|
export interface WatchBoardOwner {
|
|
74
78
|
repo: string;
|
|
75
79
|
runId: string;
|
|
@@ -80,6 +84,8 @@ export interface WatchBoardOwner {
|
|
|
80
84
|
token: string;
|
|
81
85
|
pid?: number;
|
|
82
86
|
armId?: string;
|
|
87
|
+
handle?: string;
|
|
88
|
+
retired?: true;
|
|
83
89
|
}
|
|
84
90
|
export declare const WATCH_OWNER_ENV = "TICKMARKR_WATCH_OWNER";
|
|
85
91
|
export declare function readWatchBoard(repo: string, runId: string): WatchBoardOwner | undefined;
|
package/dist/run/supervision.js
CHANGED
|
@@ -154,7 +154,7 @@ const supervisionObligationPath = (repoRoot, tier) => join(supervisionDir(repoRo
|
|
|
154
154
|
// Freshness decides presence, never a process table (SUP-02): a watcher that is killed cannot remove
|
|
155
155
|
// its own file, and an unremoved file ages past the same ceiling a beat does and stops counting.
|
|
156
156
|
const presencePrefix = (tier) => `${tier}.live.`;
|
|
157
|
-
const supervisionPresencePath = (repoRoot, tier, id) => join(supervisionDir(repoRoot), `${presencePrefix(tier)}${id}`);
|
|
157
|
+
export const supervisionPresencePath = (repoRoot, tier, id) => join(supervisionDir(repoRoot), `${presencePrefix(tier)}${id}`);
|
|
158
158
|
/** Every presence file on this tier, by name. Missing directory ⇒ nobody is present. */
|
|
159
159
|
const presenceNames = (repoRoot, tier) => {
|
|
160
160
|
try {
|
|
@@ -567,6 +567,8 @@ export const supervisionText = (tiers, divider = " · ") => `supervision: ${tier
|
|
|
567
567
|
`${t.clearOwedSince ? ` CLEAR-OWED since ${t.clearOwedSince}` : ""}` +
|
|
568
568
|
`${t.clearOwedUnreadable ? " CLEAR-OWED unreadable" : ""}`).join(divider)}`;
|
|
569
569
|
export const WATCH_OWNER_ENV = "TICKMARKR_WATCH_OWNER";
|
|
570
|
+
/** Orca exports each terminal's own handle into the processes it runs. */
|
|
571
|
+
const TERMINAL_HANDLE_ENV = "ORCA_TERMINAL_HANDLE";
|
|
570
572
|
const boardPath = (repo, runId) => join(supervisionDir(repo), `watch-board.${parseRunId(runId)}.json`);
|
|
571
573
|
const boardMessagePath = (owner, kind) => {
|
|
572
574
|
if (!/^[a-f0-9-]{36}$/.test(owner.token))
|
|
@@ -642,7 +644,8 @@ export function observeNamedRun(repo, runId, env = process.env) {
|
|
|
642
644
|
const candidate = readWatchBoard(repo, runId);
|
|
643
645
|
if (!candidate || candidate.token !== token || candidate.pid !== undefined)
|
|
644
646
|
throw new Error("watch ownership unknown; refusing narrator acknowledgement");
|
|
645
|
-
|
|
647
|
+
const handle = env[TERMINAL_HANDLE_ENV]?.trim();
|
|
648
|
+
owner = { ...candidate, pid: process.pid, armId: armed.id, ...(handle ? { handle } : {}) };
|
|
646
649
|
atomicRecord(boardPath(repo, runId), owner);
|
|
647
650
|
}
|
|
648
651
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { JournalEvent } from "./journal.js";
|
|
2
|
+
/**
|
|
3
|
+
* OBS-1201: where a run's wall time went. Every instant of the engagement window lands in exactly ONE
|
|
4
|
+
* bucket — the first of WALL_PRIORITY with a span covering it — and what no span covers is residual,
|
|
5
|
+
* so the exposed buckets always sum to the wall. Task-time sums every span as recorded, so concurrent
|
|
6
|
+
* work (two workers, four suites waiting on one lease) stays visible without inflating the wall.
|
|
7
|
+
*
|
|
8
|
+
* Only journaled observations become spans: a fresh gate row's measured duration less the matched
|
|
9
|
+
* suite waits inside it, a worker launch matched to its result, a suite wait matched to its admission,
|
|
10
|
+
* and the gap a restart left between one engagement's last row and the next run-resume. A copy of
|
|
11
|
+
* evidence — a cache reuse, a journal replay, a row naming an invocation already counted — never adds
|
|
12
|
+
* a span; a reused full suite's row still times the fresh screen it carries. A row without a duration, a launch, wait or gate start with no matching end, and one cut by
|
|
13
|
+
* a restart are counted, never timed. Residual
|
|
14
|
+
* is unattributed time, not a claim of idleness. Pure: journal rows in, numbers out.
|
|
15
|
+
*/
|
|
16
|
+
export declare const WALL_PRIORITY: readonly ["interruption", "test", "semantics", "worker", "queue", "other-gate"];
|
|
17
|
+
export type WallBucket = (typeof WALL_PRIORITY)[number];
|
|
18
|
+
type PerBucket = Record<WallBucket, number>;
|
|
19
|
+
export interface WallBudget {
|
|
20
|
+
wallMs: number;
|
|
21
|
+
exposedMs: PerBucket;
|
|
22
|
+
residualMs: number;
|
|
23
|
+
taskMs: PerBucket;
|
|
24
|
+
/** Gate-result rows by provenance. A declined gate never ran and is none of these. */
|
|
25
|
+
evidence: {
|
|
26
|
+
fresh: number;
|
|
27
|
+
replay: number;
|
|
28
|
+
reuse: number;
|
|
29
|
+
unknown: number;
|
|
30
|
+
};
|
|
31
|
+
/** Evidence that exists but carries no timing: rows without a duration, unmatched and interrupted starts. */
|
|
32
|
+
untimed: Record<WallBucket, {
|
|
33
|
+
unknown: number;
|
|
34
|
+
unmatched: number;
|
|
35
|
+
interrupted: number;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
/** Undefined when the journal names no measurable window (no run-start, or unreadable timestamps). */
|
|
39
|
+
export declare function wallBudget(events: readonly JournalEvent[]): WallBudget | undefined;
|
|
40
|
+
/** Whole seconds past a minute, one decimal below ten seconds — never a rounded-away "0s" for a real span. */
|
|
41
|
+
export declare const formatSpan: (ms: number) => string;
|
|
42
|
+
/**
|
|
43
|
+
* One fact per bucket, then residual and the evidence tally — the words every surface prints. A
|
|
44
|
+
* bucket whose only evidence is untimed reads `unknown`, never a zero it did not measure.
|
|
45
|
+
*/
|
|
46
|
+
export declare function wallBudgetFacts(b: WallBudget): Array<[label: string, fact: string]>;
|
|
47
|
+
export declare const WALL_PRIORITY_TEXT: string;
|
|
48
|
+
export {};
|