jev-layer 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +45 -34
  2. package/CONTRIBUTING.md +146 -146
  3. package/LICENSE +21 -21
  4. package/README.md +123 -126
  5. package/README.ru.md +138 -117
  6. package/README.zh-CN.md +137 -117
  7. package/RELEASE.md +53 -53
  8. package/SECURITY.md +56 -56
  9. package/bin/jev.mjs +213 -213
  10. package/config/codex.mcp.toml +8 -8
  11. package/config/generic-mcp.json +13 -13
  12. package/config/hermes.mcp.yaml +9 -9
  13. package/config/jev.example.json +22 -22
  14. package/config/omp.mcp.json +14 -14
  15. package/config/providers.env.example +13 -13
  16. package/docs/AGENT-IMPLEMENTATION.md +250 -0
  17. package/docs/COMPATIBILITY.md +15 -15
  18. package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +177 -0
  19. package/docs/PROVIDERS.md +67 -0
  20. package/docs/SCHEMA-VERSIONING.md +96 -94
  21. package/docs/architecture.svg +77 -0
  22. package/docs/jev-layer-banner.svg +38 -0
  23. package/examples/capabilities.json +39 -39
  24. package/examples/route-request.json +28 -28
  25. package/integrations/codex/.codex-plugin/plugin.json +19 -19
  26. package/integrations/codex/.mcp.json +11 -11
  27. package/integrations/codex/AGENTS.md +1 -1
  28. package/integrations/codex/run-mcp.mjs +9 -9
  29. package/integrations/codex/skills/jev-route/SKILL.md +17 -17
  30. package/integrations/hermes/__init__.py +150 -51
  31. package/integrations/hermes/plugin.yaml +10 -5
  32. package/integrations/hermes/schemas.py +89 -12
  33. package/integrations/omp/extension.js +105 -105
  34. package/integrations/template/README.md +10 -10
  35. package/integrations/template/adapter.mjs +87 -87
  36. package/package.json +60 -59
  37. package/scripts/benchmark.mjs +35 -35
  38. package/scripts/browser-benchmark.mjs +92 -92
  39. package/scripts/browser-e2e.mjs +117 -117
  40. package/scripts/capability-e2e.mjs +31 -31
  41. package/scripts/clean-install-smoke.mjs +162 -162
  42. package/scripts/codex-mcp-smoke.mjs +119 -119
  43. package/scripts/context-filter-e2e.mjs +45 -45
  44. package/scripts/fail-open-smoke.mjs +120 -120
  45. package/scripts/feature-flags-smoke.mjs +46 -46
  46. package/scripts/mcp-receipt-smoke.mjs +113 -98
  47. package/scripts/model-route-report.mjs +15 -0
  48. package/scripts/openrouter-choice.mjs +59 -59
  49. package/scripts/replay-eval.mjs +124 -124
  50. package/scripts/smoke.mjs +34 -34
  51. package/scripts/supervision-e2e.mjs +138 -109
  52. package/src/browser.mjs +571 -569
  53. package/src/cli.mjs +33 -33
  54. package/src/config.mjs +85 -75
  55. package/src/context-filter.mjs +56 -56
  56. package/src/contract.mjs +72 -72
  57. package/src/discovery.mjs +65 -65
  58. package/src/hermes-adapter.mjs +123 -0
  59. package/src/mcp-server.mjs +267 -210
  60. package/src/model-route-metrics.mjs +89 -0
  61. package/src/model-routing.mjs +87 -0
  62. package/src/providers/demo.mjs +52 -52
  63. package/src/providers/index.mjs +12 -0
  64. package/src/providers/typesafe.mjs +126 -126
  65. package/src/receipts.mjs +227 -226
  66. package/src/registry.mjs +109 -109
  67. package/src/relevance-filter.mjs +99 -99
  68. package/src/route.mjs +219 -221
  69. package/src/shadow-compaction.mjs +170 -0
  70. package/src/supervision.mjs +268 -244
  71. package/test/browser.test.mjs +199 -199
  72. package/test/capability.test.mjs +54 -54
  73. package/test/context-filter.test.mjs +65 -65
  74. package/test/hermes-adapter.test.mjs +62 -0
  75. package/test/model-route-metrics.test.mjs +61 -0
  76. package/test/model-routing.test.mjs +59 -0
  77. package/test/openrouter-provider.test.mjs +55 -55
  78. package/test/provider-transports.test.mjs +205 -0
  79. package/test/providers.test.mjs +126 -0
  80. package/test/receipts.test.mjs +71 -71
  81. package/test/route.test.mjs +74 -74
  82. package/test/shadow-compaction.test.mjs +67 -0
  83. package/test/supervision.test.mjs +117 -99
