tickmarkr 2.8.7 → 2.8.8
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/cli/commands/compile.js +1 -1
- package/dist/cli/commands/report.js +21 -12
- package/dist/cli/commands/status.js +14 -2
- package/dist/cli/commands/verify.js +29 -5
- package/dist/compile/collateral.d.ts +3 -1
- package/dist/compile/collateral.js +8 -16
- package/dist/compile/native.js +7 -2
- package/dist/gates/acceptance.d.ts +8 -0
- package/dist/gates/acceptance.js +54 -37
- package/dist/gates/run-gates.js +31 -22
- package/dist/report/bundle.d.ts +5 -2
- package/dist/report/bundle.js +11 -6
- package/dist/report/compare.d.ts +9 -0
- package/dist/report/compare.js +58 -40
- package/dist/report/operator-record.js +4 -3
- package/dist/run/daemon.d.ts +7 -3
- package/dist/run/daemon.js +64 -27
- package/dist/run/journal.js +11 -2
- package/dist/run/repair-disposition.d.ts +1 -1
- package/dist/run/repair-disposition.js +33 -2
- package/package.json +1 -1
package/dist/report/bundle.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { EvidenceCitation } from "../gates/acceptance.js";
|
|
2
2
|
import type { RunEnvironment } from "../run/environment.js";
|
|
3
3
|
import type { JournalEvent } from "../run/journal.js";
|
|
4
|
-
|
|
4
|
+
import { type SessionEnvironment } from "./compare.js";
|
|
5
|
+
export declare const BUNDLE_SCHEMA_VERSION = 3;
|
|
5
6
|
export declare const gateDeclined: (data: Record<string, unknown>) => boolean;
|
|
6
7
|
export declare const KNOWN_LIMITS: readonly string[];
|
|
7
8
|
export type BundleEvidence = string | EvidenceCitation;
|
|
@@ -18,6 +19,7 @@ export interface BundleGateResult {
|
|
|
18
19
|
/** T11: true when the gate declined to run (journaled skipped:true, e.g. review below its
|
|
19
20
|
* complexity threshold) — neither a pass nor a fail; the details carry the reason. */
|
|
20
21
|
declined?: boolean;
|
|
22
|
+
held?: boolean;
|
|
21
23
|
details: string;
|
|
22
24
|
}
|
|
23
25
|
export interface BundleTask {
|
|
@@ -28,7 +30,7 @@ export interface BundleTask {
|
|
|
28
30
|
judgeCriteria: BundleJudgeCriterion[];
|
|
29
31
|
}
|
|
30
32
|
export interface BundleContentHashes {
|
|
31
|
-
/** sha256 of
|
|
33
|
+
/** Queue 35 (D-2134 R6): sha256 of JSON.stringify of the parsed source events. */
|
|
32
34
|
journal: string;
|
|
33
35
|
/** graphDefinitionHash from run-start when recorded. */
|
|
34
36
|
graphDefinitionHash?: string;
|
|
@@ -38,6 +40,7 @@ export interface ProofBundle {
|
|
|
38
40
|
schemaVersion: number;
|
|
39
41
|
runId: string;
|
|
40
42
|
environment?: RunEnvironment;
|
|
43
|
+
environments: SessionEnvironment[];
|
|
41
44
|
contentHashes: BundleContentHashes;
|
|
42
45
|
tasks: BundleTask[];
|
|
43
46
|
/** Plain-language known limits — this is not an unconditional proof of correctness. */
|
package/dist/report/bundle.js
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
// redactSecrets seam. No I/O and no network: the caller (report CLI) owns the write.
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { redactSecrets } from "../run/redact.js";
|
|
7
|
-
import {
|
|
7
|
+
import { normalizeGateOutcome } from "../run/outcome.js";
|
|
8
|
+
import { recordedEnvironment, recordedEnvironments } from "./compare.js";
|
|
8
9
|
// Bump when the packet shape changes in a way a future reader must branch on before parsing.
|
|
9
10
|
// 2 (T11): gates gained `declined`, and `pass` narrowed from "the gate did not fail" to "the gate
|
|
10
11
|
// ran and passed" — a schema-1 reader would read a decline as a plain failure.
|
|
11
|
-
|
|
12
|
+
// Queue 35 (D-2134 R6): held gates and session environments change the packet's meaning.
|
|
13
|
+
export const BUNDLE_SCHEMA_VERSION = 3;
|
|
12
14
|
// T11: the one predicate every decline surface shares — the record, the text tickmark rate, the
|
|
13
15
|
// comparison metrics and this packet must agree on what "never ran" means. Modern runs journal
|
|
14
16
|
// `skipped: true`; runs from before that field carry the same decline only in the details prefix
|
|
@@ -19,7 +21,7 @@ export const gateDeclined = (data) => data.skipped === true || /^skipped\b/.test
|
|
|
19
21
|
export const KNOWN_LIMITS = [
|
|
20
22
|
"This packet is a portable snapshot of journaled run facts, not an independent re-verification of the work or its gates.",
|
|
21
23
|
"Judge evidence citations are copied from the journal as the judge recorded them; this packet does not re-validate citations against the judged diff.",
|
|
22
|
-
"Content hashes bind this packet to the journal
|
|
24
|
+
"Content hashes bind this packet to the JSON.stringify serialization of parsed journal events used to build it; they do not prove the underlying gates, merges, or tip verification were correct.",
|
|
23
25
|
"Secret-shaped strings are redacted by local pattern matching only; redaction is not a formal security audit.",
|
|
24
26
|
"Producing this packet never contacts a network and never uploads anything.",
|
|
25
27
|
];
|
|
@@ -135,11 +137,13 @@ export function buildProofBundle(runId, events) {
|
|
|
135
137
|
const judgeCriteria = [];
|
|
136
138
|
for (const g of gateEvents) {
|
|
137
139
|
const gate = typeof g.data.gate === "string" ? g.data.gate : "unknown";
|
|
138
|
-
//
|
|
140
|
+
// Queue 35 (D-2134 R6): a passing screen remains held until its full suite speaks.
|
|
139
141
|
const declined = gateDeclined(g.data);
|
|
140
|
-
const
|
|
142
|
+
const outcome = normalizeGateOutcome(g.data);
|
|
143
|
+
const held = outcome.kind === "held";
|
|
144
|
+
const pass = outcome.kind === "passed" && !declined;
|
|
141
145
|
const details = typeof g.data.details === "string" ? g.data.details : "";
|
|
142
|
-
gates.push({ gate, pass, ...(declined ? { declined: true } : {}), details });
|
|
146
|
+
gates.push({ gate, pass, ...(declined ? { declined: true } : {}), ...(held ? { held: true } : {}), details });
|
|
143
147
|
if (gate === "acceptance") {
|
|
144
148
|
for (const c of parseJudgeCriteria(g.data))
|
|
145
149
|
judgeCriteria.push(c);
|
|
@@ -156,6 +160,7 @@ export function buildProofBundle(runId, events) {
|
|
|
156
160
|
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
157
161
|
runId,
|
|
158
162
|
...(environment ? { environment } : {}),
|
|
163
|
+
environments: recordedEnvironments(events),
|
|
159
164
|
contentHashes: {
|
|
160
165
|
journal: journalHash,
|
|
161
166
|
...(gHash ? { graphDefinitionHash: gHash } : {}),
|
package/dist/report/compare.d.ts
CHANGED
|
@@ -8,11 +8,18 @@ export type EnvironmentCompare = {
|
|
|
8
8
|
comparable: false;
|
|
9
9
|
reason: "mismatch";
|
|
10
10
|
recorded: string;
|
|
11
|
+
mixedSessions?: true;
|
|
11
12
|
} | {
|
|
12
13
|
comparable: false;
|
|
13
14
|
reason: "unbound";
|
|
14
15
|
};
|
|
15
16
|
export declare function hasRunStart(events: JournalEvent[]): boolean;
|
|
17
|
+
export interface SessionEnvironment {
|
|
18
|
+
event: "run-start" | "run-resume";
|
|
19
|
+
ts: string;
|
|
20
|
+
environment?: RunEnvironment;
|
|
21
|
+
}
|
|
22
|
+
export declare function recordedEnvironments(events: JournalEvent[]): SessionEnvironment[];
|
|
16
23
|
export declare function recordedEnvironment(events: JournalEvent[]): RunEnvironment | undefined;
|
|
17
24
|
export declare function environmentComparable(baselineEnv: RunEnvironment | undefined, currentEnv: RunEnvironment | undefined): EnvironmentCompare;
|
|
18
25
|
export interface RunMetrics {
|
|
@@ -21,6 +28,7 @@ export interface RunMetrics {
|
|
|
21
28
|
gateFail: number;
|
|
22
29
|
gateTotal: number;
|
|
23
30
|
costUsd: number | undefined;
|
|
31
|
+
costPartial: boolean;
|
|
24
32
|
tokensTotal: number | undefined;
|
|
25
33
|
}
|
|
26
34
|
export declare function runMetrics(events: JournalEvent[], rows?: TelemetryRow[], cost?: CostConfig): RunMetrics;
|
|
@@ -28,6 +36,7 @@ export interface RunDelta {
|
|
|
28
36
|
durationMs: number | undefined;
|
|
29
37
|
gateFail: number;
|
|
30
38
|
costUsd: number | undefined;
|
|
39
|
+
costPartial: boolean;
|
|
31
40
|
tokensTotal: number | undefined;
|
|
32
41
|
}
|
|
33
42
|
export type CompareOutcome = {
|
package/dist/report/compare.js
CHANGED
|
@@ -1,43 +1,50 @@
|
|
|
1
|
+
import { normalizeGateOutcome } from "../run/outcome.js";
|
|
1
2
|
import { estimateCosts } from "./cost.js";
|
|
2
3
|
export function hasRunStart(events) {
|
|
3
4
|
return events.some((e) => e.event === "run-start");
|
|
4
5
|
}
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
// Queue 35 (D-2134 R6): validate each session's stamp before folding the run's identity.
|
|
7
|
+
function parseEnvironment(env) {
|
|
8
|
+
if (!env || typeof env !== "object" || Array.isArray(env))
|
|
9
|
+
return undefined;
|
|
10
|
+
const o = env;
|
|
11
|
+
if (typeof o.tickmarkrVersion !== "string" || !o.tickmarkrVersion.trim()
|
|
12
|
+
|| typeof o.configHash !== "string" || !o.configHash.trim())
|
|
13
|
+
return undefined;
|
|
14
|
+
if (!o.adapterVersions || typeof o.adapterVersions !== "object" || Array.isArray(o.adapterVersions))
|
|
15
|
+
return undefined;
|
|
16
|
+
const adapterVersions = {};
|
|
17
|
+
for (const [k, v] of Object.entries(o.adapterVersions)) {
|
|
18
|
+
if (typeof v !== "string")
|
|
17
19
|
return undefined;
|
|
18
|
-
|
|
19
|
-
for (const [k, v] of Object.entries(o.adapterVersions)) {
|
|
20
|
-
if (typeof v !== "string")
|
|
21
|
-
return undefined;
|
|
22
|
-
adapterVersions[k] = v;
|
|
23
|
-
}
|
|
24
|
-
// Capacity arrived in v2.0 as a pair. Two absent fields are an honest older record; one without
|
|
25
|
-
// the other is a malformed new record and fails closed instead of being compared as legacy.
|
|
26
|
-
const hasCores = Object.hasOwn(o, "cores");
|
|
27
|
-
const hasForkCap = Object.hasOwn(o, "forkCap");
|
|
28
|
-
if (hasCores !== hasForkCap)
|
|
29
|
-
return undefined;
|
|
30
|
-
if (hasCores && (typeof o.cores !== "number" || !Number.isFinite(o.cores) || o.cores <= 0
|
|
31
|
-
|| typeof o.forkCap !== "number" || !Number.isFinite(o.forkCap) || o.forkCap <= 0))
|
|
32
|
-
return undefined;
|
|
33
|
-
return {
|
|
34
|
-
tickmarkrVersion: o.tickmarkrVersion,
|
|
35
|
-
configHash: o.configHash,
|
|
36
|
-
adapterVersions,
|
|
37
|
-
...(hasCores ? { cores: o.cores, forkCap: o.forkCap } : {}),
|
|
38
|
-
};
|
|
20
|
+
adapterVersions[k] = v;
|
|
39
21
|
}
|
|
40
|
-
|
|
22
|
+
// Capacity arrived in v2.0 as a pair. Two absent fields are an honest older record; one without
|
|
23
|
+
// the other is a malformed new record and fails closed instead of being compared as legacy.
|
|
24
|
+
const hasCores = Object.hasOwn(o, "cores");
|
|
25
|
+
const hasForkCap = Object.hasOwn(o, "forkCap");
|
|
26
|
+
if (hasCores !== hasForkCap)
|
|
27
|
+
return undefined;
|
|
28
|
+
if (hasCores && (typeof o.cores !== "number" || !Number.isFinite(o.cores) || o.cores <= 0
|
|
29
|
+
|| typeof o.forkCap !== "number" || !Number.isFinite(o.forkCap) || o.forkCap <= 0))
|
|
30
|
+
return undefined;
|
|
31
|
+
return {
|
|
32
|
+
tickmarkrVersion: o.tickmarkrVersion,
|
|
33
|
+
configHash: o.configHash,
|
|
34
|
+
adapterVersions,
|
|
35
|
+
...(hasCores ? { cores: o.cores, forkCap: o.forkCap } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function recordedEnvironments(events) {
|
|
39
|
+
return events.flatMap((e) => e.event === "run-start" || e.event === "run-resume"
|
|
40
|
+
? [{ event: e.event, ts: e.ts, environment: parseEnvironment(e.data.environment) }]
|
|
41
|
+
: []);
|
|
42
|
+
}
|
|
43
|
+
export function recordedEnvironment(events) {
|
|
44
|
+
const sessions = recordedEnvironments(events);
|
|
45
|
+
const first = sessions[0]?.environment;
|
|
46
|
+
return first && sessions.every(({ environment }) => environment && envEqual(first, environment))
|
|
47
|
+
? first : undefined;
|
|
41
48
|
}
|
|
42
49
|
// Canonical identity string for the `recorded` field — configHash is the axis the acceptance
|
|
43
50
|
// criteria name; full environment equality still decides .comparable.
|
|
@@ -81,12 +88,14 @@ export function runMetrics(events, rows = [], cost = {}) {
|
|
|
81
88
|
durationMs = to - from;
|
|
82
89
|
}
|
|
83
90
|
const gates = events.filter((e) => e.event === "gate-result");
|
|
84
|
-
const gatePass = gates.filter((e) => e.data.
|
|
85
|
-
const gateFail = gates.filter((e) => e.data.
|
|
91
|
+
const gatePass = gates.filter((e) => normalizeGateOutcome(e.data).kind === "passed").length;
|
|
92
|
+
const gateFail = gates.filter((e) => normalizeGateOutcome(e.data).kind === "failed").length;
|
|
86
93
|
let costUsd;
|
|
87
94
|
let costSum = 0;
|
|
88
95
|
let hasCost = false;
|
|
89
|
-
|
|
96
|
+
const prices = estimateCosts(rows, cost);
|
|
97
|
+
const costPartial = prices.some((p) => p.partialMetering);
|
|
98
|
+
for (const p of prices) {
|
|
90
99
|
if (p.apiUsd !== undefined) {
|
|
91
100
|
costSum += p.apiUsd;
|
|
92
101
|
hasCost = true;
|
|
@@ -110,7 +119,7 @@ export function runMetrics(events, rows = [], cost = {}) {
|
|
|
110
119
|
}
|
|
111
120
|
if (hasTokens)
|
|
112
121
|
tokensTotal = tokenSum;
|
|
113
|
-
return { durationMs, gatePass, gateFail, gateTotal: gates.length, costUsd, tokensTotal };
|
|
122
|
+
return { durationMs, gatePass, gateFail, gateTotal: gates.length, costUsd, costPartial, tokensTotal };
|
|
114
123
|
}
|
|
115
124
|
const n = (x) => x.toLocaleString("en-US");
|
|
116
125
|
const EM = "—";
|
|
@@ -161,6 +170,9 @@ function comparabilityLine(cmp, baselineEnv, currentEnv) {
|
|
|
161
170
|
if (cmp.reason === "unbound") {
|
|
162
171
|
return "comparability caveat — one or both runs lack a recorded environment identity; not apples-to-apples";
|
|
163
172
|
}
|
|
173
|
+
if (cmp.mixedSessions) {
|
|
174
|
+
return "comparability caveat — environment identity disagrees across mixed sessions; not apples-to-apples";
|
|
175
|
+
}
|
|
164
176
|
const base = baselineEnv?.configHash ?? EM;
|
|
165
177
|
const cur = currentEnv?.configHash ?? EM;
|
|
166
178
|
return `comparability caveat — environment identity disagrees (baseline configHash=${base} ≠ current configHash=${cur}; recorded baseline ${cmp.recorded}); not apples-to-apples`;
|
|
@@ -182,7 +194,7 @@ export function renderComparison(opts) {
|
|
|
182
194
|
`| duration | ${fmtDuration(baseline.durationMs)} | ${fmtDuration(current.durationMs)} | ${fmtSignedDuration(delta.durationMs)} |`,
|
|
183
195
|
`| gate failures | ${fmtInt(baseline.gateFail)} | ${fmtInt(current.gateFail)} | ${fmtSignedInt(delta.gateFail)} |`,
|
|
184
196
|
`| gate pass rate | ${baseline.gateTotal ? `${baseline.gatePass}/${baseline.gateTotal}` : EM} | ${current.gateTotal ? `${current.gatePass}/${current.gateTotal}` : EM} | ${EM} |`,
|
|
185
|
-
`| cost | ${fmtUsd(baseline.costUsd)} | ${fmtUsd(current.costUsd)} | ${fmtSignedUsd(delta.costUsd)} |`,
|
|
197
|
+
`| cost | ${fmtUsd(baseline.costUsd)}${baseline.costPartial ? " (partial)" : ""} | ${fmtUsd(current.costUsd)}${current.costPartial ? " (partial)" : ""} | ${fmtSignedUsd(delta.costUsd)}${delta.costPartial ? " (partial)" : ""} |`,
|
|
186
198
|
`| tokens | ${fmtInt(baseline.tokensTotal)} | ${fmtInt(current.tokensTotal)} | ${fmtSignedInt(delta.tokensTotal)} |`,
|
|
187
199
|
"",
|
|
188
200
|
];
|
|
@@ -208,10 +220,16 @@ export function compareRuns(opts) {
|
|
|
208
220
|
}
|
|
209
221
|
const baselineEnv = recordedEnvironment(opts.baselineEvents);
|
|
210
222
|
const currentEnv = recordedEnvironment(opts.events);
|
|
211
|
-
const
|
|
223
|
+
const sessions = [...recordedEnvironments(opts.baselineEvents), ...recordedEnvironments(opts.events)];
|
|
224
|
+
const comparability = sessions.some((s) => !s.environment)
|
|
225
|
+
? { comparable: false, reason: "unbound" }
|
|
226
|
+
: !baselineEnv || !currentEnv
|
|
227
|
+
? { comparable: false, reason: "mismatch", recorded: baselineEnv?.configHash ?? "mixed sessions", mixedSessions: true }
|
|
228
|
+
: environmentComparable(baselineEnv, currentEnv);
|
|
212
229
|
const baseline = runMetrics(opts.baselineEvents, opts.baselineRows ?? [], opts.cost ?? {});
|
|
213
230
|
const current = runMetrics(opts.events, opts.rows ?? [], opts.cost ?? {});
|
|
214
231
|
const delta = {
|
|
232
|
+
costPartial: current.costPartial || baseline.costPartial,
|
|
215
233
|
durationMs: current.durationMs !== undefined && baseline.durationMs !== undefined
|
|
216
234
|
? current.durationMs - baseline.durationMs
|
|
217
235
|
: undefined,
|
|
@@ -37,15 +37,16 @@ export function formatRateBasis(row) {
|
|
|
37
37
|
export function formatChannelMoney(row) {
|
|
38
38
|
const prices = [];
|
|
39
39
|
const bases = [];
|
|
40
|
+
const floor = row.partialMetering ? "≥ " : "";
|
|
40
41
|
if (row.apiUsd !== undefined)
|
|
41
|
-
prices.push(`price: $${row.apiUsd.toFixed(6)}`);
|
|
42
|
+
prices.push(`price: ${floor}$${row.apiUsd.toFixed(6)}`);
|
|
42
43
|
if (row.amortizedUsd !== undefined && row.subPlan !== undefined) {
|
|
43
44
|
const [low, high] = row.amortizedUsd;
|
|
44
|
-
prices.push(`price: $${low.toFixed(6)}–$${high.toFixed(6)} amortized`);
|
|
45
|
+
prices.push(`price: ${floor}$${low.toFixed(6)}–$${high.toFixed(6)} amortized`);
|
|
45
46
|
bases.push(`${n(row.attempts)} windows × $${row.subPlan.planMonthly}/month ÷ ${row.subPlan.windowsPerMonthHigh}–${row.subPlan.windowsPerMonthLow} windows/month`);
|
|
46
47
|
}
|
|
47
48
|
if (row.counterfactualUsd !== undefined)
|
|
48
|
-
prices.push(`API-equivalent: $${row.counterfactualUsd.toFixed(6)}`);
|
|
49
|
+
prices.push(`API-equivalent: ${floor}$${row.counterfactualUsd.toFixed(6)}`);
|
|
49
50
|
const basis = formatRateBasis(row);
|
|
50
51
|
if (basis)
|
|
51
52
|
bases.push(basis);
|
package/dist/run/daemon.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type ModeResolution, type RoutingMode, type TickmarkrConfig } from "../
|
|
|
3
3
|
import { type DriverChoice } from "../drivers/index.js";
|
|
4
4
|
import { type ExecutorDriver, type Slot } from "../drivers/types.js";
|
|
5
5
|
import { type Baseline, type BaselineProvenance, type GateEvidenceOptions } from "../gates/baseline.js";
|
|
6
|
+
import { environmentFingerprint } from "../gates/cache.js";
|
|
6
7
|
import type { GateResult } from "../gates/types.js";
|
|
7
8
|
import { type RunGraph, type Task } from "../graph/schema.js";
|
|
8
9
|
import { type PreserveProducer, type RunCapacity } from "./git.js";
|
|
@@ -226,6 +227,7 @@ export declare function resetHarvestSilentMsForTests(): void;
|
|
|
226
227
|
export declare const HARVESTED_RESULT_SUMMARY = "harvested: the worktree carries committed work; the worker emitted no TICKMARKR_RESULT trailer";
|
|
227
228
|
/** T4 (OBS-266): identity of the command SET a tip verify ran — a changed command is a different verify. */
|
|
228
229
|
export declare function commandsHash(commands: Record<string, string>): string;
|
|
230
|
+
type TipEnvironment = ReturnType<typeof environmentFingerprint>;
|
|
229
231
|
export interface TipProof {
|
|
230
232
|
kind: "fresh" | "reused" | "failed" | "incomplete";
|
|
231
233
|
/** Each completed gate keeps its own execution provenance, even in a mixed cycle. */
|
|
@@ -261,6 +263,7 @@ export interface TipClose {
|
|
|
261
263
|
expected?: string;
|
|
262
264
|
identityFailed: boolean;
|
|
263
265
|
deferred: DeferredVerdict[];
|
|
266
|
+
tipEnvironment?: TipEnvironment;
|
|
264
267
|
}
|
|
265
268
|
interface TipVerifyOptions {
|
|
266
269
|
lastMergedTask?: string;
|
|
@@ -293,6 +296,7 @@ export declare function appendIdentityFailure(journal: Journal, f: {
|
|
|
293
296
|
state?: IntegrationRefState;
|
|
294
297
|
cmdHash: string;
|
|
295
298
|
capacity: RunCapacity;
|
|
299
|
+
tipEnvironment?: TipEnvironment;
|
|
296
300
|
infra?: boolean;
|
|
297
301
|
}): void;
|
|
298
302
|
/** O: I(expected) — or, with no expected value, the ref must be regular and its commit becomes the expected. */
|
|
@@ -309,9 +313,9 @@ export declare function openingIdentity(identity: TipIdentity, journal: Journal)
|
|
|
309
313
|
/**
|
|
310
314
|
* OBS-34's strict tip verify, but it stops re-paying for an unmoved tip (~334m corpus-wide; 69.5m in
|
|
311
315
|
* one park-heavy run whose 18 resume cycles merged nothing new). The verify journals the SHA it
|
|
312
|
-
* verified and the hash of the command set, so a later run-end can recognize the same verified state
|
|
313
|
-
*
|
|
314
|
-
* `tip-verify-cached`
|
|
316
|
+
* verified and the hash of the command set, so a later run-end can recognize the same verified state.
|
|
317
|
+
* Queue row 24 (D-2134): the last green also binds every row's stamped runner and environment identity
|
|
318
|
+
* and capacity before `tip-verify-cached` can skip with no checkout. ANY doubt — moved tip, changed commands, a gate
|
|
315
319
|
* missing from that cycle's green set, a failure recorded in it, a cycle older than the last one —
|
|
316
320
|
* runs the full verify. 2.8.1 T1 (ANL-023 §7.1): with `identity` (the daemon always passes it) run end
|
|
317
321
|
* verifies a fresh detached checkout of the expected commit under a validated HEAD reflog, never the
|
package/dist/run/daemon.js
CHANGED
|
@@ -23,7 +23,7 @@ import { herdrSealShellPrefix, MAX_BUF, SubprocessDriver } from "../drivers/subp
|
|
|
23
23
|
import { formatOwnedName } from "../drivers/types.js";
|
|
24
24
|
import { captureBaseline, detectGateCommands, detectVacuousOracles } from "../gates/baseline.js";
|
|
25
25
|
import { reobserveTestFiles, runGates } from "../gates/run-gates.js";
|
|
26
|
-
import { getVerdictStore, isInfraResult, resolveStateDir } from "../gates/cache.js";
|
|
26
|
+
import { environmentFingerprint, getVerdictStore, isInfraResult, resolveStateDir } from "../gates/cache.js";
|
|
27
27
|
import { RUNNER_INFRA_DIAGNOSTIC_RE, timeoutShaped } from "../gates/timeout-shaped.js";
|
|
28
28
|
import { filesGlob } from "../graph/files-glob.js";
|
|
29
29
|
import { addEvidence, attributeBlocked, batteryPriority, blockedTasks, getTask, graphDefinitionHash, loadGraph, pendingTasks, readyTasks, saveGraph, setStatus, taskContentDigest, tickmarkrDir } from "../graph/graph.js";
|
|
@@ -34,7 +34,7 @@ import { executionSignal, remainingExecutionMs, withExecutionBudget, withoutExec
|
|
|
34
34
|
import { repairSelectionDecision, repairSelectionEnabled } from "./repair-selection.js";
|
|
35
35
|
import { failureDisposition, reserveInfrastructureRetry } from "./recovery.js";
|
|
36
36
|
import { runEnvironment } from "./environment.js";
|
|
37
|
-
import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, declaredBaseContainment, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, reflogWitness, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WorktreeAdminRaceError, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
|
|
37
|
+
import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, declaredBaseContainment, dependencyLinkRefusal, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, reflogWitness, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WorktreeAdminRaceError, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
|
|
38
38
|
import { MASK } from "./redact.js";
|
|
39
39
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
40
40
|
import { classifyRepairDisposition, namedPaths, resolveScopeHints } from "./repair-disposition.js";
|
|
@@ -883,6 +883,32 @@ export const HARVESTED_RESULT_SUMMARY = "harvested: the worktree carries committ
|
|
|
883
883
|
export function commandsHash(commands) {
|
|
884
884
|
return createHash("sha256").update(JSON.stringify(Object.entries(commands).sort())).digest("hex").slice(0, 12);
|
|
885
885
|
}
|
|
886
|
+
// Queue row 24 (D-2134): refuse links before npm probes; an unreadable identity stays absent for the battery's infra row.
|
|
887
|
+
function measureTipEnvironment(worktree, capacity) {
|
|
888
|
+
if (dependencyLinkRefusal(worktree))
|
|
889
|
+
return undefined;
|
|
890
|
+
try {
|
|
891
|
+
return environmentFingerprint({ worktree, scope: "tip", capacity });
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
return undefined;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// Queue row 24 (D-2134): validate the parts too; a matching fingerprint alone cannot rescue a malformed stamp.
|
|
898
|
+
function sameTipEnvironment(recorded, current) {
|
|
899
|
+
if (!current || !recorded || typeof recorded !== "object" || Array.isArray(recorded))
|
|
900
|
+
return false;
|
|
901
|
+
const { fingerprint, parts } = recorded;
|
|
902
|
+
if (fingerprint !== current.fingerprint || !parts || typeof parts !== "object" || Array.isArray(parts))
|
|
903
|
+
return false;
|
|
904
|
+
const p = parts;
|
|
905
|
+
const verification = p.verification;
|
|
906
|
+
return p.nodeRuntime === current.parts.nodeRuntime && p.runner === current.parts.runner
|
|
907
|
+
&& p.lockfile === current.parts.lockfile && p.selectedSet === undefined
|
|
908
|
+
&& p.capacity !== undefined && sameCapacity(p.capacity, current.parts.capacity)
|
|
909
|
+
&& !!verification && (verification.source === "explicit" || verification.source === "npm-config")
|
|
910
|
+
&& sameVerification(verification, current.parts.verification);
|
|
911
|
+
}
|
|
886
912
|
/**
|
|
887
913
|
* T4 (OBS-266): the journal's LAST verification cycle — the (tip, cmdHash) pair the most recent run
|
|
888
914
|
* of the verify commands spoke for, the gates it got a pass from, and whether anything failed in it.
|
|
@@ -904,7 +930,7 @@ function lastVerifyCycle(events) {
|
|
|
904
930
|
if (e.event === "tip-verify-start") {
|
|
905
931
|
const { tip, cmdHash } = e.data;
|
|
906
932
|
cur = typeof tip === "string" && typeof cmdHash === "string"
|
|
907
|
-
? { tip, cmdHash, gates: new Set(), failed: false, forgiven: false, capacities: [e.data.capacity], evidenceByGate: new Map() }
|
|
933
|
+
? { tip, cmdHash, gates: new Set(), failed: false, forgiven: false, capacities: [e.data.capacity], tipEnvironments: [e.data.tipEnvironment], evidenceByGate: new Map() }
|
|
908
934
|
: undefined;
|
|
909
935
|
afterRunEnd = false;
|
|
910
936
|
continue;
|
|
@@ -927,7 +953,7 @@ function lastVerifyCycle(events) {
|
|
|
927
953
|
continue;
|
|
928
954
|
}
|
|
929
955
|
if (!cur || afterRunEnd || cur.tip !== tip || cur.cmdHash !== cmdHash) {
|
|
930
|
-
cur = { tip, cmdHash, gates: new Set(), failed: false, forgiven: false, capacities: [], evidenceByGate: new Map() };
|
|
956
|
+
cur = { tip, cmdHash, gates: new Set(), failed: false, forgiven: false, capacities: [], tipEnvironments: [], evidenceByGate: new Map() };
|
|
931
957
|
}
|
|
932
958
|
afterRunEnd = false;
|
|
933
959
|
// T7: EVERY verdict row's own capacity, not just the start row's. The start row is a statement of
|
|
@@ -935,6 +961,7 @@ function lastVerifyCycle(events) {
|
|
|
935
961
|
// these rows, so a row whose capacity differs from the session's — or which is malformed — has to
|
|
936
962
|
// be able to sink the cycle by itself.
|
|
937
963
|
cur.capacities.push(e.data.capacity);
|
|
964
|
+
cur.tipEnvironments.push(e.data.tipEnvironment);
|
|
938
965
|
if (e.event === "tip-verify-failed")
|
|
939
966
|
cur.failed = true;
|
|
940
967
|
else {
|
|
@@ -1204,13 +1231,13 @@ export function identityDetails(leg, state, branch, runId, expected) {
|
|
|
1204
1231
|
*/
|
|
1205
1232
|
export function appendIdentityFailure(journal, f) {
|
|
1206
1233
|
if (!f.started)
|
|
1207
|
-
journal.append("tip-verify-start", undefined, { tip: f.expected, cmdHash: f.cmdHash, capacity: f.capacity, gates: [], cached: false });
|
|
1234
|
+
journal.append("tip-verify-start", undefined, { tip: f.expected, cmdHash: f.cmdHash, capacity: f.capacity, tipEnvironment: f.tipEnvironment, gates: [], cached: false });
|
|
1208
1235
|
const found = f.state?.kind === "regular" || f.state?.kind === "symbolic" ? f.state.commit : undefined;
|
|
1209
1236
|
journal.append("tip-verify-failed", undefined, {
|
|
1210
1237
|
gate: "integration", cmd: "", exitCode: 1, fingerprints: [], details: f.details, ...(f.infra ? { infra: true } : {}),
|
|
1211
1238
|
integration: { branch: f.branch, expected: f.expected ?? null, found: found ?? null, leg: f.leg,
|
|
1212
1239
|
...(f.state?.kind === "symbolic" ? { target: f.state.target } : {}) },
|
|
1213
|
-
tip: f.expected, cmdHash: f.cmdHash, capacity: f.capacity,
|
|
1240
|
+
tip: f.expected, cmdHash: f.cmdHash, capacity: f.capacity, tipEnvironment: f.tipEnvironment,
|
|
1214
1241
|
});
|
|
1215
1242
|
}
|
|
1216
1243
|
/** O: I(expected) — or, with no expected value, the ref must be regular and its commit becomes the expected. */
|
|
@@ -1271,16 +1298,12 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1271
1298
|
opts.signal?.throwIfAborted();
|
|
1272
1299
|
const cmdHash = commandsHash(commands);
|
|
1273
1300
|
const { identity } = opts;
|
|
1274
|
-
//
|
|
1275
|
-
// green must match, beside the tip and the command set. A cached verdict is the one place a green
|
|
1276
|
-
// crosses a session boundary with nothing re-run, and a session resumed at a different capacity
|
|
1277
|
-
// divides the machine by a different number: that green was established in another world, so it is
|
|
1278
|
-
// not carried forward and the commands run again. A pre-T7 cycle records no capacity and keeps
|
|
1279
|
-
// exactly the behaviour it has today.
|
|
1301
|
+
// Queue row 24 (D-2134): measure once against the repository whose installed packages the verifier receives.
|
|
1280
1302
|
const capacity = resolvedCapacity();
|
|
1303
|
+
const tipEnvironment = measureTipEnvironment(identity?.repo ?? intWt, capacity);
|
|
1281
1304
|
const identityFailure = (f) => {
|
|
1282
|
-
appendIdentityFailure(journal, { ...f, branch: identity.branch, cmdHash, capacity });
|
|
1283
|
-
return { failed: true, identityFailed: true, deferred: [] };
|
|
1305
|
+
appendIdentityFailure(journal, { ...f, branch: identity.branch, cmdHash, capacity, tipEnvironment });
|
|
1306
|
+
return { failed: true, identityFailed: true, deferred: [], tipEnvironment };
|
|
1284
1307
|
};
|
|
1285
1308
|
let tip;
|
|
1286
1309
|
let clean = true; // with `identity` the battery runs in a fresh checkout and K checks tracked content
|
|
@@ -1299,25 +1322,30 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1299
1322
|
const gates = Object.keys(commands).filter((g) => g !== "tipTest");
|
|
1300
1323
|
if (!gates.includes("test") && commands.tipTest)
|
|
1301
1324
|
gates.push("test");
|
|
1325
|
+
// Queue row 24 (D-2134 R2/Q6/Q7): carry requires tip, commands, every gate and capacity AND the stamped runner
|
|
1326
|
+
// and environment identity with a measured lifecycle on every row. An unstamped cycle re-verifies once.
|
|
1327
|
+
// Package content drift under equal lockfile and links is the declared residual, as in per-gate reuse.
|
|
1302
1328
|
const cached = last !== undefined && !last.failed && !last.forgiven && last.tip === tip && last.cmdHash === cmdHash
|
|
1303
1329
|
&& gates.every((g) => last.gates.has(g))
|
|
1304
|
-
&& last.capacities.every((recorded) => sameCapacity(recorded, capacity))
|
|
1330
|
+
&& last.capacities.every((recorded) => sameCapacity(recorded, capacity))
|
|
1331
|
+
&& last.tipEnvironments.every((recorded) => sameTipEnvironment(recorded, tipEnvironment))
|
|
1332
|
+
&& !dependencyLinkRefusal(identity?.repo ?? intWt);
|
|
1305
1333
|
// A pair can be verified red and then green without either SHA or command hash changing (for
|
|
1306
1334
|
// example, an external service or ignored fixture recovers). Delimit attempts explicitly so that
|
|
1307
1335
|
// the earlier red cannot remain latched into the later complete green cycle.
|
|
1308
|
-
journal.append("tip-verify-start", undefined, { tip, cmdHash, capacity, gates, cached: clean && cached });
|
|
1336
|
+
journal.append("tip-verify-start", undefined, { tip, cmdHash, capacity, tipEnvironment, gates, cached: clean && cached });
|
|
1309
1337
|
opts.signal?.throwIfAborted();
|
|
1310
1338
|
if (clean && cached) {
|
|
1311
|
-
journal.append("tip-verify-cached", undefined, { tip, cmdHash, capacity, gates });
|
|
1339
|
+
journal.append("tip-verify-cached", undefined, { tip, cmdHash, capacity, tipEnvironment, gates });
|
|
1312
1340
|
// The skip must not read as a red. Every surface derives the tip's verdict from this cycle's
|
|
1313
1341
|
// `tip-verify` events (cockpit derive.ts tipVerificationPassed: a run-end claiming "passed" with
|
|
1314
1342
|
// ZERO events is fail-closed to FALSE), so a carried-forward green still journals its per-gate
|
|
1315
1343
|
// pass — `cached: true` keeps it honest about not having re-run the command.
|
|
1316
1344
|
for (const gate of gates) {
|
|
1317
1345
|
const cmd = gate === "test" && commands.tipTest ? commands.tipTest : commands[gate];
|
|
1318
|
-
journal.append("tip-verify", undefined, { ...reusedTipEvidence(last.evidenceByGate.get(gate) ?? {}), gate, cmd, pass: true, exitCode: 0, cached: true, tip, cmdHash, capacity });
|
|
1346
|
+
journal.append("tip-verify", undefined, { ...reusedTipEvidence(last.evidenceByGate.get(gate) ?? {}), gate, cmd, pass: true, exitCode: 0, cached: true, tip, cmdHash, capacity, tipEnvironment });
|
|
1319
1347
|
}
|
|
1320
|
-
return identity ? { failed: false, expected: tip, identityFailed: false, deferred: [] } : false;
|
|
1348
|
+
return identity ? { failed: false, expected: tip, identityFailed: false, deferred: [], tipEnvironment } : false;
|
|
1321
1349
|
}
|
|
1322
1350
|
const battery = async (dir) => {
|
|
1323
1351
|
let tipFailed = false;
|
|
@@ -1340,7 +1368,7 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1340
1368
|
};
|
|
1341
1369
|
if (r.pass) {
|
|
1342
1370
|
// Q121s: a forgiven pass journals its fingerprints — honest about what was carried, never a silent green.
|
|
1343
|
-
journal.append("tip-verify", undefined, { ...evidence, gate: r.gate, cmd: r.cmd, pass: true, exitCode: r.exitCode, details: r.details, ...(r.reused ? { cached: true } : {}), ...(r.forgiven ? { forgiven: true, fingerprints: r.fingerprints, ...(r.baselineProvenance ? { baselineProvenance: r.baselineProvenance } : {}) } : {}), tip, cmdHash, capacity: measuredCapacity });
|
|
1371
|
+
journal.append("tip-verify", undefined, { ...evidence, gate: r.gate, cmd: r.cmd, pass: true, exitCode: r.exitCode, details: r.details, ...(r.reused ? { cached: true } : {}), ...(r.forgiven ? { forgiven: true, fingerprints: r.fingerprints, ...(r.baselineProvenance ? { baselineProvenance: r.baselineProvenance } : {}) } : {}), tip, cmdHash, capacity: measuredCapacity, tipEnvironment });
|
|
1344
1372
|
}
|
|
1345
1373
|
else {
|
|
1346
1374
|
journal.append("tip-verify-failed", undefined, {
|
|
@@ -1355,6 +1383,7 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1355
1383
|
tip,
|
|
1356
1384
|
cmdHash,
|
|
1357
1385
|
capacity: measuredCapacity,
|
|
1386
|
+
tipEnvironment,
|
|
1358
1387
|
});
|
|
1359
1388
|
tipFailed = true;
|
|
1360
1389
|
}
|
|
@@ -1372,7 +1401,7 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
1372
1401
|
const changed = await checkoutChange(opened.dir, tip, opened.h0);
|
|
1373
1402
|
if (changed)
|
|
1374
1403
|
return identityFailure({ started: true, expected: tip, leg: "checkout", details: `verification checkout changed during the battery: ${changed}` });
|
|
1375
|
-
return { failed: tipFailed, expected: tip, identityFailed: false, deferred };
|
|
1404
|
+
return { failed: tipFailed, expected: tip, identityFailed: false, deferred, tipEnvironment };
|
|
1376
1405
|
}
|
|
1377
1406
|
finally {
|
|
1378
1407
|
if (checkout)
|
|
@@ -3380,7 +3409,11 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3380
3409
|
const worker = journal.read().reverse().find((e) => e.taskId === t.id && e.event === "worker-result");
|
|
3381
3410
|
const disposition = classifyRepairDisposition({
|
|
3382
3411
|
// OBS-1072 add.2: hints are lexed from the readable excerpt, never from terminal control bytes.
|
|
3383
|
-
results: results.map((g) => ({ ...g, details: readableExcerpt(g.details)
|
|
3412
|
+
results: results.map((g) => ({ ...g, details: readableExcerpt(g.details),
|
|
3413
|
+
// Queue row 235 (D-2134): oracle output has the same readable boundary as gate details.
|
|
3414
|
+
...(Array.isArray(g.meta?.oracles) ? { meta: { ...g.meta, oracles: g.meta.oracles.map((oracle) => oracle && typeof oracle === "object" && typeof oracle.output === "string"
|
|
3415
|
+
? { ...oracle, output: readableExcerpt(oracle.output) } : oracle) } } : {}),
|
|
3416
|
+
})), files: t.files, inventory: changed,
|
|
3384
3417
|
refusalSummary: worker?.data.ok === false && typeof worker.data.summary === "string" ? worker.data.summary : undefined,
|
|
3385
3418
|
});
|
|
3386
3419
|
for (const diagnostic of disposition.diagnostics) {
|
|
@@ -3895,8 +3928,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3895
3928
|
...(typeof g.meta.producerAttempt === "number" ? { producerAttempt: g.meta.producerAttempt } : {}),
|
|
3896
3929
|
} : {}),
|
|
3897
3930
|
...((cfg.executionPolicy || repairSelection) && !g.pass ? { disposition: failureDisposition(g) } : {}),
|
|
3898
|
-
...Object.fromEntries(["runnerInfraRerun", "hostStarvedRerun", "recoveryBlocked", "failingFiles", "selectionDecision", "failureEvidence",
|
|
3899
|
-
"forgivenFingerprints", "freshFingerprints", "baselineProvenance", "judgment", "adjudication", "judgeDisagreement", "interruptions"]
|
|
3931
|
+
...Object.fromEntries(["runnerInfraRerun", "judgeIndependent", "hostStarvedRerun", "recoveryBlocked", "failingFiles", "selectionDecision", "failureEvidence",
|
|
3932
|
+
"forgivenFingerprints", "freshFingerprints", "baselineProvenance", "oracles", "judgment", "adjudication", "judgeDisagreement", "interruptions"]
|
|
3900
3933
|
.filter((key) => g.meta?.[key] !== undefined).map((key) => [key, g.meta[key]])),
|
|
3901
3934
|
// OBS-540: preserve terminal-vs-retryable infra exactly. normalizeGateOutcome deliberately
|
|
3902
3935
|
// defaults a legacy infra row to retryable, so dropping an explicit false here reverses the
|
|
@@ -7478,8 +7511,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
7478
7511
|
summary.tipProof = { kind: "incomplete" };
|
|
7479
7512
|
}
|
|
7480
7513
|
else {
|
|
7481
|
-
|
|
7482
|
-
|
|
7514
|
+
const capacity = resolvedCapacity();
|
|
7515
|
+
const tipEnvironment = measureTipEnvironment(repoRoot, capacity);
|
|
7516
|
+
appendIdentityFailure(journal, { started: false, branch, ...opened, cmdHash: commandsHash(commands), capacity, tipEnvironment });
|
|
7517
|
+
tipClose = { failed: true, identityFailed: true, deferred: [], tipEnvironment };
|
|
7483
7518
|
summary.tipVerify = "failed";
|
|
7484
7519
|
summary.tipProof = runEndTipProof(journal.read());
|
|
7485
7520
|
}
|
|
@@ -7514,8 +7549,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
7514
7549
|
if (!tipClose.identityFailed && tipClose.expected !== undefined) {
|
|
7515
7550
|
const closing = await checkIntegrationIdentity(repoRoot, branch, tipClose.expected);
|
|
7516
7551
|
if (!closing.ok) {
|
|
7552
|
+
const capacity = resolvedCapacity();
|
|
7553
|
+
const tipEnvironment = tipStarted ? tipClose.tipEnvironment : measureTipEnvironment(repoRoot, capacity);
|
|
7517
7554
|
appendIdentityFailure(journal, { started: tipStarted, branch, expected: tipClose.expected, leg: closing.leg, state: closing.state,
|
|
7518
|
-
details: identityDetails(closing.leg, closing.state, branch, runId, tipClose.expected), cmdHash: commandsHash(commands), capacity
|
|
7555
|
+
details: identityDetails(closing.leg, closing.state, branch, runId, tipClose.expected), cmdHash: commandsHash(commands), capacity, tipEnvironment });
|
|
7519
7556
|
summary.tipVerify = "failed";
|
|
7520
7557
|
summary.tipProof = runEndTipProof(journal.read());
|
|
7521
7558
|
}
|
package/dist/run/journal.js
CHANGED
|
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmdirSync } from "node:fs";
|
|
5
|
-
import { join } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { channelKey, shq, TokenUsageSchema } from "../adapters/types.js";
|
|
8
8
|
import { graphDefinitionHash, stateDirName, taskContentDigest, taskDefinitionFingerprint, tickmarkrDir } from "../graph/graph.js";
|
|
@@ -2744,7 +2744,16 @@ function dischargeProblem(row, o, events, cwd, memo) {
|
|
|
2744
2744
|
return "discharge range does not bind the obligation";
|
|
2745
2745
|
let artifact;
|
|
2746
2746
|
try {
|
|
2747
|
-
|
|
2747
|
+
let artifactPath = String(d.artifactPath);
|
|
2748
|
+
if (!isAbsolute(artifactPath)) {
|
|
2749
|
+
// Queue row 31 / D-2134: resolve within the obligation's own run, with common-root fallback
|
|
2750
|
+
// only when that run is absent locally; legacy absolute paths keep their original meaning.
|
|
2751
|
+
const id = parseRunId(o.runId);
|
|
2752
|
+
const localRun = join(runsDir(cwd), id);
|
|
2753
|
+
const runDir = existsSync(localRun) ? localRun : join(runsDir(dirname(trustedCommonDir(cwd))), id);
|
|
2754
|
+
artifactPath = join(runDir, artifactPath);
|
|
2755
|
+
}
|
|
2756
|
+
const bytes = readFileSync(artifactPath);
|
|
2748
2757
|
if (createHash("sha256").update(bytes).digest("hex") !== d.artifactSha256)
|
|
2749
2758
|
return "artifact hash mismatch";
|
|
2750
2759
|
artifact = JSON.parse(bytes.toString("utf8")) ?? {};
|
|
@@ -4,7 +4,7 @@ import type { GateResult } from "../gates/types.js";
|
|
|
4
4
|
export type RepairDispositionKind = "fund-repair" | "scope-request" | "authoring" | "none";
|
|
5
5
|
export interface RepairDisposition {
|
|
6
6
|
kind: RepairDispositionKind;
|
|
7
|
-
/**
|
|
7
|
+
/** Queue row 235 (D-2134): validated inventory paths or explicitly declared oracle landings. */
|
|
8
8
|
paths: string[];
|
|
9
9
|
reason: string;
|
|
10
10
|
/** which evidence decided: a gate name, "worker" for a refusal, absent for the ordinary disposition */
|