open-multi-agent-kit 0.98.3 → 0.98.4
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/CHANGELOG.md +32 -0
- package/README.md +11 -2
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +14 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +3 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/mcp-attach.d.ts +3 -3
- package/dist/cli/mcp-attach.d.ts.map +1 -1
- package/dist/cli/mcp-attach.js +5 -4
- package/dist/cli/mcp-attach.js.map +1 -1
- package/dist/cli/model-contract.d.ts +5 -0
- package/dist/cli/model-contract.d.ts.map +1 -0
- package/dist/cli/model-contract.js +45 -0
- package/dist/cli/model-contract.js.map +1 -0
- package/dist/commands/adaptorch-doctor-cli.d.ts.map +1 -1
- package/dist/commands/adaptorch-doctor-cli.js +32 -10
- package/dist/commands/adaptorch-doctor-cli.js.map +1 -1
- package/dist/commands/init-cli.d.ts +12 -0
- package/dist/commands/init-cli.d.ts.map +1 -0
- package/dist/commands/init-cli.js +76 -0
- package/dist/commands/init-cli.js.map +1 -0
- package/dist/commands/provider-sync-cli.d.ts +27 -0
- package/dist/commands/provider-sync-cli.d.ts.map +1 -0
- package/dist/commands/provider-sync-cli.js +177 -0
- package/dist/commands/provider-sync-cli.js.map +1 -0
- package/dist/commands/run-command.d.ts +8 -0
- package/dist/commands/run-command.d.ts.map +1 -0
- package/dist/commands/run-command.js +26 -0
- package/dist/commands/run-command.js.map +1 -0
- package/dist/core/active-skill-state.d.ts +7 -0
- package/dist/core/active-skill-state.d.ts.map +1 -0
- package/dist/core/active-skill-state.js +25 -0
- package/dist/core/active-skill-state.js.map +1 -0
- package/dist/core/agent-session-services.d.ts +2 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +2 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +0 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +30 -85
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/codex-chatgpt-web-bridge.d.ts +99 -0
- package/dist/core/codex-chatgpt-web-bridge.d.ts.map +1 -0
- package/dist/core/codex-chatgpt-web-bridge.js +192 -0
- package/dist/core/codex-chatgpt-web-bridge.js.map +1 -0
- package/dist/core/codex-chatgpt-web-sync.d.ts +50 -0
- package/dist/core/codex-chatgpt-web-sync.d.ts.map +1 -0
- package/dist/core/codex-chatgpt-web-sync.js +119 -0
- package/dist/core/codex-chatgpt-web-sync.js.map +1 -0
- package/dist/core/compaction/compaction-headroom.d.ts +30 -0
- package/dist/core/compaction/compaction-headroom.d.ts.map +1 -0
- package/dist/core/compaction/compaction-headroom.js +48 -0
- package/dist/core/compaction/compaction-headroom.js.map +1 -0
- package/dist/core/compaction/compaction.d.ts +2 -29
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +19 -47
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/overflow-retry-guard.d.ts +20 -0
- package/dist/core/compaction/overflow-retry-guard.d.ts.map +1 -0
- package/dist/core/compaction/overflow-retry-guard.js +38 -0
- package/dist/core/compaction/overflow-retry-guard.js.map +1 -0
- package/dist/core/context-budget-headroom-types.d.ts.map +1 -1
- package/dist/core/context-budget-headroom-types.js +1 -1
- package/dist/core/context-budget-headroom-types.js.map +1 -1
- package/dist/core/context-budget-token-counter-types.d.ts +27 -0
- package/dist/core/context-budget-token-counter-types.d.ts.map +1 -0
- package/dist/core/context-budget-token-counter-types.js +2 -0
- package/dist/core/context-budget-token-counter-types.js.map +1 -0
- package/dist/core/context-budget-token-counter.d.ts +2 -26
- package/dist/core/context-budget-token-counter.d.ts.map +1 -1
- package/dist/core/context-budget-token-counter.js +2 -0
- package/dist/core/context-budget-token-counter.js.map +1 -1
- package/dist/core/context-budget-v2-scoring.d.ts.map +1 -1
- package/dist/core/context-budget-v2-scoring.js +14 -10
- package/dist/core/context-budget-v2-scoring.js.map +1 -1
- package/dist/core/domain-router.d.ts.map +1 -1
- package/dist/core/domain-router.js +1 -1
- package/dist/core/domain-router.js.map +1 -1
- package/dist/core/grok-harness-dispatch.d.ts +4 -0
- package/dist/core/grok-harness-dispatch.d.ts.map +1 -1
- package/dist/core/grok-harness-dispatch.js +20 -3
- package/dist/core/grok-harness-dispatch.js.map +1 -1
- package/dist/core/grok-harness.d.ts +15 -9
- package/dist/core/grok-harness.d.ts.map +1 -1
- package/dist/core/grok-harness.js +34 -8
- package/dist/core/grok-harness.js.map +1 -1
- package/dist/core/mcp/tools.d.ts +2 -2
- package/dist/core/mcp/tools.d.ts.map +1 -1
- package/dist/core/mcp/tools.js +29 -3
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/model-registry-schema.d.ts +1081 -0
- package/dist/core/model-registry-schema.d.ts.map +1 -0
- package/dist/core/model-registry-schema.js +148 -0
- package/dist/core/model-registry-schema.js.map +1 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +1 -146
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +2 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +2 -1
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/prompt-preset.d.ts +1 -1
- package/dist/core/prompt-preset.d.ts.map +1 -1
- package/dist/core/prompt-preset.js +16 -1
- package/dist/core/prompt-preset.js.map +1 -1
- package/dist/core/provider-display-names.d.ts.map +1 -1
- package/dist/core/provider-display-names.js +1 -0
- package/dist/core/provider-display-names.js.map +1 -1
- package/dist/core/provider-resilience.d.ts +8 -0
- package/dist/core/provider-resilience.d.ts.map +1 -1
- package/dist/core/provider-resilience.js +20 -3
- package/dist/core/provider-resilience.js.map +1 -1
- package/dist/core/provider-usage-types.d.ts +42 -0
- package/dist/core/provider-usage-types.d.ts.map +1 -0
- package/dist/core/provider-usage-types.js +2 -0
- package/dist/core/provider-usage-types.js.map +1 -0
- package/dist/core/provider-usage.d.ts +2 -31
- package/dist/core/provider-usage.d.ts.map +1 -1
- package/dist/core/provider-usage.js +4 -0
- package/dist/core/provider-usage.js.map +1 -1
- package/dist/core/redaction.d.ts.map +1 -1
- package/dist/core/redaction.js +18 -2
- package/dist/core/redaction.js.map +1 -1
- package/dist/core/sandbox/policy-merge.d.ts +6 -0
- package/dist/core/sandbox/policy-merge.d.ts.map +1 -0
- package/dist/core/sandbox/policy-merge.js +74 -0
- package/dist/core/sandbox/policy-merge.js.map +1 -0
- package/dist/core/sandbox/policy-paths.d.ts +4 -0
- package/dist/core/sandbox/policy-paths.d.ts.map +1 -0
- package/dist/core/sandbox/policy-paths.js +21 -0
- package/dist/core/sandbox/policy-paths.js.map +1 -0
- package/dist/core/sandbox/policy-types.d.ts +75 -0
- package/dist/core/sandbox/policy-types.d.ts.map +1 -0
- package/dist/core/sandbox/policy-types.js +2 -0
- package/dist/core/sandbox/policy-types.js.map +1 -0
- package/dist/core/sandbox/policy.d.ts +3 -77
- package/dist/core/sandbox/policy.d.ts.map +1 -1
- package/dist/core/sandbox/policy.js +3 -98
- package/dist/core/sandbox/policy.js.map +1 -1
- package/dist/core/sdk-provider-stream.d.ts +14 -0
- package/dist/core/sdk-provider-stream.d.ts.map +1 -0
- package/dist/core/sdk-provider-stream.js +38 -0
- package/dist/core/sdk-provider-stream.js.map +1 -0
- package/dist/core/sdk.d.ts +5 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +17 -33
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-failure-cause.d.ts.map +1 -1
- package/dist/core/session-failure-cause.js +10 -3
- package/dist/core/session-failure-cause.js.map +1 -1
- package/dist/core/session-run-termination.d.ts +18 -0
- package/dist/core/session-run-termination.d.ts.map +1 -0
- package/dist/core/session-run-termination.js +61 -0
- package/dist/core/session-run-termination.js.map +1 -0
- package/dist/core/session-termination.d.ts.map +1 -1
- package/dist/core/session-termination.js +1 -1
- package/dist/core/session-termination.js.map +1 -1
- package/dist/core/skill-selector.d.ts +40 -0
- package/dist/core/skill-selector.d.ts.map +1 -0
- package/dist/core/skill-selector.js +102 -0
- package/dist/core/skill-selector.js.map +1 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js +2 -4
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/turn-metrics-record.d.ts +55 -0
- package/dist/core/turn-metrics-record.d.ts.map +1 -0
- package/dist/core/turn-metrics-record.js +149 -0
- package/dist/core/turn-metrics-record.js.map +1 -0
- package/dist/core/turn-metrics.d.ts +4 -56
- package/dist/core/turn-metrics.d.ts.map +1 -1
- package/dist/core/turn-metrics.js +12 -60
- package/dist/core/turn-metrics.js.map +1 -1
- package/dist/guardrails/evidence-system.d.ts +1 -1
- package/dist/guardrails/evidence-system.d.ts.map +1 -1
- package/dist/guardrails/evidence-system.js +18 -29
- package/dist/guardrails/evidence-system.js.map +1 -1
- package/dist/guardrails/merge-gate-result.d.ts +4 -0
- package/dist/guardrails/merge-gate-result.d.ts.map +1 -0
- package/dist/guardrails/merge-gate-result.js +32 -0
- package/dist/guardrails/merge-gate-result.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +22 -22
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-login-options.d.ts +27 -0
- package/dist/modes/interactive/interactive-login-options.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-login-options.js +54 -0
- package/dist/modes/interactive/interactive-login-options.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +7 -4
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +138 -140
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/interactive-resume-command.d.ts +3 -0
- package/dist/modes/interactive/interactive-resume-command.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-resume-command.js +24 -0
- package/dist/modes/interactive/interactive-resume-command.js.map +1 -0
- package/dist/modes/interactive/interactive-tool-result.d.ts +32 -0
- package/dist/modes/interactive/interactive-tool-result.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-tool-result.js +59 -0
- package/dist/modes/interactive/interactive-tool-result.js.map +1 -0
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +22 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +10 -60
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/terminal-links.d.ts +13 -0
- package/dist/utils/terminal-links.d.ts.map +1 -0
- package/dist/utils/terminal-links.js +68 -0
- package/dist/utils/terminal-links.js.map +1 -0
- package/dist/utils/windows-clipboard-image.d.ts +16 -0
- package/dist/utils/windows-clipboard-image.d.ts.map +1 -0
- package/dist/utils/windows-clipboard-image.js +87 -0
- package/dist/utils/windows-clipboard-image.js.map +1 -0
- package/docs/adaptorch-onboarding.md +80 -0
- package/docs/containerization.md +12 -0
- package/docs/context-files.md +97 -0
- package/docs/correctness-wall.md +8 -0
- package/docs/custom-provider.md +35 -0
- package/docs/development.md +18 -3
- package/docs/environment-variables.md +2 -0
- package/docs/grok-harness.md +4 -0
- package/docs/harness-boundaries.md +154 -0
- package/docs/harness-improvements.md +90 -0
- package/docs/json.md +19 -0
- package/docs/keybindings.md +1 -1
- package/docs/mcp.md +22 -2
- package/docs/metrics.md +66 -16
- package/docs/model-catalog-refresh.md +190 -0
- package/docs/model-contract.md +168 -0
- package/docs/models.md +21 -4
- package/docs/provider-resilience.md +5 -3
- package/docs/providers.md +68 -1
- package/docs/quickstart.md +4 -0
- package/docs/release-audit-0.98.4.md +112 -0
- package/docs/review-bundle-followup.md +195 -0
- package/docs/runtime-algorithms.md +19 -0
- package/docs/sdk.md +11 -2
- package/docs/sessions.md +1 -1
- package/docs/settings.md +17 -1
- package/docs/skills.md +2 -0
- package/docs/tb21-audit.md +131 -0
- package/docs/terminal-setup.md +20 -0
- package/docs/usage.md +23 -1
- package/docs/windows.md +48 -0
- package/examples/README.md +4 -0
- package/examples/context/AGENTS.md +64 -0
- package/examples/context/CLAUDE.md +17 -0
- package/examples/context/INTERNET.md +42 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +6 -6
|
@@ -12,50 +12,15 @@
|
|
|
12
12
|
* a failed write is dropped, never surfaced as a turn error.
|
|
13
13
|
*
|
|
14
14
|
* Privacy: records counts, durations, ids, and error *classes*. It never
|
|
15
|
-
* records prompt text, tool arguments, tool output, or file contents.
|
|
16
|
-
*
|
|
15
|
+
* records prompt text, tool arguments, tool output, or file contents. New
|
|
16
|
+
* records retain bounded error classes only; raw failure text is never serialized.
|
|
17
17
|
*/
|
|
18
18
|
import fs from "node:fs";
|
|
19
19
|
import path from "node:path";
|
|
20
|
-
import {
|
|
21
|
-
export
|
|
20
|
+
import { buildTurnMetricRecord, parseTurnMetricRecord, } from "./turn-metrics-record.js";
|
|
21
|
+
export * from "./turn-metrics-record.js";
|
|
22
22
|
/** Rotate once the active file passes this size. */
|
|
23
23
|
export const DEFAULT_MAX_METRICS_BYTES = 8 * 1024 * 1024;
|
|
24
|
-
/** Cap on a retained error string. Enough to classify, too short to carry a payload. */
|
|
25
|
-
export const MAX_ERROR_CHARS = 200;
|
|
26
|
-
function truncateError(value) {
|
|
27
|
-
if (value === undefined)
|
|
28
|
-
return undefined;
|
|
29
|
-
const collapsed = value.replace(/\s+/gu, " ").trim();
|
|
30
|
-
if (collapsed.length === 0)
|
|
31
|
-
return undefined;
|
|
32
|
-
return collapsed.length > MAX_ERROR_CHARS ? `${collapsed.slice(0, MAX_ERROR_CHARS)}\u2026` : collapsed;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Build the record written for one turn. Pure: no clock, no filesystem, so the
|
|
36
|
-
* derived fields can be asserted directly.
|
|
37
|
-
*/
|
|
38
|
-
export function buildTurnMetricRecord(input) {
|
|
39
|
-
const toolCalls = (input.toolCalls ?? []).map((call) => ({
|
|
40
|
-
name: call.name,
|
|
41
|
-
durationMs: Math.max(0, Math.round(call.durationMs)),
|
|
42
|
-
ok: call.ok,
|
|
43
|
-
...(call.ok ? {} : { error: truncateError(call.error) }),
|
|
44
|
-
}));
|
|
45
|
-
const runtimeProvenance = input.runtimeProvenance === undefined ? undefined : parseRuntimeProvenance(input.runtimeProvenance);
|
|
46
|
-
// Spread the raw input minus its projection: a validated projection is re-added
|
|
47
|
-
// below, an invalid one is dropped rather than persisted.
|
|
48
|
-
const { runtimeProvenance: _rawProvenance, ...rest } = input;
|
|
49
|
-
return {
|
|
50
|
-
...rest,
|
|
51
|
-
schemaVersion: TURN_METRICS_SCHEMA_VERSION,
|
|
52
|
-
durationMs: Math.max(0, input.endedAtEpochMs - input.startedAtEpochMs),
|
|
53
|
-
toolCalls,
|
|
54
|
-
toolCallCount: toolCalls.length,
|
|
55
|
-
toolFailureCount: toolCalls.filter((call) => !call.ok).length,
|
|
56
|
-
...(runtimeProvenance === null || runtimeProvenance === undefined ? {} : { runtimeProvenance }),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
24
|
/**
|
|
60
25
|
* Append-only JSONL sink. Writes are synchronous and best-effort: metrics must
|
|
61
26
|
* never be the reason a turn fails, so every filesystem error is swallowed and
|
|
@@ -91,9 +56,14 @@ export class TurnMetricsSink {
|
|
|
91
56
|
this.dropped++;
|
|
92
57
|
return false;
|
|
93
58
|
}
|
|
59
|
+
const bytes = Buffer.byteLength(line, "utf8");
|
|
60
|
+
if (bytes > this.maxBytes) {
|
|
61
|
+
this.dropped++;
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
94
64
|
try {
|
|
95
65
|
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
96
|
-
this.rotateIfNeeded(
|
|
66
|
+
this.rotateIfNeeded(bytes);
|
|
97
67
|
fs.appendFileSync(this.filePath, line, { encoding: "utf8", mode: 0o600 });
|
|
98
68
|
this.written++;
|
|
99
69
|
return true;
|
|
@@ -128,23 +98,6 @@ function percentile(sorted, fraction) {
|
|
|
128
98
|
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(fraction * sorted.length) - 1));
|
|
129
99
|
return sorted[index];
|
|
130
100
|
}
|
|
131
|
-
function isRecord(value) {
|
|
132
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
133
|
-
}
|
|
134
|
-
function parseRecord(line) {
|
|
135
|
-
let parsed;
|
|
136
|
-
try {
|
|
137
|
-
parsed = JSON.parse(line);
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
return undefined;
|
|
141
|
-
}
|
|
142
|
-
if (!isRecord(parsed) || parsed.schemaVersion !== TURN_METRICS_SCHEMA_VERSION)
|
|
143
|
-
return undefined;
|
|
144
|
-
if (typeof parsed.turnIndex !== "number" || typeof parsed.durationMs !== "number")
|
|
145
|
-
return undefined;
|
|
146
|
-
return parsed;
|
|
147
|
-
}
|
|
148
101
|
/**
|
|
149
102
|
* Aggregate JSONL lines into a summary. Malformed lines are counted rather than
|
|
150
103
|
* discarded silently, so a truncated tail is visible instead of pretending the
|
|
@@ -157,7 +110,7 @@ export function summarizeTurnMetrics(lines) {
|
|
|
157
110
|
const trimmed = line.trim();
|
|
158
111
|
if (trimmed.length === 0)
|
|
159
112
|
continue;
|
|
160
|
-
const record =
|
|
113
|
+
const record = parseTurnMetricRecord(trimmed);
|
|
161
114
|
if (record)
|
|
162
115
|
records.push(record);
|
|
163
116
|
else
|
|
@@ -196,8 +149,7 @@ export function summarizeTurnMetrics(lines) {
|
|
|
196
149
|
if (record.contextCache.planHit)
|
|
197
150
|
planHits++;
|
|
198
151
|
}
|
|
199
|
-
|
|
200
|
-
const provenance = record.runtimeProvenance === undefined ? undefined : parseRuntimeProvenance(record.runtimeProvenance);
|
|
152
|
+
const provenance = record.runtimeProvenance;
|
|
201
153
|
if (provenance !== undefined &&
|
|
202
154
|
provenance !== null &&
|
|
203
155
|
provenance.responseModel !== null &&
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-metrics.js","sourceRoot":"","sources":["../../src/core/turn-metrics.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-turn runtime metrics.\n *\n * Before this, `telemetry.ts` covered install opt-in and nothing else, and\n * `run-journal.ts` recorded a hash-chained *integrity* log (run started /\n * finished / tool timeout). Neither could answer the questions that actually\n * drive harness work: which tools fail, how long they take, what a turn costs,\n * and whether the context cache is doing anything.\n *\n * Shape: one JSON object per line under `<cwd>/.omk/metrics/turns.jsonl`.\n * Append-only, bounded by size with a single rotation, and never load-bearing —\n * a failed write is dropped, never surfaced as a turn error.\n *\n * Privacy: records counts, durations, ids, and error *classes*. It never\n * records prompt text, tool arguments, tool output, or file contents. Tool\n * error strings are truncated and kept only to distinguish failure modes.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { parseRuntimeProvenance, type RuntimeProvenance } from \"./runtime-provenance.ts\";\n\nexport const TURN_METRICS_SCHEMA_VERSION = \"omk-turn-metrics-1\" as const;\n/** Rotate once the active file passes this size. */\nexport const DEFAULT_MAX_METRICS_BYTES = 8 * 1024 * 1024;\n/** Cap on a retained error string. Enough to classify, too short to carry a payload. */\nexport const MAX_ERROR_CHARS = 200;\n\nexport interface ToolCallMetric {\n\treadonly name: string;\n\treadonly durationMs: number;\n\treadonly ok: boolean;\n\t/** Truncated failure text, present only when `ok` is false. */\n\treadonly error?: string;\n}\n\nexport interface TurnUsageMetric {\n\treadonly input: number;\n\treadonly output: number;\n\treadonly cacheRead: number;\n\treadonly cacheWrite: number;\n\treadonly costUsd: number;\n}\n\nexport interface TurnMetricInput {\n\treadonly sessionId: string;\n\treadonly turnIndex: number;\n\treadonly provider?: string;\n\treadonly model?: string;\n\treadonly startedAtEpochMs: number;\n\treadonly endedAtEpochMs: number;\n\t/** Time to the first streamed assistant chunk, when observed. */\n\treadonly timeToFirstChunkMs?: number;\n\treadonly usage?: TurnUsageMetric;\n\treadonly stopReason?: string;\n\treadonly toolCalls?: readonly ToolCallMetric[];\n\t/** Whether compaction ran during this turn. */\n\treadonly compacted?: boolean;\n\t/** Provider failover attempts made during this turn. */\n\treadonly failovers?: number;\n\t/** Context-budget cache outcome for the turn's prompt build. */\n\treadonly contextCache?: { readonly planHit: boolean; readonly hits: number; readonly misses: number };\n\t/** Requested/selected/response model and thinking projection. Advisory metadata only. */\n\treadonly runtimeProvenance?: RuntimeProvenance;\n}\n\nexport interface TurnMetricRecord extends TurnMetricInput {\n\treadonly schemaVersion: typeof TURN_METRICS_SCHEMA_VERSION;\n\treadonly durationMs: number;\n\treadonly toolCallCount: number;\n\treadonly toolFailureCount: number;\n}\n\nfunction truncateError(value: string | undefined): string | undefined {\n\tif (value === undefined) return undefined;\n\tconst collapsed = value.replace(/\\s+/gu, \" \").trim();\n\tif (collapsed.length === 0) return undefined;\n\treturn collapsed.length > MAX_ERROR_CHARS ? `${collapsed.slice(0, MAX_ERROR_CHARS)}\\u2026` : collapsed;\n}\n\n/**\n * Build the record written for one turn. Pure: no clock, no filesystem, so the\n * derived fields can be asserted directly.\n */\nexport function buildTurnMetricRecord(input: TurnMetricInput): TurnMetricRecord {\n\tconst toolCalls = (input.toolCalls ?? []).map((call) => ({\n\t\tname: call.name,\n\t\tdurationMs: Math.max(0, Math.round(call.durationMs)),\n\t\tok: call.ok,\n\t\t...(call.ok ? {} : { error: truncateError(call.error) }),\n\t}));\n\tconst runtimeProvenance =\n\t\tinput.runtimeProvenance === undefined ? undefined : parseRuntimeProvenance(input.runtimeProvenance);\n\t// Spread the raw input minus its projection: a validated projection is re-added\n\t// below, an invalid one is dropped rather than persisted.\n\tconst { runtimeProvenance: _rawProvenance, ...rest } = input;\n\treturn {\n\t\t...rest,\n\t\tschemaVersion: TURN_METRICS_SCHEMA_VERSION,\n\t\tdurationMs: Math.max(0, input.endedAtEpochMs - input.startedAtEpochMs),\n\t\ttoolCalls,\n\t\ttoolCallCount: toolCalls.length,\n\t\ttoolFailureCount: toolCalls.filter((call) => !call.ok).length,\n\t\t...(runtimeProvenance === null || runtimeProvenance === undefined ? {} : { runtimeProvenance }),\n\t};\n}\n\nexport interface TurnMetricsSinkOptions {\n\treadonly dir: string;\n\treadonly maxBytes?: number;\n\treadonly fileName?: string;\n}\n\n/**\n * Append-only JSONL sink. Writes are synchronous and best-effort: metrics must\n * never be the reason a turn fails, so every filesystem error is swallowed and\n * counted instead of thrown.\n */\nexport class TurnMetricsSink {\n\tprivate readonly filePath: string;\n\tprivate readonly maxBytes: number;\n\tprivate dropped = 0;\n\tprivate written = 0;\n\n\tconstructor(options: TurnMetricsSinkOptions) {\n\t\tthis.filePath = path.join(options.dir, options.fileName ?? \"turns.jsonl\");\n\t\tthis.maxBytes = Math.max(1024, options.maxBytes ?? DEFAULT_MAX_METRICS_BYTES);\n\t}\n\n\tget path(): string {\n\t\treturn this.filePath;\n\t}\n\n\t/** Records written successfully this process. */\n\tget writtenCount(): number {\n\t\treturn this.written;\n\t}\n\n\t/** Records lost to a filesystem error this process. */\n\tget droppedCount(): number {\n\t\treturn this.dropped;\n\t}\n\n\t/** Append one turn. Returns `true` when the line reached disk. */\n\trecord(input: TurnMetricInput): boolean {\n\t\tlet line: string;\n\t\ttry {\n\t\t\tline = `${JSON.stringify(buildTurnMetricRecord(input))}\\n`;\n\t\t} catch {\n\t\t\tthis.dropped++;\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\tfs.mkdirSync(path.dirname(this.filePath), { recursive: true });\n\t\t\tthis.rotateIfNeeded(Buffer.byteLength(line, \"utf8\"));\n\t\t\tfs.appendFileSync(this.filePath, line, { encoding: \"utf8\", mode: 0o600 });\n\t\t\tthis.written++;\n\t\t\treturn true;\n\t\t} catch {\n\t\t\tthis.dropped++;\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate rotateIfNeeded(incomingBytes: number): void {\n\t\tlet size: number;\n\t\ttry {\n\t\t\tsize = fs.statSync(this.filePath).size;\n\t\t} catch {\n\t\t\treturn; // No file yet.\n\t\t}\n\t\tif (size + incomingBytes <= this.maxBytes) return;\n\t\ttry {\n\t\t\t// Single generation: the previous file is replaced, so disk use stays bounded at 2x.\n\t\t\tfs.renameSync(this.filePath, `${this.filePath}.1`);\n\t\t} catch {\n\t\t\t// Rotation failure is not fatal; the next append still succeeds.\n\t\t}\n\t}\n}\n\nexport interface ToolAggregate {\n\treadonly name: string;\n\treadonly calls: number;\n\treadonly failures: number;\n\treadonly failureRate: number;\n\treadonly p50Ms: number;\n\treadonly p95Ms: number;\n\treadonly totalMs: number;\n}\n\nexport interface TurnMetricsSummary {\n\treadonly turns: number;\n\treadonly sessions: number;\n\treadonly models: readonly string[];\n\treadonly totalCostUsd: number;\n\treadonly totals: TurnUsageMetric;\n\treadonly turnDurationP50Ms: number;\n\treadonly turnDurationP95Ms: number;\n\treadonly timeToFirstChunkP50Ms?: number;\n\treadonly cacheReadShare: number;\n\treadonly compactions: number;\n\treadonly failovers: number;\n\treadonly contextCachePlanHitRate?: number;\n\t/** Turns where a valid projection's response model differs from the selected model. */\n\treadonly provenanceMismatchTurns: number;\n\treadonly tools: readonly ToolAggregate[];\n\t/** Lines that could not be parsed as a metric record. */\n\treadonly malformedLines: number;\n}\n\nfunction percentile(sorted: readonly number[], fraction: number): number {\n\tif (sorted.length === 0) return 0;\n\tconst index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(fraction * sorted.length) - 1));\n\treturn sorted[index];\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction parseRecord(line: string): TurnMetricRecord | undefined {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!isRecord(parsed) || parsed.schemaVersion !== TURN_METRICS_SCHEMA_VERSION) return undefined;\n\tif (typeof parsed.turnIndex !== \"number\" || typeof parsed.durationMs !== \"number\") return undefined;\n\treturn parsed as unknown as TurnMetricRecord;\n}\n\n/**\n * Aggregate JSONL lines into a summary. Malformed lines are counted rather than\n * discarded silently, so a truncated tail is visible instead of pretending the\n * data is complete.\n */\nexport function summarizeTurnMetrics(lines: readonly string[]): TurnMetricsSummary {\n\tconst records: TurnMetricRecord[] = [];\n\tlet malformedLines = 0;\n\tfor (const line of lines) {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed.length === 0) continue;\n\t\tconst record = parseRecord(trimmed);\n\t\tif (record) records.push(record);\n\t\telse malformedLines++;\n\t}\n\n\tconst sessions = new Set<string>();\n\tconst models = new Set<string>();\n\tconst totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, costUsd: 0 };\n\tconst turnDurations: number[] = [];\n\tconst firstChunkTimes: number[] = [];\n\tconst toolStats = new Map<string, { durations: number[]; failures: number }>();\n\tlet compactions = 0;\n\tlet failovers = 0;\n\tlet planHits = 0;\n\tlet planObservations = 0;\n\tlet provenanceMismatchTurns = 0;\n\n\tfor (const record of records) {\n\t\tsessions.add(record.sessionId);\n\t\tif (record.model) models.add(record.provider ? `${record.provider}/${record.model}` : record.model);\n\t\tif (record.usage) {\n\t\t\ttotals.input += record.usage.input;\n\t\t\ttotals.output += record.usage.output;\n\t\t\ttotals.cacheRead += record.usage.cacheRead;\n\t\t\ttotals.cacheWrite += record.usage.cacheWrite;\n\t\t\ttotals.costUsd += record.usage.costUsd;\n\t\t}\n\t\tturnDurations.push(record.durationMs);\n\t\tif (typeof record.timeToFirstChunkMs === \"number\") firstChunkTimes.push(record.timeToFirstChunkMs);\n\t\tif (record.compacted) compactions++;\n\t\tfailovers += record.failovers ?? 0;\n\t\tif (record.contextCache) {\n\t\t\tplanObservations++;\n\t\t\tif (record.contextCache.planHit) planHits++;\n\t\t}\n\t\t// Re-validate provenance at read time: a tampered projection never counts.\n\t\tconst provenance =\n\t\t\trecord.runtimeProvenance === undefined ? undefined : parseRuntimeProvenance(record.runtimeProvenance);\n\t\tif (\n\t\t\tprovenance !== undefined &&\n\t\t\tprovenance !== null &&\n\t\t\tprovenance.responseModel !== null &&\n\t\t\tprovenance.responseModel !== provenance.selectedModel\n\t\t) {\n\t\t\tprovenanceMismatchTurns++;\n\t\t}\n\t\tfor (const call of record.toolCalls ?? []) {\n\t\t\tconst stats = toolStats.get(call.name) ?? { durations: [], failures: 0 };\n\t\t\tstats.durations.push(call.durationMs);\n\t\t\tif (!call.ok) stats.failures++;\n\t\t\ttoolStats.set(call.name, stats);\n\t\t}\n\t}\n\n\tturnDurations.sort((a, b) => a - b);\n\tfirstChunkTimes.sort((a, b) => a - b);\n\n\tconst tools: ToolAggregate[] = [...toolStats.entries()]\n\t\t.map(([name, stats]) => {\n\t\t\tconst sorted = [...stats.durations].sort((a, b) => a - b);\n\t\t\treturn {\n\t\t\t\tname,\n\t\t\t\tcalls: sorted.length,\n\t\t\t\tfailures: stats.failures,\n\t\t\t\tfailureRate: sorted.length === 0 ? 0 : stats.failures / sorted.length,\n\t\t\t\tp50Ms: percentile(sorted, 0.5),\n\t\t\t\tp95Ms: percentile(sorted, 0.95),\n\t\t\t\ttotalMs: sorted.reduce((sum, value) => sum + value, 0),\n\t\t\t};\n\t\t})\n\t\t.sort((a, b) => b.totalMs - a.totalMs || a.name.localeCompare(b.name));\n\n\tconst promptSideTotal = totals.input + totals.cacheRead;\n\treturn {\n\t\tturns: records.length,\n\t\tsessions: sessions.size,\n\t\tmodels: [...models].sort(),\n\t\ttotalCostUsd: totals.costUsd,\n\t\ttotals,\n\t\tturnDurationP50Ms: percentile(turnDurations, 0.5),\n\t\tturnDurationP95Ms: percentile(turnDurations, 0.95),\n\t\ttimeToFirstChunkP50Ms: firstChunkTimes.length > 0 ? percentile(firstChunkTimes, 0.5) : undefined,\n\t\tcacheReadShare: promptSideTotal === 0 ? 0 : totals.cacheRead / promptSideTotal,\n\t\tcompactions,\n\t\tfailovers,\n\t\tcontextCachePlanHitRate: planObservations === 0 ? undefined : planHits / planObservations,\n\t\tprovenanceMismatchTurns,\n\t\ttools,\n\t\tmalformedLines,\n\t};\n}\n\n/** Read and summarize a metrics file. A missing file summarizes as zero turns. */\nexport function summarizeTurnMetricsFile(filePath: string): TurnMetricsSummary {\n\tlet raw: string;\n\ttry {\n\t\traw = fs.readFileSync(filePath, \"utf8\");\n\t} catch {\n\t\treturn summarizeTurnMetrics([]);\n\t}\n\treturn summarizeTurnMetrics(raw.split(\"\\n\"));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"turn-metrics.js","sourceRoot":"","sources":["../../src/core/turn-metrics.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-turn runtime metrics.\n *\n * Before this, `telemetry.ts` covered install opt-in and nothing else, and\n * `run-journal.ts` recorded a hash-chained *integrity* log (run started /\n * finished / tool timeout). Neither could answer the questions that actually\n * drive harness work: which tools fail, how long they take, what a turn costs,\n * and whether the context cache is doing anything.\n *\n * Shape: one JSON object per line under `<cwd>/.omk/metrics/turns.jsonl`.\n * Append-only, bounded by size with a single rotation, and never load-bearing —\n * a failed write is dropped, never surfaced as a turn error.\n *\n * Privacy: records counts, durations, ids, and error *classes*. It never\n * records prompt text, tool arguments, tool output, or file contents. New\n * records retain bounded error classes only; raw failure text is never serialized.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport {\n\tbuildTurnMetricRecord,\n\tparseTurnMetricRecord,\n\ttype TurnMetricInput,\n\ttype TurnMetricRecord,\n\ttype TurnUsageMetric,\n} from \"./turn-metrics-record.ts\";\n\nexport * from \"./turn-metrics-record.ts\";\n/** Rotate once the active file passes this size. */\nexport const DEFAULT_MAX_METRICS_BYTES = 8 * 1024 * 1024;\nexport interface TurnMetricsSinkOptions {\n\treadonly dir: string;\n\treadonly maxBytes?: number;\n\treadonly fileName?: string;\n}\n\n/**\n * Append-only JSONL sink. Writes are synchronous and best-effort: metrics must\n * never be the reason a turn fails, so every filesystem error is swallowed and\n * counted instead of thrown.\n */\nexport class TurnMetricsSink {\n\tprivate readonly filePath: string;\n\tprivate readonly maxBytes: number;\n\tprivate dropped = 0;\n\tprivate written = 0;\n\n\tconstructor(options: TurnMetricsSinkOptions) {\n\t\tthis.filePath = path.join(options.dir, options.fileName ?? \"turns.jsonl\");\n\t\tthis.maxBytes = Math.max(1024, options.maxBytes ?? DEFAULT_MAX_METRICS_BYTES);\n\t}\n\n\tget path(): string {\n\t\treturn this.filePath;\n\t}\n\n\t/** Records written successfully this process. */\n\tget writtenCount(): number {\n\t\treturn this.written;\n\t}\n\n\t/** Records lost to a filesystem error this process. */\n\tget droppedCount(): number {\n\t\treturn this.dropped;\n\t}\n\n\t/** Append one turn. Returns `true` when the line reached disk. */\n\trecord(input: TurnMetricInput): boolean {\n\t\tlet line: string;\n\t\ttry {\n\t\t\tline = `${JSON.stringify(buildTurnMetricRecord(input))}\\n`;\n\t\t} catch {\n\t\t\tthis.dropped++;\n\t\t\treturn false;\n\t\t}\n\t\tconst bytes = Buffer.byteLength(line, \"utf8\");\n\t\tif (bytes > this.maxBytes) {\n\t\t\tthis.dropped++;\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\tfs.mkdirSync(path.dirname(this.filePath), { recursive: true });\n\t\t\tthis.rotateIfNeeded(bytes);\n\t\t\tfs.appendFileSync(this.filePath, line, { encoding: \"utf8\", mode: 0o600 });\n\t\t\tthis.written++;\n\t\t\treturn true;\n\t\t} catch {\n\t\t\tthis.dropped++;\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate rotateIfNeeded(incomingBytes: number): void {\n\t\tlet size: number;\n\t\ttry {\n\t\t\tsize = fs.statSync(this.filePath).size;\n\t\t} catch {\n\t\t\treturn; // No file yet.\n\t\t}\n\t\tif (size + incomingBytes <= this.maxBytes) return;\n\t\ttry {\n\t\t\t// Single generation: the previous file is replaced, so disk use stays bounded at 2x.\n\t\t\tfs.renameSync(this.filePath, `${this.filePath}.1`);\n\t\t} catch {\n\t\t\t// Rotation failure is not fatal; the next append still succeeds.\n\t\t}\n\t}\n}\n\nexport interface ToolAggregate {\n\treadonly name: string;\n\treadonly calls: number;\n\treadonly failures: number;\n\treadonly failureRate: number;\n\treadonly p50Ms: number;\n\treadonly p95Ms: number;\n\treadonly totalMs: number;\n}\n\nexport interface TurnMetricsSummary {\n\treadonly turns: number;\n\treadonly sessions: number;\n\treadonly models: readonly string[];\n\treadonly totalCostUsd: number;\n\treadonly totals: TurnUsageMetric;\n\treadonly turnDurationP50Ms: number;\n\treadonly turnDurationP95Ms: number;\n\treadonly timeToFirstChunkP50Ms?: number;\n\treadonly cacheReadShare: number;\n\treadonly compactions: number;\n\treadonly failovers: number;\n\treadonly contextCachePlanHitRate?: number;\n\t/** Turns where a valid projection's response model differs from the selected model. */\n\treadonly provenanceMismatchTurns: number;\n\treadonly tools: readonly ToolAggregate[];\n\t/** Lines that could not be parsed as a metric record. */\n\treadonly malformedLines: number;\n}\n\nfunction percentile(sorted: readonly number[], fraction: number): number {\n\tif (sorted.length === 0) return 0;\n\tconst index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(fraction * sorted.length) - 1));\n\treturn sorted[index];\n}\n\n/**\n * Aggregate JSONL lines into a summary. Malformed lines are counted rather than\n * discarded silently, so a truncated tail is visible instead of pretending the\n * data is complete.\n */\nexport function summarizeTurnMetrics(lines: readonly string[]): TurnMetricsSummary {\n\tconst records: TurnMetricRecord[] = [];\n\tlet malformedLines = 0;\n\tfor (const line of lines) {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed.length === 0) continue;\n\t\tconst record = parseTurnMetricRecord(trimmed);\n\t\tif (record) records.push(record);\n\t\telse malformedLines++;\n\t}\n\n\tconst sessions = new Set<string>();\n\tconst models = new Set<string>();\n\tconst totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, costUsd: 0 };\n\tconst turnDurations: number[] = [];\n\tconst firstChunkTimes: number[] = [];\n\tconst toolStats = new Map<string, { durations: number[]; failures: number }>();\n\tlet compactions = 0;\n\tlet failovers = 0;\n\tlet planHits = 0;\n\tlet planObservations = 0;\n\tlet provenanceMismatchTurns = 0;\n\n\tfor (const record of records) {\n\t\tsessions.add(record.sessionId);\n\t\tif (record.model) models.add(record.provider ? `${record.provider}/${record.model}` : record.model);\n\t\tif (record.usage) {\n\t\t\ttotals.input += record.usage.input;\n\t\t\ttotals.output += record.usage.output;\n\t\t\ttotals.cacheRead += record.usage.cacheRead;\n\t\t\ttotals.cacheWrite += record.usage.cacheWrite;\n\t\t\ttotals.costUsd += record.usage.costUsd;\n\t\t}\n\t\tturnDurations.push(record.durationMs);\n\t\tif (typeof record.timeToFirstChunkMs === \"number\") firstChunkTimes.push(record.timeToFirstChunkMs);\n\t\tif (record.compacted) compactions++;\n\t\tfailovers += record.failovers ?? 0;\n\t\tif (record.contextCache) {\n\t\t\tplanObservations++;\n\t\t\tif (record.contextCache.planHit) planHits++;\n\t\t}\n\t\tconst provenance = record.runtimeProvenance;\n\t\tif (\n\t\t\tprovenance !== undefined &&\n\t\t\tprovenance !== null &&\n\t\t\tprovenance.responseModel !== null &&\n\t\t\tprovenance.responseModel !== provenance.selectedModel\n\t\t) {\n\t\t\tprovenanceMismatchTurns++;\n\t\t}\n\t\tfor (const call of record.toolCalls ?? []) {\n\t\t\tconst stats = toolStats.get(call.name) ?? { durations: [], failures: 0 };\n\t\t\tstats.durations.push(call.durationMs);\n\t\t\tif (!call.ok) stats.failures++;\n\t\t\ttoolStats.set(call.name, stats);\n\t\t}\n\t}\n\n\tturnDurations.sort((a, b) => a - b);\n\tfirstChunkTimes.sort((a, b) => a - b);\n\n\tconst tools: ToolAggregate[] = [...toolStats.entries()]\n\t\t.map(([name, stats]) => {\n\t\t\tconst sorted = [...stats.durations].sort((a, b) => a - b);\n\t\t\treturn {\n\t\t\t\tname,\n\t\t\t\tcalls: sorted.length,\n\t\t\t\tfailures: stats.failures,\n\t\t\t\tfailureRate: sorted.length === 0 ? 0 : stats.failures / sorted.length,\n\t\t\t\tp50Ms: percentile(sorted, 0.5),\n\t\t\t\tp95Ms: percentile(sorted, 0.95),\n\t\t\t\ttotalMs: sorted.reduce((sum, value) => sum + value, 0),\n\t\t\t};\n\t\t})\n\t\t.sort((a, b) => b.totalMs - a.totalMs || a.name.localeCompare(b.name));\n\n\tconst promptSideTotal = totals.input + totals.cacheRead;\n\treturn {\n\t\tturns: records.length,\n\t\tsessions: sessions.size,\n\t\tmodels: [...models].sort(),\n\t\ttotalCostUsd: totals.costUsd,\n\t\ttotals,\n\t\tturnDurationP50Ms: percentile(turnDurations, 0.5),\n\t\tturnDurationP95Ms: percentile(turnDurations, 0.95),\n\t\ttimeToFirstChunkP50Ms: firstChunkTimes.length > 0 ? percentile(firstChunkTimes, 0.5) : undefined,\n\t\tcacheReadShare: promptSideTotal === 0 ? 0 : totals.cacheRead / promptSideTotal,\n\t\tcompactions,\n\t\tfailovers,\n\t\tcontextCachePlanHitRate: planObservations === 0 ? undefined : planHits / planObservations,\n\t\tprovenanceMismatchTurns,\n\t\ttools,\n\t\tmalformedLines,\n\t};\n}\n\n/** Read and summarize a metrics file. A missing file summarizes as zero turns. */\nexport function summarizeTurnMetricsFile(filePath: string): TurnMetricsSummary {\n\tlet raw: string;\n\ttry {\n\t\traw = fs.readFileSync(filePath, \"utf8\");\n\t} catch {\n\t\treturn summarizeTurnMetrics([]);\n\t}\n\treturn summarizeTurnMetrics(raw.split(\"\\n\"));\n}\n"]}
|
|
@@ -77,7 +77,7 @@ export declare class EvidenceGate {
|
|
|
77
77
|
check(contract: TaskContract): MergeGateResult;
|
|
78
78
|
}
|
|
79
79
|
export declare class FailClosedMergeGate {
|
|
80
|
-
private gates;
|
|
80
|
+
private readonly gates;
|
|
81
81
|
constructor(gates?: EvidenceGate[]);
|
|
82
82
|
check(contract: TaskContract): MergeGateResult;
|
|
83
83
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evidence-system.d.ts","sourceRoot":"","sources":["../../src/guardrails/evidence-system.ts"],"names":[],"mappings":"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","sourcesContent":["// allow: SIZE_OK - evidence ledger guardrails: tamper-evident replay chain + fail-closed parsing.\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type {\n\tEvidenceCommandDescriptor,\n\tEvidenceItem,\n\tEvidenceReceiptMode,\n\tEvidenceStatus,\n\tMergeGateResult,\n\tMergeGateStatus,\n\tReplayEvent,\n\tReplayLedger,\n\tReplayLedgerHead,\n\tTaskContract,\n\tVerifiedReplayLedgerSnapshot,\n\tWorkspaceScope,\n} from \"../types/evidence.ts\";\nimport { redactCommandDescriptor } from \"./command-redaction.ts\";\nimport {\n\ttype CommandHmacBinder,\n\tlatestRelevantWorkspaceMutationSeq,\n\tparseVerifiedLedgerSnapshot,\n\treplayPayloadMatches,\n\tverifyCommandAttestation,\n\tverifyWorkspaceBinding,\n} from \"./evidence-attestation.ts\";\n\nexport { latestRelevantWorkspaceMutationSeq } from \"./evidence-attestation.ts\";\n\nimport {\n\tcomputeEvidenceCommandSha256,\n\tconstantTimeSha256Equal,\n\tevidenceReceiptReplayPayload,\n\tparseSha256Hex,\n\tvalidateEvidenceReceipt,\n} from \"./evidence-receipt.ts\";\nimport { ReplayLedgerStore, replayLedgerHeadsEqual } from \"./replay-ledger-store.ts\";\nimport {\n\tcomputeReplayPayloadHash,\n\tJCS_REPLAY_PAYLOAD_HASH_ALGORITHM,\n\tLEGACY_REPLAY_PAYLOAD_HASH_ALGORITHM,\n} from \"./replay-payload-hash.ts\";\n\nfunction sha256(input: string): string {\n\treturn createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nfunction ensureDir(path: string): void {\n\tconst dir = dirname(path);\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n}\n\n// ============================================================================\n// TaskContract\n// ============================================================================\n\nexport class TaskContractBuilder {\n\tprivate goalId: string;\n\tprivate completionClaim: string = \"\";\n\tprivate requiredEvidence: EvidenceItem[] = [];\n\tprivate finalRisk: string = \"\";\n\tprivate verdict: \"pass\" | \"fail\" | \"conditional\" = \"fail\";\n\tprivate createdAt: string;\n\tprivate updatedAt: string;\n\n\tconstructor(goalId: string) {\n\t\tthis.goalId = goalId;\n\t\tconst now = new Date().toISOString();\n\t\tthis.createdAt = now;\n\t\tthis.updatedAt = now;\n\t}\n\n\tsetClaim(claim: string): this {\n\t\tthis.completionClaim = claim;\n\t\tthis.touch();\n\t\treturn this;\n\t}\n\n\taddRequiredEvidence(evidence: Omit<EvidenceItem, \"timestamp\" | \"status\">): this {\n\t\tthis.requiredEvidence.push({\n\t\t\t...evidence,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tstatus: \"pending\",\n\t\t});\n\t\tthis.touch();\n\t\treturn this;\n\t}\n\n\tsetFinalRisk(risk: string): this {\n\t\tthis.finalRisk = risk;\n\t\tthis.touch();\n\t\treturn this;\n\t}\n\n\t/** @deprecated Use `evaluateTask()` from `omk-protocol`; verdicts are derived, not mutated. */\n\tsetVerdict(verdict: \"pass\" | \"fail\" | \"conditional\"): this {\n\t\tthis.verdict = verdict;\n\t\tthis.touch();\n\t\treturn this;\n\t}\n\n\t/** @deprecated Append a protocol Observation and re-evaluate instead of mutating evidence state. */\n\tupdateEvidenceStatus(claim: string, status: EvidenceStatus, gapReason?: string): this {\n\t\tconst item = this.requiredEvidence.find((e) => e.claim === claim);\n\t\tif (item) {\n\t\t\titem.status = status;\n\t\t\titem.gapReason = gapReason;\n\t\t\titem.timestamp = new Date().toISOString();\n\t\t\tthis.touch();\n\t\t}\n\t\treturn this;\n\t}\n\n\tprivate touch(): void {\n\t\tthis.updatedAt = new Date().toISOString();\n\t}\n\n\tbuild(): TaskContract {\n\t\treturn structuredClone({\n\t\t\tgoalId: this.goalId,\n\t\t\tcompletionClaim: this.completionClaim,\n\t\t\trequiredEvidence: this.requiredEvidence,\n\t\t\tfinalRisk: this.finalRisk,\n\t\t\tverdict: this.verdict,\n\t\t\tcreatedAt: this.createdAt,\n\t\t\tupdatedAt: this.updatedAt,\n\t\t});\n\t}\n\n\t/** Parse and validate a serialized TaskContract. Fails closed on any shape violation. */\n\tstatic fromJSON(json: string): TaskContract {\n\t\tconst raw: unknown = JSON.parse(json);\n\t\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) {\n\t\t\tthrow new Error(\"TaskContract JSON must be an object\");\n\t\t}\n\t\tconst { goalId, completionClaim, finalRisk, verdict, createdAt, updatedAt, requiredEvidence } = raw as Record<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>;\n\t\tif (\n\t\t\ttypeof goalId !== \"string\" ||\n\t\t\ttypeof completionClaim !== \"string\" ||\n\t\t\ttypeof finalRisk !== \"string\" ||\n\t\t\ttypeof createdAt !== \"string\" ||\n\t\t\ttypeof updatedAt !== \"string\"\n\t\t) {\n\t\t\tthrow new Error(\"TaskContract JSON has missing or mistyped string fields\");\n\t\t}\n\t\tif (verdict !== \"pass\" && verdict !== \"fail\" && verdict !== \"conditional\") {\n\t\t\tthrow new Error(`TaskContract verdict must be pass|fail|conditional, got ${String(verdict)}`);\n\t\t}\n\t\tif (!Array.isArray(requiredEvidence)) {\n\t\t\tthrow new Error(\"TaskContract requiredEvidence must be an array\");\n\t\t}\n\t\treturn {\n\t\t\tgoalId,\n\t\t\tcompletionClaim,\n\t\t\tfinalRisk,\n\t\t\tverdict,\n\t\t\tcreatedAt,\n\t\t\tupdatedAt,\n\t\t\trequiredEvidence: requiredEvidence.map((item, index) => parseEvidenceItem(item, index)),\n\t\t};\n\t}\n\n\tstatic toJSON(contract: TaskContract): string {\n\t\treturn JSON.stringify(contract, null, 2);\n\t}\n}\n\nconst EVIDENCE_STATUSES: ReadonlySet<string> = new Set([\"pending\", \"gathering\", \"satisfied\", \"failed\", \"waived\"]);\n\nfunction optionalStringField(value: unknown, field: string, index: number): string | undefined {\n\tif (value !== undefined && typeof value !== \"string\") {\n\t\tthrow new Error(`TaskContract evidence[${index}].${field} must be a string when present`);\n\t}\n\treturn value;\n}\n\nfunction parseEvidenceItem(raw: unknown, index: number): EvidenceItem {\n\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) {\n\t\tthrow new Error(`TaskContract evidence[${index}] must be an object`);\n\t}\n\tconst item = raw as Record<string, unknown>;\n\tconst { claim, category, timestamp, status } = item;\n\tif (typeof claim !== \"string\" || typeof category !== \"string\" || typeof timestamp !== \"string\") {\n\t\tthrow new Error(`TaskContract evidence[${index}] has missing or mistyped fields`);\n\t}\n\tif (typeof status !== \"string\" || !EVIDENCE_STATUSES.has(status)) {\n\t\tthrow new Error(`TaskContract evidence[${index}] has invalid status ${String(status)}`);\n\t}\n\tconst receiptSchemaVersion = item.receiptSchemaVersion;\n\tif (receiptSchemaVersion !== undefined && receiptSchemaVersion !== 3) {\n\t\tthrow new Error(`TaskContract evidence[${index}].receiptSchemaVersion must be 3 when present`);\n\t}\n\tconst receiptCommandSha256 = optionalStringField(item.receiptCommandSha256, \"receiptCommandSha256\", index);\n\treturn {\n\t\tclaim,\n\t\tcategory: category as EvidenceItem[\"category\"],\n\t\ttimestamp,\n\t\tstatus: status as EvidenceStatus,\n\t\tartifactPath: optionalStringField(item.artifactPath, \"artifactPath\", index),\n\t\tverificationCommand: optionalStringField(item.verificationCommand, \"verificationCommand\", index),\n\t\thash: optionalStringField(item.hash, \"hash\", index),\n\t\treceiptId: optionalStringField(item.receiptId, \"receiptId\", index),\n\t\treceiptSchemaVersion,\n\t\t...(receiptCommandSha256 !== undefined\n\t\t\t? { receiptCommandSha256: parseSha256Hex(receiptCommandSha256, \"receiptCommandSha256\") }\n\t\t\t: {}),\n\t\treceiptLaneId: optionalStringField(item.receiptLaneId, \"receiptLaneId\", index),\n\t\tgapReason: optionalStringField(item.gapReason, \"gapReason\", index),\n\t};\n}\n\n// ============================================================================\n// ReplayLedger\n// ============================================================================\n\n/** prevHash sentinel for the first event in a replay chain. */\nconst GENESIS_HASH = \"genesis\";\n\nfunction computeEventHash(event: Omit<ReplayEvent, \"eventHash\">): string {\n\tconst metadata = [event.seq, event.type, event.timestamp, event.goalId, event.laneId ?? null];\n\tconst material =\n\t\tevent.payloadHashAlgorithm === undefined\n\t\t\t? [...metadata, event.payloadHash, event.prevHash]\n\t\t\t: [...metadata, event.payloadHashAlgorithm, event.payloadHash, event.prevHash];\n\treturn sha256(JSON.stringify(material));\n}\n\nfunction parseReplayEventLine(line: string, lineNumber: number): ReplayEvent {\n\tlet raw: unknown;\n\ttry {\n\t\traw = JSON.parse(line);\n\t} catch {\n\t\tthrow new Error(`Replay ledger corrupted at line ${lineNumber}: invalid JSON`);\n\t}\n\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) {\n\t\tthrow new Error(`Replay ledger corrupted at line ${lineNumber}: event must be an object`);\n\t}\n\tconst event = raw as Record<string, unknown>;\n\tconst { seq, type, timestamp, goalId, laneId, payloadHashAlgorithm, payloadHash, prevHash, eventHash } = event;\n\tif (\n\t\ttypeof seq !== \"number\" ||\n\t\ttypeof type !== \"string\" ||\n\t\ttypeof timestamp !== \"string\" ||\n\t\ttypeof goalId !== \"string\" ||\n\t\ttypeof payloadHash !== \"string\" ||\n\t\ttypeof prevHash !== \"string\" ||\n\t\ttypeof eventHash !== \"string\" ||\n\t\t(laneId !== undefined && typeof laneId !== \"string\") ||\n\t\t!(\"payload\" in event)\n\t) {\n\t\tthrow new Error(`Replay ledger corrupted at line ${lineNumber}: event fields are missing or mistyped`);\n\t}\n\tif (\n\t\tpayloadHashAlgorithm !== undefined &&\n\t\tpayloadHashAlgorithm !== LEGACY_REPLAY_PAYLOAD_HASH_ALGORITHM &&\n\t\tpayloadHashAlgorithm !== JCS_REPLAY_PAYLOAD_HASH_ALGORITHM\n\t) {\n\t\tthrow new Error(`Replay ledger corrupted at line ${lineNumber}: unsupported payload hash algorithm`);\n\t}\n\tconst parsed: ReplayEvent = {\n\t\tseq,\n\t\ttype: type as ReplayEvent[\"type\"],\n\t\ttimestamp,\n\t\tgoalId,\n\t\tlaneId,\n\t\tpayload: event.payload,\n\t\tpayloadHash,\n\t\tprevHash,\n\t\teventHash,\n\t};\n\treturn payloadHashAlgorithm === undefined ? parsed : { ...parsed, payloadHashAlgorithm };\n}\n\nfunction parseReplayLedgerBytes(bytes: Buffer, goalId: string): ReplayEvent[] {\n\tconst lines = bytes\n\t\t.toString(\"utf8\")\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.trim().length > 0);\n\tconst events: ReplayEvent[] = [];\n\tlet prevHash = GENESIS_HASH;\n\tfor (let index = 0; index < lines.length; index++) {\n\t\tconst event = parseReplayEventLine(lines[index], index + 1);\n\t\tif (event.goalId !== goalId) throw new Error(`Replay ledger goal mismatch at line ${index + 1}`);\n\t\tif (event.seq !== index + 1) {\n\t\t\tthrow new Error(`Replay ledger corrupted at line ${index + 1}: expected seq ${index + 1}, got ${event.seq}`);\n\t\t}\n\t\tif (event.prevHash !== prevHash) throw new Error(`Replay ledger chain broken at line ${index + 1}`);\n\t\tconst payloadHashAlgorithm = event.payloadHashAlgorithm ?? LEGACY_REPLAY_PAYLOAD_HASH_ALGORITHM;\n\t\tif (computeReplayPayloadHash(event.payload, payloadHashAlgorithm) !== event.payloadHash) {\n\t\t\tthrow new Error(`Replay ledger payload tampered at line ${index + 1} (seq ${event.seq})`);\n\t\t}\n\t\tif (computeEventHash(event) !== event.eventHash) {\n\t\t\tthrow new Error(`Replay ledger event hash mismatch at line ${index + 1} (seq ${event.seq})`);\n\t\t}\n\t\tprevHash = event.eventHash;\n\t\tevents.push(event);\n\t}\n\treturn events;\n}\n\nexport class ReplayLedgerManager {\n\tprivate readonly goalId: string;\n\tprivate readonly ledgerPath: string;\n\tprivate readonly store: ReplayLedgerStore;\n\tprivate events: ReplayEvent[] = [];\n\tprivate committedHead: ReplayLedgerHead;\n\n\tconstructor(goalId: string, ledgerPath: string, expectedHead?: ReplayLedgerHead) {\n\t\tthis.goalId = goalId;\n\t\tthis.ledgerPath = ledgerPath;\n\t\tthis.committedHead = { fileIdentity: null, size: 0, lastSeq: 0, lastHash: GENESIS_HASH };\n\t\tthis.store = new ReplayLedgerStore(ledgerPath, (bytes) => {\n\t\t\tconst events = parseReplayLedgerBytes(bytes, goalId);\n\t\t\tconst last = events[events.length - 1];\n\t\t\treturn { lastSeq: last?.seq ?? 0, lastHash: last?.eventHash ?? GENESIS_HASH };\n\t\t});\n\t\tthis.refresh(expectedHead);\n\t}\n\n\tprivate refresh(expectedHead?: ReplayLedgerHead): void {\n\t\tconst snapshot = this.store.load(expectedHead);\n\t\tthis.events = parseReplayLedgerBytes(snapshot.bytes, this.goalId);\n\t\tthis.committedHead = snapshot.head;\n\t}\n\n\tappend(\n\t\tevent: Omit<ReplayEvent, \"seq\" | \"timestamp\" | \"payloadHashAlgorithm\" | \"payloadHash\" | \"prevHash\" | \"eventHash\">,\n\t\texpectedHead: ReplayLedgerHead = this.committedHead,\n\t): ReplayEvent {\n\t\tif (event.goalId !== this.goalId) throw new Error(\"Replay event goalId does not match the ledger\");\n\t\tif (!replayLedgerHeadsEqual(expectedHead, this.committedHead)) {\n\t\t\tthrow new Error(\"Replay manager expected-head CAS is stale\");\n\t\t}\n\t\tconst payloadHashAlgorithm = JCS_REPLAY_PAYLOAD_HASH_ALGORITHM;\n\t\tconst payloadHash = computeReplayPayloadHash(event.payload, payloadHashAlgorithm);\n\t\tconst chained: Omit<ReplayEvent, \"eventHash\"> = {\n\t\t\t...event,\n\t\t\tseq: expectedHead.lastSeq + 1,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tpayloadHashAlgorithm,\n\t\t\tpayloadHash,\n\t\t\tprevHash: expectedHead.lastHash,\n\t\t};\n\t\tconst fullEvent: ReplayEvent = { ...chained, eventHash: computeEventHash(chained) };\n\t\tthis.committedHead = this.store.append(\n\t\t\tBuffer.from(`${JSON.stringify(fullEvent)}\\n`, \"utf8\"),\n\t\t\tfullEvent.seq,\n\t\t\tfullEvent.eventHash,\n\t\t\texpectedHead,\n\t\t);\n\t\tthis.events.push(fullEvent);\n\t\treturn structuredClone(fullEvent);\n\t}\n\n\t/** Compatibility no-op that still verifies the caller's expected committed head. */\n\tpersist(expectedHead: ReplayLedgerHead = this.committedHead): void {\n\t\tthis.getVerifiedSnapshot(expectedHead);\n\t}\n\n\tload(expectedHead: ReplayLedgerHead = this.committedHead): void {\n\t\tthis.refresh(expectedHead);\n\t}\n\n\tgetCommittedHead(): ReplayLedgerHead {\n\t\treturn structuredClone(this.committedHead);\n\t}\n\n\tgetVerifiedSnapshot(expectedHead: ReplayLedgerHead = this.committedHead): VerifiedReplayLedgerSnapshot {\n\t\tthis.refresh(expectedHead);\n\t\treturn structuredClone({ events: this.events, head: this.committedHead });\n\t}\n\n\tgetEvents(): ReadonlyArray<ReplayEvent> {\n\t\treturn structuredClone(this.events);\n\t}\n\n\tgetLedger(): Readonly<ReplayLedger> {\n\t\treturn structuredClone({\n\t\t\tgoalId: this.goalId,\n\t\t\tevents: this.events,\n\t\t\tledgerPath: this.ledgerPath,\n\t\t\tlastPersistedSeq: this.committedHead.lastSeq,\n\t\t});\n\t}\n\n\treplay<T>(handler: (event: ReplayEvent) => T | undefined): T[] {\n\t\treturn this.events.map(handler).filter((result): result is T => result !== undefined);\n\t}\n\n\texportToFile(path: string): void {\n\t\tensureDir(path);\n\t\twriteFileSync(path, JSON.stringify(this.events, null, 2), \"utf8\");\n\t}\n}\n\n// ============================================================================\n// EvidenceGate\n// ============================================================================\n\nexport interface EvidenceGateOptions {\n\t/** Minimum number of satisfied evidence items required. */\n\tminEvidenceCount?: number;\n\t/** Require at least one artifact with a SHA-256 hash in explicit legacy mode. */\n\trequireHash?: boolean;\n\t/** Require at least one verification command in explicit legacy mode. */\n\trequireVerificationCommand?: boolean;\n\t/** Receipt policy. Defaults to prefer; legacy must be selected explicitly. */\n\treceiptMode?: EvidenceReceiptMode;\n\t/** Resolve a serialized or decoded receipt by ID. The gate validates the returned value. */\n\tresolveReceipt?: (receiptId: string) => unknown;\n\t/** Strict source: one expected-head, file-identity, suffix, and chain-verified snapshot. */\n\tresolveVerifiedLedgerSnapshot?: () => VerifiedReplayLedgerSnapshot;\n\t/** Prefer-mode migration source; strict mode requires resolveVerifiedLedgerSnapshot. */\n\tresolveLedgerEvent?: (seq: number) => ReplayEvent | undefined;\n\t/** Recapture the receipt's workspace scope (same kind and scope) for freshness comparison. */\n\tcaptureWorkspaceFingerprint?: (scope: WorkspaceScope) => unknown;\n\t/**\n\t * Optional freshness source: the latest replay-ledger sequence of a workspace mutation\n\t * relevant to the given scope, or null/undefined when none is known. When configured,\n\t * the receipt's verified ledger seq must strictly exceed this value; a throwing or\n\t * out-of-contract source is a hard block (positive safe integers only).\n\t */\n\tresolveLatestWorkspaceMutationSeq?: (scope: WorkspaceScope) => number | null | undefined;\n\t/** Ephemeral trust anchor used to verify the original command's keyed attestation. */\n\tcommandAttestationBinder?: Pick<CommandHmacBinder, \"verify\">;\n\t/** Resolve the original command inside the same trust boundary as the binder. */\n\tresolveAttestedCommand?: (receiptId: string) => EvidenceCommandDescriptor | undefined;\n}\n\ninterface ReceiptCheckIssue {\n\treadonly message: string;\n\treadonly hard: boolean;\n}\n\nfunction receiptCheckIssue(message: string, hard: boolean): ReceiptCheckIssue {\n\treturn { message, hard };\n}\n\nfunction receiptIssue(\n\titem: EvidenceItem,\n\tcontract: TaskContract,\n\toptions: EvidenceGateOptions,\n): ReceiptCheckIssue | undefined {\n\tif (!item.receiptId) {\n\t\treturn receiptCheckIssue(`Execution receipt is missing for evidence \"${item.claim}\".`, false);\n\t}\n\tconst strict = options.receiptMode === \"strict\";\n\tif (item.receiptSchemaVersion !== 3 && (item.receiptSchemaVersion !== undefined || strict)) {\n\t\treturn receiptCheckIssue(`Execution receipt schema metadata is invalid for evidence \"${item.claim}\".`, true);\n\t}\n\tif (!options.resolveReceipt) {\n\t\treturn receiptCheckIssue(`Execution receipt resolver is missing for evidence \"${item.claim}\".`, false);\n\t}\n\n\tlet rawReceipt: unknown;\n\ttry {\n\t\trawReceipt = options.resolveReceipt(item.receiptId);\n\t} catch {\n\t\treturn receiptCheckIssue(`Execution receipt resolver failed for evidence \"${item.claim}\".`, true);\n\t}\n\tif (rawReceipt === undefined) {\n\t\treturn receiptCheckIssue(`Execution receipt is missing for evidence \"${item.claim}\".`, false);\n\t}\n\n\ttry {\n\t\tconst receipt = validateEvidenceReceipt(rawReceipt);\n\t\tif (receipt.core.receiptId !== item.receiptId) {\n\t\t\treturn receiptCheckIssue(`Execution receipt ID does not match evidence \"${item.claim}\".`, true);\n\t\t}\n\t\tif (receipt.core.goalId !== contract.goalId) {\n\t\t\treturn receiptCheckIssue(`Execution receipt goal does not match evidence \"${item.claim}\".`, true);\n\t\t}\n\t\tif (receipt.core.claim !== item.claim) {\n\t\t\treturn receiptCheckIssue(`Execution receipt claim does not match evidence \"${item.claim}\".`, true);\n\t\t}\n\t\tif (receipt.core.status !== \"passed\" || receipt.core.exitCode !== 0) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt for evidence \"${item.claim}\" did not pass with exit code 0.`,\n\t\t\t\ttrue,\n\t\t\t);\n\t\t}\n\n\t\tif (item.receiptCommandSha256 === undefined) {\n\t\t\treturn receiptCheckIssue(`Execution receipt command binding is missing for evidence \"${item.claim}\".`, false);\n\t\t}\n\t\tconst receiptCommandSha256 = computeEvidenceCommandSha256(receipt.core.command);\n\t\tif (!constantTimeSha256Equal(item.receiptCommandSha256, receiptCommandSha256)) {\n\t\t\treturn receiptCheckIssue(`Execution receipt command does not match evidence \"${item.claim}\".`, true);\n\t\t}\n\t\tconst attestationIssue = verifyCommandAttestation({\n\t\t\treceiptId: receipt.core.receiptId,\n\t\t\tbinding: receipt.core.commandBinding,\n\t\t\tpersistedCommand: receipt.core.command,\n\t\t\tpersistedSummary: receipt.core.commandRedaction,\n\t\t\tbinder: options.commandAttestationBinder,\n\t\t\tresolveCommand: options.resolveAttestedCommand,\n\t\t\tredact: redactCommandDescriptor,\n\t\t});\n\t\tif (attestationIssue !== undefined) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt command attestation ${attestationIssue.detail} for evidence \"${item.claim}\".`,\n\t\t\t\tattestationIssue.hard,\n\t\t\t);\n\t\t}\n\n\t\tif (receipt.core.laneId !== undefined && item.receiptLaneId === undefined) {\n\t\t\treturn receiptCheckIssue(`Execution receipt lane binding is missing for evidence \"${item.claim}\".`, false);\n\t\t}\n\t\tif (item.receiptLaneId !== receipt.core.laneId) {\n\t\t\treturn receiptCheckIssue(`Execution receipt lane does not match evidence \"${item.claim}\".`, true);\n\t\t}\n\n\t\tconst workspaceIssue = verifyWorkspaceBinding(receipt.core.workspaceAfter, options.captureWorkspaceFingerprint);\n\t\tif (workspaceIssue !== undefined) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt ${workspaceIssue.detail} for evidence \"${item.claim}\".`,\n\t\t\t\tworkspaceIssue.hard,\n\t\t\t);\n\t\t}\n\n\t\tlet snapshot: VerifiedReplayLedgerSnapshot | undefined;\n\t\tif (options.resolveVerifiedLedgerSnapshot !== undefined) {\n\t\t\ttry {\n\t\t\t\tsnapshot = parseVerifiedLedgerSnapshot(\n\t\t\t\t\toptions.resolveVerifiedLedgerSnapshot(),\n\t\t\t\t\tcontract.goalId,\n\t\t\t\t\tparseReplayLedgerBytes,\n\t\t\t\t);\n\t\t\t} catch {\n\t\t\t\treturn receiptCheckIssue(\n\t\t\t\t\t`Execution receipt verified ledger snapshot failed for evidence \"${item.claim}\".`,\n\t\t\t\t\ttrue,\n\t\t\t\t);\n\t\t\t}\n\t\t} else if (strict) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt verified ledger snapshot is missing for evidence \"${item.claim}\".`,\n\t\t\t\tfalse,\n\t\t\t);\n\t\t}\n\t\tlet latestMutationSeq =\n\t\t\tsnapshot === undefined\n\t\t\t\t? null\n\t\t\t\t: latestRelevantWorkspaceMutationSeq(snapshot.events, receipt.core.workspaceAfter.scope);\n\t\tif (snapshot === undefined && options.resolveLatestWorkspaceMutationSeq !== undefined) {\n\t\t\tlet resolved: unknown;\n\t\t\ttry {\n\t\t\t\tresolved = options.resolveLatestWorkspaceMutationSeq(receipt.core.workspaceAfter.scope);\n\t\t\t} catch {\n\t\t\t\treturn receiptCheckIssue(\n\t\t\t\t\t`Execution receipt workspace-mutation freshness source failed for evidence \"${item.claim}\".`,\n\t\t\t\t\ttrue,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (resolved !== null && resolved !== undefined) {\n\t\t\t\tif (!Number.isSafeInteger(resolved) || (resolved as number) <= 0) {\n\t\t\t\t\treturn receiptCheckIssue(\n\t\t\t\t\t\t`Execution receipt workspace-mutation freshness source is malformed for evidence \"${item.claim}\".`,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tlatestMutationSeq = resolved as number;\n\t\t\t}\n\t\t}\n\n\t\tconst binding = receipt.envelope.ledgerBinding;\n\t\tif (!binding) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt ledger binding is missing for evidence \"${item.claim}\".`,\n\t\t\t\tlatestMutationSeq !== null,\n\t\t\t);\n\t\t}\n\t\tconst event = snapshot\n\t\t\t? snapshot.events.find((candidate) => candidate.seq === binding.seq)\n\t\t\t: options.resolveLedgerEvent?.(binding.seq);\n\t\tif (!event && snapshot === undefined && !options.resolveLedgerEvent) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt ledger resolver is missing for evidence \"${item.claim}\".`,\n\t\t\t\tlatestMutationSeq !== null,\n\t\t\t);\n\t\t}\n\t\tconst replayPayload = evidenceReceiptReplayPayload(receipt);\n\t\tif (\n\t\t\t!event ||\n\t\t\tevent.seq !== binding.seq ||\n\t\t\tevent.type !== \"evidence_receipt\" ||\n\t\t\tevent.goalId !== contract.goalId ||\n\t\t\tevent.laneId !== receipt.core.laneId ||\n\t\t\t!constantTimeSha256Equal(event.eventHash, binding.eventHash) ||\n\t\t\t!replayPayloadMatches(event.payload, replayPayload.receiptId, replayPayload.coreSha256)\n\t\t) {\n\t\t\treturn receiptCheckIssue(`Execution receipt ledger binding does not match evidence \"${item.claim}\".`, true);\n\t\t}\n\t\tif (latestMutationSeq !== null && event.seq <= latestMutationSeq) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt is stale: workspace mutation seq ${latestMutationSeq} does not precede receipt ledger seq ${event.seq} for evidence \"${item.claim}\".`,\n\t\t\t\ttrue,\n\t\t\t);\n\t\t}\n\t} catch {\n\t\treturn receiptCheckIssue(`Execution receipt is invalid for evidence \"${item.claim}\".`, true);\n\t}\n\treturn undefined;\n}\n\nexport class EvidenceGate {\n\tprivate options: EvidenceGateOptions;\n\n\tconstructor(options: EvidenceGateOptions = {}) {\n\t\tthis.options = {\n\t\t\tminEvidenceCount: 1,\n\t\t\trequireHash: true,\n\t\t\trequireVerificationCommand: true,\n\t\t\treceiptMode: \"prefer\",\n\t\t\t...options,\n\t\t};\n\t}\n\n\tcheck(contract: TaskContract): MergeGateResult {\n\t\tconst evidence = contract.requiredEvidence;\n\t\tconst satisfied = evidence.filter((e) => e.status === \"satisfied\");\n\t\tconst failed = evidence.filter((e) => e.status === \"failed\");\n\t\tconst pending = evidence.filter((e) => e.status === \"pending\" || e.status === \"gathering\");\n\t\tconst mode = this.options.receiptMode ?? \"prefer\";\n\t\tconst checks: string[] = [];\n\t\tlet hardReceiptFailure = false;\n\n\t\tif (satisfied.length < (this.options.minEvidenceCount ?? 1)) {\n\t\t\tchecks.push(`Only ${satisfied.length}/${this.options.minEvidenceCount} required evidence items satisfied.`);\n\t\t}\n\t\tif (failed.length > 0) {\n\t\t\tchecks.push(`${failed.length} evidence item(s) failed.`);\n\t\t}\n\n\t\tif (mode === \"legacy\") {\n\t\t\tif (this.options.requireHash) {\n\t\t\t\tconst hashed = satisfied.filter((e) => e.hash && e.hash.length > 0);\n\t\t\t\tif (hashed.length === 0) checks.push(\"No satisfied evidence has a SHA-256 hash.\");\n\t\t\t}\n\t\t\tif (this.options.requireVerificationCommand) {\n\t\t\t\tconst withCommand = satisfied.filter((e) => e.verificationCommand && e.verificationCommand.length > 0);\n\t\t\t\tif (withCommand.length === 0) checks.push(\"No satisfied evidence has a verification command.\");\n\t\t\t}\n\t\t} else {\n\t\t\tconst unsatisfied = evidence.filter((item) => item.status !== \"satisfied\");\n\t\t\tif (unsatisfied.length > 0) {\n\t\t\t\tchecks.push(`${unsatisfied.length} required evidence item(s) are not satisfied.`);\n\t\t\t}\n\t\t\tfor (const item of satisfied) {\n\t\t\t\tconst issue = receiptIssue(item, contract, this.options);\n\t\t\t\tif (issue) {\n\t\t\t\t\tchecks.push(issue.message);\n\t\t\t\t\thardReceiptFailure ||= issue.hard;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (contract.verdict === \"fail\" || (mode !== \"legacy\" && contract.verdict !== \"pass\")) {\n\t\t\tchecks.push(`Task contract verdict is '${contract.verdict}'.`);\n\t\t}\n\n\t\tlet status: MergeGateStatus;\n\t\tlet reason: string;\n\t\tlet suggestion: string | undefined;\n\t\tif (checks.length === 0) {\n\t\t\tstatus = \"open\";\n\t\t\treason = `Evidence gate passed: ${satisfied.length}/${evidence.length} evidence items satisfied.`;\n\t\t} else {\n\t\t\tconst contractFailure = mode === \"legacy\" ? contract.verdict === \"fail\" : contract.verdict !== \"pass\";\n\t\t\tconst hasHardFailure = failed.length > 0 || contractFailure || hardReceiptFailure || mode === \"strict\";\n\t\t\tstatus = hasHardFailure ? \"blocked\" : mode === \"prefer\" || pending.length > 0 ? \"conditional\" : \"blocked\";\n\t\t\treason = checks.join(\"; \");\n\t\t\tsuggestion = `Gather remaining evidence (${pending.length} pending) and re-run verification.`;\n\t\t}\n\n\t\treturn {\n\t\t\tgateId: \"evidence-gate\",\n\t\t\tstatus,\n\t\t\treason,\n\t\t\tsuggestion,\n\t\t\tevidenceChecked: evidence,\n\t\t};\n\t}\n}\n\n// ============================================================================\n// FailClosedMergeGate\n// ============================================================================\n\nexport class FailClosedMergeGate {\n\tprivate gates: EvidenceGate[];\n\n\tconstructor(gates: EvidenceGate[] = [new EvidenceGate()]) {\n\t\tthis.gates = gates;\n\t}\n\n\tcheck(contract: TaskContract): MergeGateResult {\n\t\tconst results = this.gates.map((gate) => gate.check(contract));\n\t\tconst blocked = results.filter((r) => r.status === \"blocked\");\n\t\tconst conditional = results.filter((r) => r.status === \"conditional\");\n\n\t\tif (blocked.length > 0) {\n\t\t\treturn {\n\t\t\t\tgateId: \"fail-closed-merge-gate\",\n\t\t\t\tstatus: \"blocked\",\n\t\t\t\treason: `Blocked by ${blocked.length} gate(s): ${blocked.map((b) => b.reason).join(\"; \")}`,\n\t\t\t\tsuggestion: \"Resolve all blocking conditions before merge.\",\n\t\t\t\tevidenceChecked: contract.requiredEvidence,\n\t\t\t};\n\t\t}\n\n\t\tif (conditional.length > 0) {\n\t\t\treturn {\n\t\t\t\tgateId: \"fail-closed-merge-gate\",\n\t\t\t\tstatus: \"conditional\",\n\t\t\t\treason: `Conditional pass: ${conditional.length} gate(s) have pending conditions.`,\n\t\t\t\tsuggestion: \"Complete pending evidence or waive with explicit approval.\",\n\t\t\t\tevidenceChecked: contract.requiredEvidence,\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tgateId: \"fail-closed-merge-gate\",\n\t\t\tstatus: \"open\",\n\t\t\treason: \"All merge gates passed. Evidence verified and contract verdict is acceptable.\",\n\t\t\tevidenceChecked: contract.requiredEvidence,\n\t\t};\n\t}\n}\n\n// ============================================================================\n// Verify Reporter v2\n// ============================================================================\n\n/** Escape pipes/newlines so untrusted claim or command text cannot restructure the report table. */\nfunction escapeTableCell(value: string): string {\n\treturn value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\\|/g, \"\\\\|\").replace(/\\r?\\n/g, \" \");\n}\n\nexport interface VerifyReporterV2Options {\n\toutputDir: string;\n\tgoalId: string;\n}\n\nexport class VerifyReporterV2 {\n\tprivate options: VerifyReporterV2Options;\n\n\tconstructor(options: VerifyReporterV2Options) {\n\t\tthis.options = options;\n\t}\n\n\trender(contract: TaskContract, mergeGate: MergeGateResult): string {\n\t\tconst lines: string[] = [\n\t\t\t`## OMK Verification Report v2 — ${contract.goalId}`,\n\t\t\t\"\",\n\t\t\t\"### Contract\",\n\t\t\t\"\",\n\t\t\t`| Field | Value |`,\n\t\t\t`|-------|-------|`,\n\t\t\t`| Claim | ${escapeTableCell(contract.completionClaim)} |`,\n\t\t\t`| Verdict | ${contract.verdict} |`,\n\t\t\t`| Final Risk | ${escapeTableCell(contract.finalRisk)} |`,\n\t\t\t\"\",\n\t\t\t\"### Evidence\",\n\t\t\t\"\",\n\t\t\t`| Claim | Status | Artifact | Command |`,\n\t\t\t`|-------|--------|----------|----------|`,\n\t\t];\n\n\t\tfor (const ev of contract.requiredEvidence) {\n\t\t\tlines.push(\n\t\t\t\t`| ${escapeTableCell(ev.claim)} | ${ev.status} | ${escapeTableCell(ev.artifactPath ?? \"—\")} | ${escapeTableCell(ev.verificationCommand ?? \"—\")} |`,\n\t\t\t);\n\t\t}\n\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t\"### Merge Gate\",\n\t\t\t\"\",\n\t\t\t`| Gate | Status | Reason |`,\n\t\t\t`|------|--------|--------|`,\n\t\t\t`| ${mergeGate.gateId} | ${mergeGate.status} | ${escapeTableCell(mergeGate.reason)} |`,\n\t\t\t\"\",\n\t\t);\n\n\t\tif (mergeGate.suggestion) {\n\t\t\tlines.push(`**Suggestion:** ${mergeGate.suggestion}`, \"\");\n\t\t}\n\n\t\treturn lines.join(\"\\n\");\n\t}\n\n\twrite(contract: TaskContract, mergeGate: MergeGateResult): string {\n\t\tconst markdown = this.render(contract, mergeGate);\n\t\tconst path = join(this.options.outputDir, `${this.options.goalId}.verify.md`);\n\t\tensureDir(path);\n\t\twriteFileSync(path, markdown, \"utf-8\");\n\t\treturn path;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"evidence-system.d.ts","sourceRoot":"","sources":["../../src/guardrails/evidence-system.ts"],"names":[],"mappings":"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","sourcesContent":["// allow: SIZE_OK - evidence ledger guardrails: tamper-evident replay chain + fail-closed parsing.\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type {\n\tEvidenceCommandDescriptor,\n\tEvidenceItem,\n\tEvidenceReceiptMode,\n\tEvidenceStatus,\n\tMergeGateResult,\n\tMergeGateStatus,\n\tReplayEvent,\n\tReplayLedger,\n\tReplayLedgerHead,\n\tTaskContract,\n\tVerifiedReplayLedgerSnapshot,\n\tWorkspaceScope,\n} from \"../types/evidence.ts\";\nimport { redactCommandDescriptor } from \"./command-redaction.ts\";\nimport {\n\ttype CommandHmacBinder,\n\tlatestRelevantWorkspaceMutationSeq,\n\tparseVerifiedLedgerSnapshot,\n\treplayPayloadMatches,\n\tverifyCommandAttestation,\n\tverifyWorkspaceBinding,\n} from \"./evidence-attestation.ts\";\n\nexport { latestRelevantWorkspaceMutationSeq } from \"./evidence-attestation.ts\";\n\nimport {\n\tcomputeEvidenceCommandSha256,\n\tconstantTimeSha256Equal,\n\tevidenceReceiptReplayPayload,\n\tparseSha256Hex,\n\tvalidateEvidenceReceipt,\n} from \"./evidence-receipt.ts\";\nimport { combineMergeGateResults } from \"./merge-gate-result.ts\";\nimport { ReplayLedgerStore, replayLedgerHeadsEqual } from \"./replay-ledger-store.ts\";\nimport {\n\tcomputeReplayPayloadHash,\n\tJCS_REPLAY_PAYLOAD_HASH_ALGORITHM,\n\tLEGACY_REPLAY_PAYLOAD_HASH_ALGORITHM,\n} from \"./replay-payload-hash.ts\";\n\nfunction sha256(input: string): string {\n\treturn createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nfunction ensureDir(path: string): void {\n\tconst dir = dirname(path);\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n}\n\n// ============================================================================\n// TaskContract\n// ============================================================================\n\nexport class TaskContractBuilder {\n\tprivate goalId: string;\n\tprivate completionClaim: string = \"\";\n\tprivate requiredEvidence: EvidenceItem[] = [];\n\tprivate finalRisk: string = \"\";\n\tprivate verdict: \"pass\" | \"fail\" | \"conditional\" = \"fail\";\n\tprivate createdAt: string;\n\tprivate updatedAt: string;\n\n\tconstructor(goalId: string) {\n\t\tthis.goalId = goalId;\n\t\tconst now = new Date().toISOString();\n\t\tthis.createdAt = now;\n\t\tthis.updatedAt = now;\n\t}\n\n\tsetClaim(claim: string): this {\n\t\tthis.completionClaim = claim;\n\t\tthis.touch();\n\t\treturn this;\n\t}\n\n\taddRequiredEvidence(evidence: Omit<EvidenceItem, \"timestamp\" | \"status\">): this {\n\t\tthis.requiredEvidence.push({\n\t\t\t...evidence,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tstatus: \"pending\",\n\t\t});\n\t\tthis.touch();\n\t\treturn this;\n\t}\n\n\tsetFinalRisk(risk: string): this {\n\t\tthis.finalRisk = risk;\n\t\tthis.touch();\n\t\treturn this;\n\t}\n\n\t/** @deprecated Use `evaluateTask()` from `omk-protocol`; verdicts are derived, not mutated. */\n\tsetVerdict(verdict: \"pass\" | \"fail\" | \"conditional\"): this {\n\t\tthis.verdict = verdict;\n\t\tthis.touch();\n\t\treturn this;\n\t}\n\n\t/** @deprecated Append a protocol Observation and re-evaluate instead of mutating evidence state. */\n\tupdateEvidenceStatus(claim: string, status: EvidenceStatus, gapReason?: string): this {\n\t\tconst item = this.requiredEvidence.find((e) => e.claim === claim);\n\t\tif (item) {\n\t\t\titem.status = status;\n\t\t\titem.gapReason = gapReason;\n\t\t\titem.timestamp = new Date().toISOString();\n\t\t\tthis.touch();\n\t\t}\n\t\treturn this;\n\t}\n\n\tprivate touch(): void {\n\t\tthis.updatedAt = new Date().toISOString();\n\t}\n\n\tbuild(): TaskContract {\n\t\treturn structuredClone({\n\t\t\tgoalId: this.goalId,\n\t\t\tcompletionClaim: this.completionClaim,\n\t\t\trequiredEvidence: this.requiredEvidence,\n\t\t\tfinalRisk: this.finalRisk,\n\t\t\tverdict: this.verdict,\n\t\t\tcreatedAt: this.createdAt,\n\t\t\tupdatedAt: this.updatedAt,\n\t\t});\n\t}\n\n\t/** Parse and validate a serialized TaskContract. Fails closed on any shape violation. */\n\tstatic fromJSON(json: string): TaskContract {\n\t\tlet raw: unknown;\n\t\ttry {\n\t\t\traw = JSON.parse(json);\n\t\t} catch (error) {\n\t\t\tif (error instanceof SyntaxError) throw new SyntaxError(\"Invalid TaskContract JSON\");\n\t\t\tthrow error;\n\t\t}\n\t\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) {\n\t\t\tthrow new Error(\"TaskContract JSON must be an object\");\n\t\t}\n\t\tconst { goalId, completionClaim, finalRisk, verdict, createdAt, updatedAt, requiredEvidence } = raw as Record<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>;\n\t\tif (\n\t\t\ttypeof goalId !== \"string\" ||\n\t\t\ttypeof completionClaim !== \"string\" ||\n\t\t\ttypeof finalRisk !== \"string\" ||\n\t\t\ttypeof createdAt !== \"string\" ||\n\t\t\ttypeof updatedAt !== \"string\"\n\t\t) {\n\t\t\tthrow new Error(\"TaskContract JSON has missing or mistyped string fields\");\n\t\t}\n\t\tif (verdict !== \"pass\" && verdict !== \"fail\" && verdict !== \"conditional\") {\n\t\t\tthrow new Error(`TaskContract verdict must be pass|fail|conditional, got ${String(verdict)}`);\n\t\t}\n\t\tif (!Array.isArray(requiredEvidence)) {\n\t\t\tthrow new Error(\"TaskContract requiredEvidence must be an array\");\n\t\t}\n\t\treturn {\n\t\t\tgoalId,\n\t\t\tcompletionClaim,\n\t\t\tfinalRisk,\n\t\t\tverdict,\n\t\t\tcreatedAt,\n\t\t\tupdatedAt,\n\t\t\trequiredEvidence: requiredEvidence.map((item, index) => parseEvidenceItem(item, index)),\n\t\t};\n\t}\n\n\tstatic toJSON(contract: TaskContract): string {\n\t\treturn JSON.stringify(contract, null, 2);\n\t}\n}\n\nconst EVIDENCE_STATUSES: ReadonlySet<string> = new Set([\"pending\", \"gathering\", \"satisfied\", \"failed\", \"waived\"]);\n\nfunction optionalStringField(value: unknown, field: string, index: number): string | undefined {\n\tif (value !== undefined && typeof value !== \"string\") {\n\t\tthrow new Error(`TaskContract evidence[${index}].${field} must be a string when present`);\n\t}\n\treturn value;\n}\n\nfunction parseEvidenceItem(raw: unknown, index: number): EvidenceItem {\n\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) {\n\t\tthrow new Error(`TaskContract evidence[${index}] must be an object`);\n\t}\n\tconst item = raw as Record<string, unknown>;\n\tconst { claim, category, timestamp, status } = item;\n\tif (typeof claim !== \"string\" || typeof category !== \"string\" || typeof timestamp !== \"string\") {\n\t\tthrow new Error(`TaskContract evidence[${index}] has missing or mistyped fields`);\n\t}\n\tif (typeof status !== \"string\" || !EVIDENCE_STATUSES.has(status)) {\n\t\tthrow new Error(`TaskContract evidence[${index}] has invalid status ${String(status)}`);\n\t}\n\tconst receiptSchemaVersion = item.receiptSchemaVersion;\n\tif (receiptSchemaVersion !== undefined && receiptSchemaVersion !== 3) {\n\t\tthrow new Error(`TaskContract evidence[${index}].receiptSchemaVersion must be 3 when present`);\n\t}\n\tconst receiptCommandSha256 = optionalStringField(item.receiptCommandSha256, \"receiptCommandSha256\", index);\n\treturn {\n\t\tclaim,\n\t\tcategory: category as EvidenceItem[\"category\"],\n\t\ttimestamp,\n\t\tstatus: status as EvidenceStatus,\n\t\tartifactPath: optionalStringField(item.artifactPath, \"artifactPath\", index),\n\t\tverificationCommand: optionalStringField(item.verificationCommand, \"verificationCommand\", index),\n\t\thash: optionalStringField(item.hash, \"hash\", index),\n\t\treceiptId: optionalStringField(item.receiptId, \"receiptId\", index),\n\t\treceiptSchemaVersion,\n\t\t...(receiptCommandSha256 !== undefined\n\t\t\t? { receiptCommandSha256: parseSha256Hex(receiptCommandSha256, \"receiptCommandSha256\") }\n\t\t\t: {}),\n\t\treceiptLaneId: optionalStringField(item.receiptLaneId, \"receiptLaneId\", index),\n\t\tgapReason: optionalStringField(item.gapReason, \"gapReason\", index),\n\t};\n}\n\n// ============================================================================\n// ReplayLedger\n// ============================================================================\n\n/** prevHash sentinel for the first event in a replay chain. */\nconst GENESIS_HASH = \"genesis\";\n\nfunction computeEventHash(event: Omit<ReplayEvent, \"eventHash\">): string {\n\tconst metadata = [event.seq, event.type, event.timestamp, event.goalId, event.laneId ?? null];\n\tconst material =\n\t\tevent.payloadHashAlgorithm === undefined\n\t\t\t? [...metadata, event.payloadHash, event.prevHash]\n\t\t\t: [...metadata, event.payloadHashAlgorithm, event.payloadHash, event.prevHash];\n\treturn sha256(JSON.stringify(material));\n}\n\nfunction parseReplayEventLine(line: string, lineNumber: number): ReplayEvent {\n\tlet raw: unknown;\n\ttry {\n\t\traw = JSON.parse(line);\n\t} catch {\n\t\tthrow new Error(`Replay ledger corrupted at line ${lineNumber}: invalid JSON`);\n\t}\n\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) {\n\t\tthrow new Error(`Replay ledger corrupted at line ${lineNumber}: event must be an object`);\n\t}\n\tconst event = raw as Record<string, unknown>;\n\tconst { seq, type, timestamp, goalId, laneId, payloadHashAlgorithm, payloadHash, prevHash, eventHash } = event;\n\tif (\n\t\ttypeof seq !== \"number\" ||\n\t\ttypeof type !== \"string\" ||\n\t\ttypeof timestamp !== \"string\" ||\n\t\ttypeof goalId !== \"string\" ||\n\t\ttypeof payloadHash !== \"string\" ||\n\t\ttypeof prevHash !== \"string\" ||\n\t\ttypeof eventHash !== \"string\" ||\n\t\t(laneId !== undefined && typeof laneId !== \"string\") ||\n\t\t!(\"payload\" in event)\n\t) {\n\t\tthrow new Error(`Replay ledger corrupted at line ${lineNumber}: event fields are missing or mistyped`);\n\t}\n\tif (\n\t\tpayloadHashAlgorithm !== undefined &&\n\t\tpayloadHashAlgorithm !== LEGACY_REPLAY_PAYLOAD_HASH_ALGORITHM &&\n\t\tpayloadHashAlgorithm !== JCS_REPLAY_PAYLOAD_HASH_ALGORITHM\n\t) {\n\t\tthrow new Error(`Replay ledger corrupted at line ${lineNumber}: unsupported payload hash algorithm`);\n\t}\n\tconst parsed: ReplayEvent = {\n\t\tseq,\n\t\ttype: type as ReplayEvent[\"type\"],\n\t\ttimestamp,\n\t\tgoalId,\n\t\tlaneId,\n\t\tpayload: event.payload,\n\t\tpayloadHash,\n\t\tprevHash,\n\t\teventHash,\n\t};\n\treturn payloadHashAlgorithm === undefined ? parsed : { ...parsed, payloadHashAlgorithm };\n}\n\nfunction parseReplayLedgerBytes(bytes: Buffer, goalId: string): ReplayEvent[] {\n\tconst lines = bytes\n\t\t.toString(\"utf8\")\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.trim().length > 0);\n\tconst events: ReplayEvent[] = [];\n\tlet prevHash = GENESIS_HASH;\n\tfor (let index = 0; index < lines.length; index++) {\n\t\tconst event = parseReplayEventLine(lines[index], index + 1);\n\t\tif (event.goalId !== goalId) throw new Error(`Replay ledger goal mismatch at line ${index + 1}`);\n\t\tif (event.seq !== index + 1) {\n\t\t\tthrow new Error(`Replay ledger corrupted at line ${index + 1}: expected seq ${index + 1}, got ${event.seq}`);\n\t\t}\n\t\tif (event.prevHash !== prevHash) throw new Error(`Replay ledger chain broken at line ${index + 1}`);\n\t\tconst payloadHashAlgorithm = event.payloadHashAlgorithm ?? LEGACY_REPLAY_PAYLOAD_HASH_ALGORITHM;\n\t\tif (computeReplayPayloadHash(event.payload, payloadHashAlgorithm) !== event.payloadHash) {\n\t\t\tthrow new Error(`Replay ledger payload tampered at line ${index + 1} (seq ${event.seq})`);\n\t\t}\n\t\tif (computeEventHash(event) !== event.eventHash) {\n\t\t\tthrow new Error(`Replay ledger event hash mismatch at line ${index + 1} (seq ${event.seq})`);\n\t\t}\n\t\tprevHash = event.eventHash;\n\t\tevents.push(event);\n\t}\n\treturn events;\n}\n\nexport class ReplayLedgerManager {\n\tprivate readonly goalId: string;\n\tprivate readonly ledgerPath: string;\n\tprivate readonly store: ReplayLedgerStore;\n\tprivate events: ReplayEvent[] = [];\n\tprivate committedHead: ReplayLedgerHead;\n\n\tconstructor(goalId: string, ledgerPath: string, expectedHead?: ReplayLedgerHead) {\n\t\tthis.goalId = goalId;\n\t\tthis.ledgerPath = ledgerPath;\n\t\tthis.committedHead = { fileIdentity: null, size: 0, lastSeq: 0, lastHash: GENESIS_HASH };\n\t\tthis.store = new ReplayLedgerStore(ledgerPath, (bytes) => {\n\t\t\tconst events = parseReplayLedgerBytes(bytes, goalId);\n\t\t\tconst last = events[events.length - 1];\n\t\t\treturn { lastSeq: last?.seq ?? 0, lastHash: last?.eventHash ?? GENESIS_HASH };\n\t\t});\n\t\tthis.refresh(expectedHead);\n\t}\n\n\tprivate refresh(expectedHead?: ReplayLedgerHead): void {\n\t\tconst snapshot = this.store.load(expectedHead);\n\t\tthis.events = parseReplayLedgerBytes(snapshot.bytes, this.goalId);\n\t\tthis.committedHead = snapshot.head;\n\t}\n\n\tappend(\n\t\tevent: Omit<ReplayEvent, \"seq\" | \"timestamp\" | \"payloadHashAlgorithm\" | \"payloadHash\" | \"prevHash\" | \"eventHash\">,\n\t\texpectedHead: ReplayLedgerHead = this.committedHead,\n\t): ReplayEvent {\n\t\tif (event.goalId !== this.goalId) throw new Error(\"Replay event goalId does not match the ledger\");\n\t\tif (!replayLedgerHeadsEqual(expectedHead, this.committedHead)) {\n\t\t\tthrow new Error(\"Replay manager expected-head CAS is stale\");\n\t\t}\n\t\tconst payloadHashAlgorithm = JCS_REPLAY_PAYLOAD_HASH_ALGORITHM;\n\t\tconst payloadHash = computeReplayPayloadHash(event.payload, payloadHashAlgorithm);\n\t\tconst chained: Omit<ReplayEvent, \"eventHash\"> = {\n\t\t\t...event,\n\t\t\tseq: expectedHead.lastSeq + 1,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tpayloadHashAlgorithm,\n\t\t\tpayloadHash,\n\t\t\tprevHash: expectedHead.lastHash,\n\t\t};\n\t\tconst fullEvent: ReplayEvent = { ...chained, eventHash: computeEventHash(chained) };\n\t\tthis.committedHead = this.store.append(\n\t\t\tBuffer.from(`${JSON.stringify(fullEvent)}\\n`, \"utf8\"),\n\t\t\tfullEvent.seq,\n\t\t\tfullEvent.eventHash,\n\t\t\texpectedHead,\n\t\t);\n\t\tthis.events.push(fullEvent);\n\t\treturn structuredClone(fullEvent);\n\t}\n\n\t/** Compatibility no-op that still verifies the caller's expected committed head. */\n\tpersist(expectedHead: ReplayLedgerHead = this.committedHead): void {\n\t\tthis.getVerifiedSnapshot(expectedHead);\n\t}\n\n\tload(expectedHead: ReplayLedgerHead = this.committedHead): void {\n\t\tthis.refresh(expectedHead);\n\t}\n\n\tgetCommittedHead(): ReplayLedgerHead {\n\t\treturn structuredClone(this.committedHead);\n\t}\n\n\tgetVerifiedSnapshot(expectedHead: ReplayLedgerHead = this.committedHead): VerifiedReplayLedgerSnapshot {\n\t\tthis.refresh(expectedHead);\n\t\treturn structuredClone({ events: this.events, head: this.committedHead });\n\t}\n\n\tgetEvents(): ReadonlyArray<ReplayEvent> {\n\t\treturn structuredClone(this.events);\n\t}\n\n\tgetLedger(): Readonly<ReplayLedger> {\n\t\treturn structuredClone({\n\t\t\tgoalId: this.goalId,\n\t\t\tevents: this.events,\n\t\t\tledgerPath: this.ledgerPath,\n\t\t\tlastPersistedSeq: this.committedHead.lastSeq,\n\t\t});\n\t}\n\n\treplay<T>(handler: (event: ReplayEvent) => T | undefined): T[] {\n\t\treturn this.events.map(handler).filter((result): result is T => result !== undefined);\n\t}\n\n\texportToFile(path: string): void {\n\t\tensureDir(path);\n\t\twriteFileSync(path, JSON.stringify(this.events, null, 2), \"utf8\");\n\t}\n}\n\n// ============================================================================\n// EvidenceGate\n// ============================================================================\n\nexport interface EvidenceGateOptions {\n\t/** Minimum number of satisfied evidence items required. */\n\tminEvidenceCount?: number;\n\t/** Require at least one artifact with a SHA-256 hash in explicit legacy mode. */\n\trequireHash?: boolean;\n\t/** Require at least one verification command in explicit legacy mode. */\n\trequireVerificationCommand?: boolean;\n\t/** Receipt policy. Defaults to prefer; legacy must be selected explicitly. */\n\treceiptMode?: EvidenceReceiptMode;\n\t/** Resolve a serialized or decoded receipt by ID. The gate validates the returned value. */\n\tresolveReceipt?: (receiptId: string) => unknown;\n\t/** Strict source: one expected-head, file-identity, suffix, and chain-verified snapshot. */\n\tresolveVerifiedLedgerSnapshot?: () => VerifiedReplayLedgerSnapshot;\n\t/** Prefer-mode migration source; strict mode requires resolveVerifiedLedgerSnapshot. */\n\tresolveLedgerEvent?: (seq: number) => ReplayEvent | undefined;\n\t/** Recapture the receipt's workspace scope (same kind and scope) for freshness comparison. */\n\tcaptureWorkspaceFingerprint?: (scope: WorkspaceScope) => unknown;\n\t/**\n\t * Optional freshness source: the latest replay-ledger sequence of a workspace mutation\n\t * relevant to the given scope, or null/undefined when none is known. When configured,\n\t * the receipt's verified ledger seq must strictly exceed this value; a throwing or\n\t * out-of-contract source is a hard block (positive safe integers only).\n\t */\n\tresolveLatestWorkspaceMutationSeq?: (scope: WorkspaceScope) => number | null | undefined;\n\t/** Ephemeral trust anchor used to verify the original command's keyed attestation. */\n\tcommandAttestationBinder?: Pick<CommandHmacBinder, \"verify\">;\n\t/** Resolve the original command inside the same trust boundary as the binder. */\n\tresolveAttestedCommand?: (receiptId: string) => EvidenceCommandDescriptor | undefined;\n}\n\ninterface ReceiptCheckIssue {\n\treadonly message: string;\n\treadonly hard: boolean;\n}\n\nfunction receiptCheckIssue(message: string, hard: boolean): ReceiptCheckIssue {\n\treturn { message, hard };\n}\n\nfunction receiptIssue(\n\titem: EvidenceItem,\n\tcontract: TaskContract,\n\toptions: EvidenceGateOptions,\n): ReceiptCheckIssue | undefined {\n\tif (!item.receiptId) {\n\t\treturn receiptCheckIssue(`Execution receipt is missing for evidence \"${item.claim}\".`, false);\n\t}\n\tconst strict = options.receiptMode === \"strict\";\n\tif (item.receiptSchemaVersion !== 3 && (item.receiptSchemaVersion !== undefined || strict)) {\n\t\treturn receiptCheckIssue(`Execution receipt schema metadata is invalid for evidence \"${item.claim}\".`, true);\n\t}\n\tif (!options.resolveReceipt) {\n\t\treturn receiptCheckIssue(`Execution receipt resolver is missing for evidence \"${item.claim}\".`, false);\n\t}\n\n\tlet rawReceipt: unknown;\n\ttry {\n\t\trawReceipt = options.resolveReceipt(item.receiptId);\n\t} catch {\n\t\treturn receiptCheckIssue(`Execution receipt resolver failed for evidence \"${item.claim}\".`, true);\n\t}\n\tif (rawReceipt === undefined) {\n\t\treturn receiptCheckIssue(`Execution receipt is missing for evidence \"${item.claim}\".`, false);\n\t}\n\n\ttry {\n\t\tconst receipt = validateEvidenceReceipt(rawReceipt);\n\t\tif (receipt.core.receiptId !== item.receiptId) {\n\t\t\treturn receiptCheckIssue(`Execution receipt ID does not match evidence \"${item.claim}\".`, true);\n\t\t}\n\t\tif (receipt.core.goalId !== contract.goalId) {\n\t\t\treturn receiptCheckIssue(`Execution receipt goal does not match evidence \"${item.claim}\".`, true);\n\t\t}\n\t\tif (receipt.core.claim !== item.claim) {\n\t\t\treturn receiptCheckIssue(`Execution receipt claim does not match evidence \"${item.claim}\".`, true);\n\t\t}\n\t\tif (receipt.core.status !== \"passed\" || receipt.core.exitCode !== 0) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt for evidence \"${item.claim}\" did not pass with exit code 0.`,\n\t\t\t\ttrue,\n\t\t\t);\n\t\t}\n\n\t\tif (item.receiptCommandSha256 === undefined) {\n\t\t\treturn receiptCheckIssue(`Execution receipt command binding is missing for evidence \"${item.claim}\".`, false);\n\t\t}\n\t\tconst receiptCommandSha256 = computeEvidenceCommandSha256(receipt.core.command);\n\t\tif (!constantTimeSha256Equal(item.receiptCommandSha256, receiptCommandSha256)) {\n\t\t\treturn receiptCheckIssue(`Execution receipt command does not match evidence \"${item.claim}\".`, true);\n\t\t}\n\t\tconst attestationIssue = verifyCommandAttestation({\n\t\t\treceiptId: receipt.core.receiptId,\n\t\t\tbinding: receipt.core.commandBinding,\n\t\t\tpersistedCommand: receipt.core.command,\n\t\t\tpersistedSummary: receipt.core.commandRedaction,\n\t\t\tbinder: options.commandAttestationBinder,\n\t\t\tresolveCommand: options.resolveAttestedCommand,\n\t\t\tredact: redactCommandDescriptor,\n\t\t});\n\t\tif (attestationIssue !== undefined) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt command attestation ${attestationIssue.detail} for evidence \"${item.claim}\".`,\n\t\t\t\tattestationIssue.hard,\n\t\t\t);\n\t\t}\n\n\t\tif (receipt.core.laneId !== undefined && item.receiptLaneId === undefined) {\n\t\t\treturn receiptCheckIssue(`Execution receipt lane binding is missing for evidence \"${item.claim}\".`, false);\n\t\t}\n\t\tif (item.receiptLaneId !== receipt.core.laneId) {\n\t\t\treturn receiptCheckIssue(`Execution receipt lane does not match evidence \"${item.claim}\".`, true);\n\t\t}\n\n\t\tconst workspaceIssue = verifyWorkspaceBinding(receipt.core.workspaceAfter, options.captureWorkspaceFingerprint);\n\t\tif (workspaceIssue !== undefined) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt ${workspaceIssue.detail} for evidence \"${item.claim}\".`,\n\t\t\t\tworkspaceIssue.hard,\n\t\t\t);\n\t\t}\n\n\t\tlet snapshot: VerifiedReplayLedgerSnapshot | undefined;\n\t\tif (options.resolveVerifiedLedgerSnapshot !== undefined) {\n\t\t\ttry {\n\t\t\t\tsnapshot = parseVerifiedLedgerSnapshot(\n\t\t\t\t\toptions.resolveVerifiedLedgerSnapshot(),\n\t\t\t\t\tcontract.goalId,\n\t\t\t\t\tparseReplayLedgerBytes,\n\t\t\t\t);\n\t\t\t} catch {\n\t\t\t\treturn receiptCheckIssue(\n\t\t\t\t\t`Execution receipt verified ledger snapshot failed for evidence \"${item.claim}\".`,\n\t\t\t\t\ttrue,\n\t\t\t\t);\n\t\t\t}\n\t\t} else if (strict) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt verified ledger snapshot is missing for evidence \"${item.claim}\".`,\n\t\t\t\tfalse,\n\t\t\t);\n\t\t}\n\t\tlet latestMutationSeq =\n\t\t\tsnapshot === undefined\n\t\t\t\t? null\n\t\t\t\t: latestRelevantWorkspaceMutationSeq(snapshot.events, receipt.core.workspaceAfter.scope);\n\t\tif (snapshot === undefined && options.resolveLatestWorkspaceMutationSeq !== undefined) {\n\t\t\tlet resolved: unknown;\n\t\t\ttry {\n\t\t\t\tresolved = options.resolveLatestWorkspaceMutationSeq(receipt.core.workspaceAfter.scope);\n\t\t\t} catch {\n\t\t\t\treturn receiptCheckIssue(\n\t\t\t\t\t`Execution receipt workspace-mutation freshness source failed for evidence \"${item.claim}\".`,\n\t\t\t\t\ttrue,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (resolved !== null && resolved !== undefined) {\n\t\t\t\tif (!Number.isSafeInteger(resolved) || (resolved as number) <= 0) {\n\t\t\t\t\treturn receiptCheckIssue(\n\t\t\t\t\t\t`Execution receipt workspace-mutation freshness source is malformed for evidence \"${item.claim}\".`,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tlatestMutationSeq = resolved as number;\n\t\t\t}\n\t\t}\n\n\t\tconst binding = receipt.envelope.ledgerBinding;\n\t\tif (!binding) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt ledger binding is missing for evidence \"${item.claim}\".`,\n\t\t\t\tlatestMutationSeq !== null,\n\t\t\t);\n\t\t}\n\t\tconst event = snapshot\n\t\t\t? snapshot.events.find((candidate) => candidate.seq === binding.seq)\n\t\t\t: options.resolveLedgerEvent?.(binding.seq);\n\t\tif (!event && snapshot === undefined && !options.resolveLedgerEvent) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt ledger resolver is missing for evidence \"${item.claim}\".`,\n\t\t\t\tlatestMutationSeq !== null,\n\t\t\t);\n\t\t}\n\t\tconst replayPayload = evidenceReceiptReplayPayload(receipt);\n\t\tif (\n\t\t\t!event ||\n\t\t\tevent.seq !== binding.seq ||\n\t\t\tevent.type !== \"evidence_receipt\" ||\n\t\t\tevent.goalId !== contract.goalId ||\n\t\t\tevent.laneId !== receipt.core.laneId ||\n\t\t\t!constantTimeSha256Equal(event.eventHash, binding.eventHash) ||\n\t\t\t!replayPayloadMatches(event.payload, replayPayload.receiptId, replayPayload.coreSha256)\n\t\t) {\n\t\t\treturn receiptCheckIssue(`Execution receipt ledger binding does not match evidence \"${item.claim}\".`, true);\n\t\t}\n\t\tif (latestMutationSeq !== null && event.seq <= latestMutationSeq) {\n\t\t\treturn receiptCheckIssue(\n\t\t\t\t`Execution receipt is stale: workspace mutation seq ${latestMutationSeq} does not precede receipt ledger seq ${event.seq} for evidence \"${item.claim}\".`,\n\t\t\t\ttrue,\n\t\t\t);\n\t\t}\n\t} catch {\n\t\treturn receiptCheckIssue(`Execution receipt is invalid for evidence \"${item.claim}\".`, true);\n\t}\n\treturn undefined;\n}\n\nexport class EvidenceGate {\n\tprivate options: EvidenceGateOptions;\n\n\tconstructor(options: EvidenceGateOptions = {}) {\n\t\tthis.options = {\n\t\t\tminEvidenceCount: 1,\n\t\t\trequireHash: true,\n\t\t\trequireVerificationCommand: true,\n\t\t\treceiptMode: \"prefer\",\n\t\t\t...options,\n\t\t};\n\t}\n\n\tcheck(contract: TaskContract): MergeGateResult {\n\t\tconst evidence = contract.requiredEvidence;\n\t\tconst satisfied = evidence.filter((e) => e.status === \"satisfied\");\n\t\tconst failed = evidence.filter((e) => e.status === \"failed\");\n\t\tconst pending = evidence.filter((e) => e.status === \"pending\" || e.status === \"gathering\");\n\t\tconst mode = this.options.receiptMode ?? \"prefer\";\n\t\tconst checks: string[] = [];\n\t\tlet hardReceiptFailure = false;\n\n\t\tif (satisfied.length < (this.options.minEvidenceCount ?? 1)) {\n\t\t\tchecks.push(`Only ${satisfied.length}/${this.options.minEvidenceCount} required evidence items satisfied.`);\n\t\t}\n\t\tif (failed.length > 0) {\n\t\t\tchecks.push(`${failed.length} evidence item(s) failed.`);\n\t\t}\n\n\t\tif (mode === \"legacy\") {\n\t\t\tif (this.options.requireHash) {\n\t\t\t\tconst hashed = satisfied.filter((e) => e.hash && e.hash.length > 0);\n\t\t\t\tif (hashed.length === 0) checks.push(\"No satisfied evidence has a SHA-256 hash.\");\n\t\t\t}\n\t\t\tif (this.options.requireVerificationCommand) {\n\t\t\t\tconst withCommand = satisfied.filter((e) => e.verificationCommand && e.verificationCommand.length > 0);\n\t\t\t\tif (withCommand.length === 0) checks.push(\"No satisfied evidence has a verification command.\");\n\t\t\t}\n\t\t} else {\n\t\t\tconst unsatisfied = evidence.filter((item) => item.status !== \"satisfied\");\n\t\t\tif (unsatisfied.length > 0) {\n\t\t\t\tchecks.push(`${unsatisfied.length} required evidence item(s) are not satisfied.`);\n\t\t\t}\n\t\t\tfor (const item of satisfied) {\n\t\t\t\tconst issue = receiptIssue(item, contract, this.options);\n\t\t\t\tif (issue) {\n\t\t\t\t\tchecks.push(issue.message);\n\t\t\t\t\thardReceiptFailure ||= issue.hard;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (contract.verdict === \"fail\" || (mode !== \"legacy\" && contract.verdict !== \"pass\")) {\n\t\t\tchecks.push(`Task contract verdict is '${contract.verdict}'.`);\n\t\t}\n\n\t\tlet status: MergeGateStatus;\n\t\tlet reason: string;\n\t\tlet suggestion: string | undefined;\n\t\tif (checks.length === 0) {\n\t\t\tstatus = \"open\";\n\t\t\treason = `Evidence gate passed: ${satisfied.length}/${evidence.length} evidence items satisfied.`;\n\t\t} else {\n\t\t\tconst contractFailure = mode === \"legacy\" ? contract.verdict === \"fail\" : contract.verdict !== \"pass\";\n\t\t\tconst hasHardFailure = failed.length > 0 || contractFailure || hardReceiptFailure || mode === \"strict\";\n\t\t\tstatus = hasHardFailure ? \"blocked\" : mode === \"prefer\" || pending.length > 0 ? \"conditional\" : \"blocked\";\n\t\t\treason = checks.join(\"; \");\n\t\t\tsuggestion = `Gather remaining evidence (${pending.length} pending) and re-run verification.`;\n\t\t}\n\n\t\treturn {\n\t\t\tgateId: \"evidence-gate\",\n\t\t\tstatus,\n\t\t\treason,\n\t\t\tsuggestion,\n\t\t\tevidenceChecked: evidence,\n\t\t};\n\t}\n}\n\n// ============================================================================\n// FailClosedMergeGate\n// ============================================================================\n\nexport class FailClosedMergeGate {\n\tprivate readonly gates: readonly EvidenceGate[];\n\n\tconstructor(gates: EvidenceGate[] = [new EvidenceGate()]) {\n\t\tif (!Array.isArray(gates) || gates.length === 0) throw new TypeError(\"At least one evidence gate is required\");\n\t\tthis.gates = Object.freeze(\n\t\t\tArray.from(gates, (gate) => {\n\t\t\t\tif (!(gate instanceof EvidenceGate)) throw new TypeError(\"Invalid evidence gate\");\n\t\t\t\treturn gate;\n\t\t\t}),\n\t\t);\n\t}\n\n\tcheck(contract: TaskContract): MergeGateResult {\n\t\treturn combineMergeGateResults(\n\t\t\tcontract,\n\t\t\tthis.gates.map((gate) => gate.check(contract)),\n\t\t);\n\t}\n}\n\n// ============================================================================\n// Verify Reporter v2\n// ============================================================================\n\n/** Escape pipes/newlines so untrusted claim or command text cannot restructure the report table. */\nfunction escapeTableCell(value: string): string {\n\treturn value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\\|/g, \"\\\\|\").replace(/\\r?\\n/g, \" \");\n}\n\nexport interface VerifyReporterV2Options {\n\toutputDir: string;\n\tgoalId: string;\n}\n\nexport class VerifyReporterV2 {\n\tprivate options: VerifyReporterV2Options;\n\n\tconstructor(options: VerifyReporterV2Options) {\n\t\tthis.options = options;\n\t}\n\n\trender(contract: TaskContract, mergeGate: MergeGateResult): string {\n\t\tconst lines: string[] = [\n\t\t\t`## OMK Verification Report v2 — ${contract.goalId}`,\n\t\t\t\"\",\n\t\t\t\"### Contract\",\n\t\t\t\"\",\n\t\t\t`| Field | Value |`,\n\t\t\t`|-------|-------|`,\n\t\t\t`| Claim | ${escapeTableCell(contract.completionClaim)} |`,\n\t\t\t`| Verdict | ${contract.verdict} |`,\n\t\t\t`| Final Risk | ${escapeTableCell(contract.finalRisk)} |`,\n\t\t\t\"\",\n\t\t\t\"### Evidence\",\n\t\t\t\"\",\n\t\t\t`| Claim | Status | Artifact | Command |`,\n\t\t\t`|-------|--------|----------|----------|`,\n\t\t];\n\n\t\tfor (const ev of contract.requiredEvidence) {\n\t\t\tlines.push(\n\t\t\t\t`| ${escapeTableCell(ev.claim)} | ${ev.status} | ${escapeTableCell(ev.artifactPath ?? \"—\")} | ${escapeTableCell(ev.verificationCommand ?? \"—\")} |`,\n\t\t\t);\n\t\t}\n\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t\"### Merge Gate\",\n\t\t\t\"\",\n\t\t\t`| Gate | Status | Reason |`,\n\t\t\t`|------|--------|--------|`,\n\t\t\t`| ${mergeGate.gateId} | ${mergeGate.status} | ${escapeTableCell(mergeGate.reason)} |`,\n\t\t\t\"\",\n\t\t);\n\n\t\tif (mergeGate.suggestion) {\n\t\t\tlines.push(`**Suggestion:** ${mergeGate.suggestion}`, \"\");\n\t\t}\n\n\t\treturn lines.join(\"\\n\");\n\t}\n\n\twrite(contract: TaskContract, mergeGate: MergeGateResult): string {\n\t\tconst markdown = this.render(contract, mergeGate);\n\t\tconst path = join(this.options.outputDir, `${this.options.goalId}.verify.md`);\n\t\tensureDir(path);\n\t\twriteFileSync(path, markdown, \"utf-8\");\n\t\treturn path;\n\t}\n}\n"]}
|
|
@@ -6,6 +6,7 @@ import { redactCommandDescriptor } from "./command-redaction.js";
|
|
|
6
6
|
import { latestRelevantWorkspaceMutationSeq, parseVerifiedLedgerSnapshot, replayPayloadMatches, verifyCommandAttestation, verifyWorkspaceBinding, } from "./evidence-attestation.js";
|
|
7
7
|
export { latestRelevantWorkspaceMutationSeq } from "./evidence-attestation.js";
|
|
8
8
|
import { computeEvidenceCommandSha256, constantTimeSha256Equal, evidenceReceiptReplayPayload, parseSha256Hex, validateEvidenceReceipt, } from "./evidence-receipt.js";
|
|
9
|
+
import { combineMergeGateResults } from "./merge-gate-result.js";
|
|
9
10
|
import { ReplayLedgerStore, replayLedgerHeadsEqual } from "./replay-ledger-store.js";
|
|
10
11
|
import { computeReplayPayloadHash, JCS_REPLAY_PAYLOAD_HASH_ALGORITHM, LEGACY_REPLAY_PAYLOAD_HASH_ALGORITHM, } from "./replay-payload-hash.js";
|
|
11
12
|
function sha256(input) {
|
|
@@ -86,7 +87,15 @@ export class TaskContractBuilder {
|
|
|
86
87
|
}
|
|
87
88
|
/** Parse and validate a serialized TaskContract. Fails closed on any shape violation. */
|
|
88
89
|
static fromJSON(json) {
|
|
89
|
-
|
|
90
|
+
let raw;
|
|
91
|
+
try {
|
|
92
|
+
raw = JSON.parse(json);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof SyntaxError)
|
|
96
|
+
throw new SyntaxError("Invalid TaskContract JSON");
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
90
99
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
91
100
|
throw new Error("TaskContract JSON must be an object");
|
|
92
101
|
}
|
|
@@ -525,36 +534,16 @@ export class EvidenceGate {
|
|
|
525
534
|
export class FailClosedMergeGate {
|
|
526
535
|
gates;
|
|
527
536
|
constructor(gates = [new EvidenceGate()]) {
|
|
528
|
-
|
|
537
|
+
if (!Array.isArray(gates) || gates.length === 0)
|
|
538
|
+
throw new TypeError("At least one evidence gate is required");
|
|
539
|
+
this.gates = Object.freeze(Array.from(gates, (gate) => {
|
|
540
|
+
if (!(gate instanceof EvidenceGate))
|
|
541
|
+
throw new TypeError("Invalid evidence gate");
|
|
542
|
+
return gate;
|
|
543
|
+
}));
|
|
529
544
|
}
|
|
530
545
|
check(contract) {
|
|
531
|
-
|
|
532
|
-
const blocked = results.filter((r) => r.status === "blocked");
|
|
533
|
-
const conditional = results.filter((r) => r.status === "conditional");
|
|
534
|
-
if (blocked.length > 0) {
|
|
535
|
-
return {
|
|
536
|
-
gateId: "fail-closed-merge-gate",
|
|
537
|
-
status: "blocked",
|
|
538
|
-
reason: `Blocked by ${blocked.length} gate(s): ${blocked.map((b) => b.reason).join("; ")}`,
|
|
539
|
-
suggestion: "Resolve all blocking conditions before merge.",
|
|
540
|
-
evidenceChecked: contract.requiredEvidence,
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
if (conditional.length > 0) {
|
|
544
|
-
return {
|
|
545
|
-
gateId: "fail-closed-merge-gate",
|
|
546
|
-
status: "conditional",
|
|
547
|
-
reason: `Conditional pass: ${conditional.length} gate(s) have pending conditions.`,
|
|
548
|
-
suggestion: "Complete pending evidence or waive with explicit approval.",
|
|
549
|
-
evidenceChecked: contract.requiredEvidence,
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
return {
|
|
553
|
-
gateId: "fail-closed-merge-gate",
|
|
554
|
-
status: "open",
|
|
555
|
-
reason: "All merge gates passed. Evidence verified and contract verdict is acceptable.",
|
|
556
|
-
evidenceChecked: contract.requiredEvidence,
|
|
557
|
-
};
|
|
546
|
+
return combineMergeGateResults(contract, this.gates.map((gate) => gate.check(contract)));
|
|
558
547
|
}
|
|
559
548
|
}
|
|
560
549
|
// ============================================================================
|