@@ -1,98 +1,113 @@
1
- import assert from "node:assert/strict";
2
- import { mkdtemp, readFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join, resolve } from "node:path";
5
- import { spawn } from "node:child_process";
6
- import { createInterface } from "node:readline";
7
-
8
- const root = resolve(new URL("..", import.meta.url).pathname);
9
- const casesPath = join(await mkdtemp(join(tmpdir(), "jev-mcp-receipt-")), "cases.jsonl");
10
- const child = spawn(process.env.JEV_NODE ?? "node", [resolve(root, "src/mcp-server.mjs")], {
11
- cwd: root,
12
- env: { ...process.env, JEV_REPLAY_CASES: casesPath },
13
- stdio: ["pipe", "pipe", "pipe"],
14
- });
15
- const lines = createInterface({ input: child.stdout });
16
- const pending = new Map();
17
- lines.on("line", (line) => {
18
- if (!line.trim()) return;
19
- const message = JSON.parse(line);
20
- const waiter = pending.get(message.id);
21
- if (waiter) {
22
- pending.delete(message.id);
23
- waiter(message);
24
- }
25
- });
26
-
27
- try {
28
- const initialized = await request(1, "initialize", {});
29
- assert.equal(initialized.result.serverInfo.name, "jev-layer");
30
- await notification("notifications/initialized", {});
31
- const listed = await request(2, "tools/list", {});
32
- assert.ok(listed.result.tools.some((tool) => tool.name === "jev_route"));
33
- assert.ok(listed.result.tools.some((tool) => tool.name === "jev_record_execution"));
34
-
35
- const routed = await request(3, "tools/call", {
36
- name: "jev_route",
37
- arguments: {
38
- provider: "demo",
39
- harness: "mcp-receipt-smoke",
40
- intent: "read repository metadata",
41
- actor_permissions: ["read"],
42
- capabilities: [{
43
- id: "read_metadata",
44
- kind: "tool",
45
- name: "Read metadata",
46
- description: "Read repository metadata without modifying files",
47
- permissions: ["read"],
48
- risk: "low",
49
- }],
50
- },
51
- });
52
- const decision = routed.result.structuredContent;
53
- assert.equal(decision.status, "selected");
54
- assert.equal(decision.receipt.replay_persisted, true);
55
-
56
- const recorded = await request(4, "tools/call", {
57
- name: "jev_record_execution",
58
- arguments: {
59
- correlation_id: decision.correlation_id,
60
- harness: "mcp-receipt-smoke",
61
- capability_id: decision.selected,
62
- status: "completed",
63
- result: { observed: "metadata" },
64
- exit_status: 0,
65
- duration_ms: 3.2,
66
- },
67
- });
68
- const receipt = recorded.result.structuredContent;
69
- assert.equal(receipt.correlation_id, decision.correlation_id);
70
- assert.equal(receipt.host.exit_status, 0);
71
- assert.equal(receipt.host.duration_ms, 3.2);
72
-
73
- const records = (await readFile(casesPath, "utf8")).trim().split(/\r?\n/).map(JSON.parse);
74
- assert.deepEqual(records.map((record) => record.record_type), ["routing_case", "execution_receipt"]);
75
- console.log(JSON.stringify({ ok: true, cases_path: casesPath, receipt }, null, 2));
76
- } finally {
77
- child.kill();
78
- await new Promise((resolveExit) => child.once("close", resolveExit));
79
- }
80
-
81
- function request(id, method, params) {
82
- return new Promise((resolveResponse, reject) => {
83
- const timer = setTimeout(() => {
84
- pending.delete(id);
85
- reject(new Error(`MCP request timed out: ${method}`));
86
- }, 15_000);
87
- pending.set(id, (message) => {
88
- clearTimeout(timer);
89
- resolveResponse(message);
90
- });
91
- child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
92
- });
93
- }
94
-
95
- function notification(method, params) {
96
- child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
97
- return new Promise((resolveNotification) => setImmediate(resolveNotification));
98
- }
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { spawn } from "node:child_process";
6
+ import { createInterface } from "node:readline";
7
+
8
+ const root = resolve(new URL("..", import.meta.url).pathname);
9
+ const casesPath = join(await mkdtemp(join(tmpdir(), "jev-mcp-receipt-")), "cases.jsonl");
10
+ const child = spawn(process.env.JEV_NODE ?? "node", [resolve(root, "src/mcp-server.mjs")], {
11
+ cwd: root,
12
+ env: { ...process.env, JEV_REPLAY_CASES: casesPath },
13
+ stdio: ["pipe", "pipe", "pipe"],
14
+ });
15
+ const lines = createInterface({ input: child.stdout });
16
+ const pending = new Map();
17
+ lines.on("line", (line) => {
18
+ if (!line.trim()) return;
19
+ const message = JSON.parse(line);
20
+ const waiter = pending.get(message.id);
21
+ if (waiter) {
22
+ pending.delete(message.id);
23
+ waiter(message);
24
+ }
25
+ });
26
+
27
+ try {
28
+ const initialized = await request(1, "initialize", {});
29
+ assert.equal(initialized.result.serverInfo.name, "jev-layer");
30
+ await notification("notifications/initialized", {});
31
+ const listed = await request(2, "tools/list", {});
32
+ assert.ok(listed.result.tools.some((tool) => tool.name === "jev_route"));
33
+ assert.ok(listed.result.tools.some((tool) => tool.name === "jev_model_route"));
34
+ assert.ok(listed.result.tools.some((tool) => tool.name === "jev_shadow_compaction"));
35
+ assert.ok(listed.result.tools.some((tool) => tool.name === "jev_record_execution"));
36
+
37
+ const routed = await request(3, "tools/call", {
38
+ name: "jev_route",
39
+ arguments: {
40
+ provider: "demo",
41
+ harness: "mcp-receipt-smoke",
42
+ intent: "read repository metadata",
43
+ actor_permissions: ["read"],
44
+ capabilities: [{
45
+ id: "read_metadata",
46
+ kind: "tool",
47
+ name: "Read metadata",
48
+ description: "Read repository metadata without modifying files",
49
+ permissions: ["read"],
50
+ risk: "low",
51
+ }],
52
+ },
53
+ });
54
+ const decision = routed.result.structuredContent;
55
+ assert.equal(decision.status, "selected");
56
+ assert.equal(decision.receipt.replay_persisted, true);
57
+
58
+ const recorded = await request(4, "tools/call", {
59
+ name: "jev_record_execution",
60
+ arguments: {
61
+ correlation_id: decision.correlation_id,
62
+ harness: "mcp-receipt-smoke",
63
+ capability_id: decision.selected,
64
+ status: "completed",
65
+ result: { observed: "metadata" },
66
+ exit_status: 0,
67
+ duration_ms: 3.2,
68
+ },
69
+ });
70
+ const receipt = recorded.result.structuredContent;
71
+ assert.equal(receipt.correlation_id, decision.correlation_id);
72
+ assert.equal(receipt.host.exit_status, 0);
73
+ assert.equal(receipt.host.duration_ms, 3.2);
74
+
75
+ const shadow = await request(5, "tools/call", {
76
+ name: "jev_shadow_compaction",
77
+ arguments: {
78
+ provider: "demo",
79
+ intent: "inspect context before compaction",
80
+ context: { messages: ["stale note", "Requirement: preserve /workspace/config.json"] },
81
+ },
82
+ });
83
+ const shadowReport = shadow.result.structuredContent;
84
+ assert.equal(shadowReport.mode, "jev_shadow");
85
+ assert.equal(shadowReport.changed, false);
86
+ assert.equal(shadowReport.protected_count, 1);
87
+
88
+ const records = (await readFile(casesPath, "utf8")).trim().split(/\r?\n/).map(JSON.parse);
89
+ assert.deepEqual(records.map((record) => record.record_type), ["routing_case", "execution_receipt"]);
90
+ console.log(JSON.stringify({ ok: true, cases_path: casesPath, receipt }, null, 2));
91
+ } finally {
92
+ child.kill();
93
+ await new Promise((resolveExit) => child.once("close", resolveExit));
94
+ }
95
+
96
+ function request(id, method, params) {
97
+ return new Promise((resolveResponse, reject) => {
98
+ const timer = setTimeout(() => {
99
+ pending.delete(id);
100
+ reject(new Error(`MCP request timed out: ${method}`));
101
+ }, 15_000);
102
+ pending.set(id, (message) => {
103
+ clearTimeout(timer);
104
+ resolveResponse(message);
105
+ });
106
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
107
+ });
108
+ }
109
+
110
+ function notification(method, params) {
111
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
112
+ return new Promise((resolveNotification) => setImmediate(resolveNotification));
113
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { buildModelRouteReport } from "../src/model-route-metrics.mjs";
4
+
5
+ const path = process.argv[2] ?? process.env.JEV_REPLAY_CASES ?? ".jev/replay/cases.jsonl";
6
+ let text = "";
7
+ try {
8
+ text = await readFile(path, "utf8");
9
+ } catch (error) {
10
+ if (error?.code !== "ENOENT") throw error;
11
+ }
12
+ const records = text.split(/\r?\n/).filter(Boolean).flatMap((line) => {
13
+ try { return [JSON.parse(line)]; } catch { return []; }
14
+ });
15
+ console.log(JSON.stringify(buildModelRouteReport(records), null, 2));
@@ -1,59 +1,59 @@
1
- import { routeRequest } from "../src/route.mjs";
2
-
3
- const endpoint = process.env.OPENROUTER_DECISIONS_ENDPOINT ?? "https://openrouter.ai/api/alpha/decisions";
4
- const model = process.env.OPENROUTER_DECISIONS_MODEL ?? "typesafe/jev-1.13";
5
- const decision = await routeRequest({
6
- schema_version: 1,
7
- harness: "jev-layer-openrouter-smoke",
8
- intent: "Find where the OpenRouter Decisions provider is implemented without modifying files",
9
- context: {
10
- purpose: "one real bounded Choice call",
11
- secret_note: "must be redacted before provider transmission",
12
- },
13
- actor_permissions: ["read", "browser", "terminal:read"],
14
- capabilities: [
15
- {
16
- id: "repo_search",
17
- kind: "mcp",
18
- name: "Repository search",
19
- description: "Search repository files and symbols without changing files",
20
- permissions: ["read"],
21
- risk: "low",
22
- },
23
- {
24
- id: "browser_inspect",
25
- kind: "mcp",
26
- name: "Browser inspection",
27
- description: "Open a webpage and inspect visible browser state",
28
- permissions: ["browser"],
29
- risk: "medium",
30
- },
31
- {
32
- id: "terminal_read",
33
- kind: "cli",
34
- name: "Read-only terminal",
35
- description: "Run a read-only shell command and return its output",
36
- permissions: ["terminal:read"],
37
- risk: "low",
38
- },
39
- ],
40
- }, {
41
- provider: "openrouter",
42
- openrouter: { endpoint, model, timeoutMs: 15_000 },
43
- });
44
-
45
- const raw = decision.raw_jev ?? {};
46
- const usage = raw.usage && typeof raw.usage === "object" ? raw.usage : {};
47
- console.log(JSON.stringify({
48
- request: { endpoint, model, method: "POST" },
49
- response: { id: raw.id ?? null, model: raw.model ?? null, provider: raw.provider ?? null },
50
- status: decision.status,
51
- selected: decision.selected,
52
- probabilities: decision.probabilities,
53
- confidence: decision.confidence,
54
- latency_ms: decision.receipt.latency_ms,
55
- cost_usd: typeof usage.cost === "number" ? usage.cost : null,
56
- usage,
57
- fallback: decision.fallback,
58
- execution: decision.execution,
59
- }, null, 2));
1
+ import { routeRequest } from "../src/route.mjs";
2
+
3
+ const endpoint = process.env.OPENROUTER_DECISIONS_ENDPOINT ?? "https://openrouter.ai/api/alpha/decisions";
4
+ const model = process.env.OPENROUTER_DECISIONS_MODEL ?? "typesafe/jev-1.13";
5
+ const decision = await routeRequest({
6
+ schema_version: 1,
7
+ harness: "jev-layer-openrouter-smoke",
8
+ intent: "Find where the OpenRouter Decisions provider is implemented without modifying files",
9
+ context: {
10
+ purpose: "one real bounded Choice call",
11
+ secret_note: "must be redacted before provider transmission",
12
+ },
13
+ actor_permissions: ["read", "browser", "terminal:read"],
14
+ capabilities: [
15
+ {
16
+ id: "repo_search",
17
+ kind: "mcp",
18
+ name: "Repository search",
19
+ description: "Search repository files and symbols without changing files",
20
+ permissions: ["read"],
21
+ risk: "low",
22
+ },
23
+ {
24
+ id: "browser_inspect",
25
+ kind: "mcp",
26
+ name: "Browser inspection",
27
+ description: "Open a webpage and inspect visible browser state",
28
+ permissions: ["browser"],
29
+ risk: "medium",
30
+ },
31
+ {
32
+ id: "terminal_read",
33
+ kind: "cli",
34
+ name: "Read-only terminal",
35
+ description: "Run a read-only shell command and return its output",
36
+ permissions: ["terminal:read"],
37
+ risk: "low",
38
+ },
39
+ ],
40
+ }, {
41
+ provider: "openrouter",
42
+ openrouter: { endpoint, model, timeoutMs: 15_000 },
43
+ });
44
+
45
+ const raw = decision.raw_jev ?? {};
46
+ const usage = raw.usage && typeof raw.usage === "object" ? raw.usage : {};
47
+ console.log(JSON.stringify({
48
+ request: { endpoint, model, method: "POST" },
49
+ response: { id: raw.id ?? null, model: raw.model ?? null, provider: raw.provider ?? null },
50
+ status: decision.status,
51
+ selected: decision.selected,
52
+ probabilities: decision.probabilities,
53
+ confidence: decision.confidence,
54
+ latency_ms: decision.receipt.latency_ms,
55
+ cost_usd: typeof usage.cost === "number" ? usage.cost : null,
56
+ usage,
57
+ fallback: decision.fallback,
58
+ execution: decision.execution,
59
+ }, null, 2));
@@ -1,124 +1,124 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname } from "node:path";
3
- import { readRoutingCases, readSupervisionCases, replayCasePath } from "../src/receipts.mjs";
4
- import { deterministicSupervisionPolicy } from "../src/supervision.mjs";
5
- import { routeRequest } from "../src/route.mjs";
6
-
7
- const args = parseArgs(process.argv.slice(2));
8
- const inputPath = replayCasePath(args.input);
9
- const provider = args.provider ?? process.env.JEV_REPLAY_PROVIDER ?? "demo";
10
- const allCases = await readRoutingCases(inputPath);
11
- const cases = args.limit ? allCases.slice(0, args.limit) : allCases;
12
- const results = [];
13
-
14
- for (const routingCase of cases) {
15
- const decision = await routeRequest(routingCase.request, { provider });
16
- if (decision.execution?.enabled) throw new Error(`replay attempted host execution for ${routingCase.case_id}`);
17
- results.push({
18
- source_case_id: routingCase.case_id,
19
- source_correlation_id: routingCase.correlation_id,
20
- replay_correlation_id: decision.correlation_id,
21
- surface: routingCase.request.context?.browser ? "browser" : "generic",
22
- harness: routingCase.harness,
23
- selected: decision.selected,
24
- confidence: decision.confidence,
25
- status: decision.status,
26
- fallback: decision.fallback?.type ?? null,
27
- latency_ms: decision.receipt?.latency_ms ?? null,
28
- cost_usd: decision.receipt?.cost_usd ?? decision.raw_jev?.usage?.cost ?? null,
29
- execution: decision.execution,
30
- });
31
- }
32
-
33
- const supervisionCases = args.limit
34
- ? (await readSupervisionCases(inputPath)).slice(0, args.limit)
35
- : await readSupervisionCases(inputPath);
36
- const supervisionResults = supervisionCases.map((record) => ({
37
- source_case_id: record.case_id,
38
- source_correlation_id: record.correlation_id,
39
- harness: record.harness,
40
- assessment: record.supervision.assessment,
41
- recorded_action: record.supervision.action,
42
- replayed_policy: deterministicSupervisionPolicy({
43
- assessment: record.supervision.assessment,
44
- evidence: record.request.context?.evidence ?? {},
45
- }),
46
- host_execution: false,
47
- }));
48
-
49
-
50
- const summary = summarize(results);
51
- summary.supervision_case_count = supervisionResults.length;
52
- summary.supervision_policy_match_rate = rate(
53
- supervisionResults.filter((result) => result.replayed_policy.action === result.recorded_action).length,
54
- supervisionResults.length,
55
- );
56
- const report = {
57
- ok: true,
58
- source: inputPath,
59
- provider,
60
- replayed_cases: results.length,
61
- replayed_supervision_cases: supervisionResults.length,
62
- host_execution_calls: 0,
63
- summary,
64
- cases: results,
65
- supervision_cases: supervisionResults,
66
- };
67
-
68
- if (args.output) {
69
- await mkdir(dirname(args.output), { recursive: true });
70
- await writeFile(args.output, `${JSON.stringify(report, null, 2)}\n`, "utf8");
71
- }
72
- console.log(JSON.stringify(report, null, 2));
73
- function summarize(results) {
74
- const total = results.length;
75
- const fallbackCount = results.filter((result) => result.status === "fallback").length;
76
- const selectedCount = results.filter((result) => result.status === "selected" || result.status === "needs_confirmation").length;
77
- const noDecisionCount = results.filter((result) => result.status === "no_decision").length;
78
- const browserResults = results.filter((result) => result.surface === "browser");
79
- const confidences = results.map((result) => result.confidence).filter((value) => typeof value === "number");
80
- const latencies = results.map((result) => result.latency_ms).filter((value) => typeof value === "number");
81
- const costs = results.map((result) => result.cost_usd).filter((value) => typeof value === "number");
82
- return {
83
- selected_rate: rate(selectedCount, total),
84
- fallback_rate: rate(fallbackCount, total),
85
- no_decision_rate: rate(noDecisionCount, total),
86
- browser_case_count: browserResults.length,
87
- browser_selected_rate: rate(browserResults.filter((result) => result.status === "selected" || result.status === "needs_confirmation").length, browserResults.length),
88
- average_confidence: average(confidences),
89
- average_latency_ms: average(latencies),
90
- p50_latency_ms: percentile(latencies, 0.5),
91
- p95_latency_ms: percentile(latencies, 0.95),
92
- total_cost_usd: Number(costs.reduce((sum, value) => sum + value, 0).toFixed(9)),
93
- status_counts: Object.fromEntries([...new Set(results.map((result) => result.status))].map((status) => [status, results.filter((result) => result.status === status).length])),
94
- };
95
- }
96
-
97
- function parseArgs(argv) {
98
- const result = {};
99
- for (let index = 0; index < argv.length; index += 1) {
100
- const value = argv[index];
101
- if (value === "--input") result.input = argv[++index];
102
- else if (value === "--provider") result.provider = argv[++index];
103
- else if (value === "--limit") result.limit = Number(argv[++index]);
104
- else if (value === "--output") result.output = argv[++index];
105
- }
106
- if (result.limit !== undefined && (!Number.isInteger(result.limit) || result.limit < 0)) throw new Error("--limit must be a non-negative integer");
107
- return result;
108
- }
109
-
110
- function average(values) {
111
- return values.length ? Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(3)) : null;
112
- }
113
-
114
- function percentile(values, fraction) {
115
- if (!values.length) return null;
116
- const sorted = [...values].sort((left, right) => left - right);
117
- const rank = Math.max(0, Math.ceil(sorted.length * fraction) - 1);
118
- return Number(sorted[Math.min(sorted.length - 1, rank)].toFixed(3));
119
- }
120
-
121
-
122
- function rate(part, total) {
123
- return total ? Number((part / total).toFixed(4)) : 0;
124
- }
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { readRoutingCases, readSupervisionCases, replayCasePath } from "../src/receipts.mjs";
4
+ import { deterministicSupervisionPolicy } from "../src/supervision.mjs";
5
+ import { routeRequest } from "../src/route.mjs";
6
+
7
+ const args = parseArgs(process.argv.slice(2));
8
+ const inputPath = replayCasePath(args.input);
9
+ const provider = args.provider ?? process.env.JEV_REPLAY_PROVIDER ?? "demo";
10
+ const allCases = await readRoutingCases(inputPath);
11
+ const cases = args.limit ? allCases.slice(0, args.limit) : allCases;
12
+ const results = [];
13
+
14
+ for (const routingCase of cases) {
15
+ const decision = await routeRequest(routingCase.request, { provider });
16
+ if (decision.execution?.enabled) throw new Error(`replay attempted host execution for ${routingCase.case_id}`);
17
+ results.push({
18
+ source_case_id: routingCase.case_id,
19
+ source_correlation_id: routingCase.correlation_id,
20
+ replay_correlation_id: decision.correlation_id,
21
+ surface: routingCase.request.context?.browser ? "browser" : "generic",
22
+ harness: routingCase.harness,
23
+ selected: decision.selected,
24
+ confidence: decision.confidence,
25
+ status: decision.status,
26
+ fallback: decision.fallback?.type ?? null,
27
+ latency_ms: decision.receipt?.latency_ms ?? null,
28
+ cost_usd: decision.receipt?.cost_usd ?? decision.raw_jev?.usage?.cost ?? null,
29
+ execution: decision.execution,
30
+ });
31
+ }
32
+
33
+ const supervisionCases = args.limit
34
+ ? (await readSupervisionCases(inputPath)).slice(0, args.limit)
35
+ : await readSupervisionCases(inputPath);
36
+ const supervisionResults = supervisionCases.map((record) => ({
37
+ source_case_id: record.case_id,
38
+ source_correlation_id: record.correlation_id,
39
+ harness: record.harness,
40
+ assessment: record.supervision.assessment,
41
+ recorded_action: record.supervision.action,
42
+ replayed_policy: deterministicSupervisionPolicy({
43
+ assessment: record.supervision.assessment,
44
+ evidence: record.request.context?.evidence ?? {},
45
+ }),
46
+ host_execution: false,
47
+ }));
48
+
49
+
50
+ const summary = summarize(results);
51
+ summary.supervision_case_count = supervisionResults.length;
52
+ summary.supervision_policy_match_rate = rate(
53
+ supervisionResults.filter((result) => result.replayed_policy.action === result.recorded_action).length,
54
+ supervisionResults.length,
55
+ );
56
+ const report = {
57
+ ok: true,
58
+ source: inputPath,
59
+ provider,
60
+ replayed_cases: results.length,
61
+ replayed_supervision_cases: supervisionResults.length,
62
+ host_execution_calls: 0,
63
+ summary,
64
+ cases: results,
65
+ supervision_cases: supervisionResults,
66
+ };
67
+
68
+ if (args.output) {
69
+ await mkdir(dirname(args.output), { recursive: true });
70
+ await writeFile(args.output, `${JSON.stringify(report, null, 2)}\n`, "utf8");
71
+ }
72
+ console.log(JSON.stringify(report, null, 2));
73
+ function summarize(results) {
74
+ const total = results.length;
75
+ const fallbackCount = results.filter((result) => result.status === "fallback").length;
76
+ const selectedCount = results.filter((result) => result.status === "selected" || result.status === "needs_confirmation").length;
77
+ const noDecisionCount = results.filter((result) => result.status === "no_decision").length;
78
+ const browserResults = results.filter((result) => result.surface === "browser");
79
+ const confidences = results.map((result) => result.confidence).filter((value) => typeof value === "number");
80
+ const latencies = results.map((result) => result.latency_ms).filter((value) => typeof value === "number");
81
+ const costs = results.map((result) => result.cost_usd).filter((value) => typeof value === "number");
82
+ return {
83
+ selected_rate: rate(selectedCount, total),
84
+ fallback_rate: rate(fallbackCount, total),
85
+ no_decision_rate: rate(noDecisionCount, total),
86
+ browser_case_count: browserResults.length,
87
+ browser_selected_rate: rate(browserResults.filter((result) => result.status === "selected" || result.status === "needs_confirmation").length, browserResults.length),
88
+ average_confidence: average(confidences),
89
+ average_latency_ms: average(latencies),
90
+ p50_latency_ms: percentile(latencies, 0.5),
91
+ p95_latency_ms: percentile(latencies, 0.95),
92
+ total_cost_usd: Number(costs.reduce((sum, value) => sum + value, 0).toFixed(9)),
93
+ status_counts: Object.fromEntries([...new Set(results.map((result) => result.status))].map((status) => [status, results.filter((result) => result.status === status).length])),
94
+ };
95
+ }
96
+
97
+ function parseArgs(argv) {
98
+ const result = {};
99
+ for (let index = 0; index < argv.length; index += 1) {
100
+ const value = argv[index];
101
+ if (value === "--input") result.input = argv[++index];
102
+ else if (value === "--provider") result.provider = argv[++index];
103
+ else if (value === "--limit") result.limit = Number(argv[++index]);
104
+ else if (value === "--output") result.output = argv[++index];
105
+ }
106
+ if (result.limit !== undefined && (!Number.isInteger(result.limit) || result.limit < 0)) throw new Error("--limit must be a non-negative integer");
107
+ return result;
108
+ }
109
+
110
+ function average(values) {
111
+ return values.length ? Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(3)) : null;
112
+ }
113
+
114
+ function percentile(values, fraction) {
115
+ if (!values.length) return null;
116
+ const sorted = [...values].sort((left, right) => left - right);
117
+ const rank = Math.max(0, Math.ceil(sorted.length * fraction) - 1);
118
+ return Number(sorted[Math.min(sorted.length - 1, rank)].toFixed(3));
119
+ }
120
+
121
+
122
+ function rate(part, total) {
123
+ return total ? Number((part / total).toFixed(4)) : 0;
124
+ }