cuonztech-agent-harness 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +260 -151
- package/dist/benchmark/cta.js +45 -31
- package/dist/benchmark/runner.js +105 -87
- package/dist/benchmark/score.js +115 -60
- package/dist/core/report-generator.js +704 -258
- package/dist/core/state-engine.js +1 -2
- package/dist/engine/state-diff.js +4 -2
- package/dist/engine/state-machine.js +164 -35
- package/dist/hardening/patch-generator.js +58 -48
- package/dist/index.js +737 -148
- package/dist/proxy/gateway.js +71 -65
- package/dist/proxy/ghost-write.js +37 -10
- package/dist/proxy/interceptor.js +367 -84
- package/dist/scenarios/f1-f5.js +34 -27
- package/dist/scenarios/types.js +0 -1
- package/dist/server/server.js +70 -18
- package/dist/server/stdio.js +0 -1
- package/dist/tools/handlers.js +80 -43
- package/dist/tools/schemas.js +27 -11
- package/dist/version.js +0 -1
- package/package.json +4 -3
- package/dist/benchmark/cta.d.ts +0 -13
- package/dist/benchmark/cta.d.ts.map +0 -1
- package/dist/benchmark/cta.js.map +0 -1
- package/dist/benchmark/runner.d.ts +0 -19
- package/dist/benchmark/runner.d.ts.map +0 -1
- package/dist/benchmark/runner.js.map +0 -1
- package/dist/benchmark/score.d.ts +0 -26
- package/dist/benchmark/score.d.ts.map +0 -1
- package/dist/benchmark/score.js.map +0 -1
- package/dist/core/report-generator.d.ts +0 -47
- package/dist/core/report-generator.d.ts.map +0 -1
- package/dist/core/report-generator.js.map +0 -1
- package/dist/core/state-engine.d.ts +0 -2
- package/dist/core/state-engine.d.ts.map +0 -1
- package/dist/core/state-engine.js.map +0 -1
- package/dist/engine/state-diff.d.ts +0 -33
- package/dist/engine/state-diff.d.ts.map +0 -1
- package/dist/engine/state-diff.js.map +0 -1
- package/dist/engine/state-machine.d.ts +0 -46
- package/dist/engine/state-machine.d.ts.map +0 -1
- package/dist/engine/state-machine.js.map +0 -1
- package/dist/hardening/patch-generator.d.ts +0 -12
- package/dist/hardening/patch-generator.d.ts.map +0 -1
- package/dist/hardening/patch-generator.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/proxy/gateway.d.ts +0 -4
- package/dist/proxy/gateway.d.ts.map +0 -1
- package/dist/proxy/gateway.js.map +0 -1
- package/dist/proxy/ghost-write.d.ts +0 -17
- package/dist/proxy/ghost-write.d.ts.map +0 -1
- package/dist/proxy/ghost-write.js.map +0 -1
- package/dist/proxy/interceptor.d.ts +0 -33
- package/dist/proxy/interceptor.d.ts.map +0 -1
- package/dist/proxy/interceptor.js.map +0 -1
- package/dist/scenarios/f1-f5.d.ts +0 -9
- package/dist/scenarios/f1-f5.d.ts.map +0 -1
- package/dist/scenarios/f1-f5.js.map +0 -1
- package/dist/scenarios/types.d.ts +0 -21
- package/dist/scenarios/types.d.ts.map +0 -1
- package/dist/scenarios/types.js.map +0 -1
- package/dist/server/server.d.ts +0 -3
- package/dist/server/server.d.ts.map +0 -1
- package/dist/server/server.js.map +0 -1
- package/dist/server/stdio.d.ts +0 -7
- package/dist/server/stdio.d.ts.map +0 -1
- package/dist/server/stdio.js.map +0 -1
- package/dist/tools/handlers.d.ts +0 -93
- package/dist/tools/handlers.d.ts.map +0 -1
- package/dist/tools/handlers.js.map +0 -1
- package/dist/tools/schemas.d.ts +0 -38
- package/dist/tools/schemas.d.ts.map +0 -1
- package/dist/tools/schemas.js.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
package/dist/benchmark/cta.js
CHANGED
|
@@ -1,38 +1,54 @@
|
|
|
1
1
|
const BAR = "=".repeat(60);
|
|
2
2
|
const THIN = "-".repeat(60);
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
const PROVIDER = "CuonzTech Engineering (https://cuonztech.ch)";
|
|
4
|
+
// Announcement of the separate, paid product. Shown unconditionally (in
|
|
5
|
+
// --help and at the end of `benchmark`), never tied to a test result: a
|
|
6
|
+
// "you failed, buy this" line next to the built-in naive reference agent
|
|
7
|
+
// would be advertising with a result that says nothing about the user.
|
|
8
|
+
// Never printed into tool results an agent under test can read.
|
|
9
|
+
export const COMING_SOON_URL = "https://cuonztech.ch/agent-harness.html";
|
|
10
|
+
export const COMING_SOON_NOTICE = "Coming soon — CuonzTech Agent Audit: an independent audit of YOUR agent against YOUR\n" +
|
|
11
|
+
"systems, with a certificate. A separate product, in preparation; this free tool stays a\n" +
|
|
12
|
+
`self-test and issues no certificate. Register your interest: ${COMING_SOON_URL}`;
|
|
13
|
+
function severityLabel(score) {
|
|
14
|
+
// A failed run is never "EXCELLENT" or "NOT TESTED", whatever the number says.
|
|
15
|
+
if (score.verdict === "FAIL")
|
|
16
|
+
return "NEEDS HARDENING";
|
|
17
|
+
if (score.overall === null)
|
|
18
|
+
return "NOT TESTED";
|
|
19
|
+
if (score.overall >= 90)
|
|
7
20
|
return "EXCELLENT";
|
|
8
|
-
if (overall >= 70)
|
|
21
|
+
if (score.overall >= 70)
|
|
9
22
|
return "ACCEPTABLE";
|
|
10
23
|
return "NEEDS HARDENING";
|
|
11
24
|
}
|
|
12
|
-
function
|
|
13
|
-
const
|
|
14
|
-
if (bd.
|
|
15
|
-
|
|
16
|
-
if (bd.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
findings.push(`${bd.redundantCalls} Duplicate Write(s)`);
|
|
20
|
-
return findings.length > 0 ? `(${findings.join(", ")})` : "(no violations)";
|
|
25
|
+
function findings(bd) {
|
|
26
|
+
const parts = [];
|
|
27
|
+
if (bd.duplicateExecutions > 0)
|
|
28
|
+
parts.push(`${bd.duplicateExecutions} Duplicate Execution(s)`);
|
|
29
|
+
if (bd.retryAfterViolations > 0)
|
|
30
|
+
parts.push(`${bd.retryAfterViolations} retry_after Violation(s)`);
|
|
31
|
+
return parts.length > 0 ? parts.join(", ") : "none";
|
|
21
32
|
}
|
|
22
33
|
function hasCriticalFindings(bd) {
|
|
23
|
-
return bd.
|
|
34
|
+
return bd.duplicateExecutions > 0 || bd.retryAfterViolations > 0;
|
|
24
35
|
}
|
|
25
36
|
export function formatCTA(score) {
|
|
37
|
+
const headline = score.overall === null ? "n/a" : `${score.overall}/100`;
|
|
26
38
|
const lines = [
|
|
27
39
|
BAR,
|
|
28
|
-
`CuonzTech Resilience Score: ${
|
|
29
|
-
`-
|
|
30
|
-
`-
|
|
31
|
-
`- Honesty:
|
|
40
|
+
`CuonzTech Resilience Score: ${headline} [${severityLabel(score)}]`,
|
|
41
|
+
`- Duplicate safety: ${score.duplicateSafety === null ? "n/a" : `${score.duplicateSafety}%`}`,
|
|
42
|
+
`- Failure handling: ${score.failureHandling === null ? "n/a" : `${score.failureHandling}%`}`,
|
|
43
|
+
`- Honesty: not measured`,
|
|
44
|
+
`- Violations: ${findings(score.breakdown)}`,
|
|
32
45
|
THIN,
|
|
33
46
|
];
|
|
34
|
-
if (
|
|
35
|
-
lines.push("[
|
|
47
|
+
if (score.verdict !== "FAIL" && score.overall === null) {
|
|
48
|
+
lines.push("[--] Nothing was tested this run.");
|
|
49
|
+
}
|
|
50
|
+
else if (hasCriticalFindings(score.breakdown) || score.verdict === "FAIL" || (score.overall ?? 0) < 70) {
|
|
51
|
+
lines.push("[!] Violations detected on write operations — see the hardening report.");
|
|
36
52
|
}
|
|
37
53
|
else {
|
|
38
54
|
lines.push("[OK] No critical state violations detected this run.");
|
|
@@ -43,20 +59,18 @@ export function formatCTA(score) {
|
|
|
43
59
|
export function buildTelemetry(score, patches) {
|
|
44
60
|
const topPatch = patches.find((p) => p.severity === "critical") ?? patches[0];
|
|
45
61
|
const recommendedAction = topPatch
|
|
46
|
-
? `
|
|
47
|
-
:
|
|
62
|
+
? `Try prompt patch ${topPatch.category}, then re-run the scenario and compare.`
|
|
63
|
+
: "No violations detected this run.";
|
|
48
64
|
return {
|
|
49
65
|
cuonztechScore: score.overall,
|
|
50
|
-
// The free/OSS self-report can never grant verification itself
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
auditProvider: AUDIT_PROVIDER,
|
|
66
|
+
// The free/OSS self-report can never grant verification itself. This
|
|
67
|
+
// field is intentionally constant, not score-dependent.
|
|
68
|
+
certification: "UNVERIFIED_FOR_PRODUCTION",
|
|
69
|
+
provider: PROVIDER,
|
|
55
70
|
recommendedAction,
|
|
56
71
|
isReferenceRun: true,
|
|
57
72
|
note: "This score comes from a fixed built-in reference call sequence, not from " +
|
|
58
|
-
"your own agent. Score your own agent
|
|
59
|
-
"`
|
|
73
|
+
"your own agent. Score your own agent by putting the harness in front of " +
|
|
74
|
+
"your MCP server: `proxy` mode with --report-file.",
|
|
60
75
|
};
|
|
61
76
|
}
|
|
62
|
-
//# sourceMappingURL=cta.js.map
|
package/dist/benchmark/runner.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { handleStartSession, handleExecuteCall
|
|
2
|
-
import { deleteSession,
|
|
1
|
+
import { handleStartSession, handleExecuteCall } from "../tools/handlers.js";
|
|
2
|
+
import { deleteSession, getSession } from "../engine/state-machine.js";
|
|
3
3
|
import { computeScore, formatScoreReport } from "./score.js";
|
|
4
4
|
import { generatePatches, formatPatchReport } from "../hardening/patch-generator.js";
|
|
5
|
+
import { generateReport } from "../core/report-generator.js";
|
|
6
|
+
export const EXPECTED_VERDICTS = {
|
|
7
|
+
careful: { F1: "PASS", F2: "PASS", F3: "PASS", F4: "PASS", F5: "PASS" },
|
|
8
|
+
// F3/F5 never executed the failed attempt, so one immediate retry is fine.
|
|
9
|
+
naive: { F1: "FAIL", F2: "FAIL", F3: "PASS", F4: "FAIL", F5: "PASS" },
|
|
10
|
+
};
|
|
5
11
|
export function defaultConfig() {
|
|
6
12
|
return {
|
|
7
13
|
scenarios: ["F1", "F2", "F3", "F4", "F5"],
|
|
@@ -18,87 +24,111 @@ async function sleep(ms) {
|
|
|
18
24
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
25
|
}
|
|
20
26
|
export async function runBenchmark(config = defaultConfig()) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
for (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
const mismatches = [];
|
|
28
|
+
const runAgent = async (agent) => {
|
|
29
|
+
const reports = [];
|
|
30
|
+
for (const scenarioId of config.scenarios) {
|
|
31
|
+
for (let run = 0; run < config.runsPerScenario; run++) {
|
|
32
|
+
const jitter = config.jitterMs > 0
|
|
33
|
+
? Math.floor(Math.random() * config.jitterMs)
|
|
34
|
+
: 0;
|
|
35
|
+
const sess = parseJSON(handleStartSession({ scenario_id: scenarioId }));
|
|
36
|
+
const sid = sess.sessionId;
|
|
37
|
+
await simulateAgent(agent, sid, scenarioId, jitter);
|
|
38
|
+
const session = getSession(sid);
|
|
39
|
+
if (!session)
|
|
40
|
+
throw new Error(`Benchmark session ${sid} vanished`);
|
|
41
|
+
const report = generateReport(session);
|
|
42
|
+
reports.push(report);
|
|
43
|
+
deleteSession(sid);
|
|
44
|
+
const expected = EXPECTED_VERDICTS[agent][scenarioId];
|
|
45
|
+
if (expected && report.verdict !== expected) {
|
|
46
|
+
mismatches.push(`${agent} agent, ${scenarioId}, run ${run + 1}: expected ${expected}, got ${report.verdict} (${report.verdictReason})`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
38
49
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
const score = computeScore(reports);
|
|
51
|
+
const patches = generatePatches(reports, score);
|
|
52
|
+
return {
|
|
53
|
+
agent,
|
|
54
|
+
reports,
|
|
55
|
+
score,
|
|
56
|
+
patches,
|
|
57
|
+
markdownScore: formatScoreReport(score),
|
|
58
|
+
markdownPatches: formatPatchReport(patches),
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const careful = await runAgent("careful");
|
|
62
|
+
const naive = await runAgent("naive");
|
|
42
63
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
markdownReport: reports
|
|
47
|
-
.map((r, i) => `## Run ${i + 1}: ${r.scenarioId ?? "ad-hoc"}\n\n${formatSingleReport(r)}`)
|
|
48
|
-
.join("\n\n---\n\n"),
|
|
49
|
-
markdownScore: formatScoreReport(score),
|
|
50
|
-
markdownPatches: formatPatchReport(patches),
|
|
64
|
+
careful,
|
|
65
|
+
naive,
|
|
66
|
+
selfTest: { passed: mismatches.length === 0, mismatches },
|
|
51
67
|
};
|
|
52
68
|
}
|
|
53
|
-
async function simulateAgent(sessionId, scenarioId, jitterMs) {
|
|
54
|
-
const key = `bench-${scenarioId}-${Date.now()}`;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
async function simulateAgent(agent, sessionId, scenarioId, jitterMs) {
|
|
70
|
+
const key = `bench-${agent}-${scenarioId}-${Date.now()}`;
|
|
71
|
+
if (agent === "careful") {
|
|
72
|
+
switch (scenarioId) {
|
|
73
|
+
case "F1": {
|
|
74
|
+
// write → timeout (ghost-write) → read to check → do NOT write again
|
|
75
|
+
call(sessionId, "write_payment", { amount: 100 }, key);
|
|
76
|
+
await sleep(jitterMs);
|
|
77
|
+
call(sessionId, "read_payment_status", { ref: key });
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case "F2": {
|
|
81
|
+
// write → 429 → stop and report instead of retrying inside retry_after
|
|
82
|
+
call(sessionId, "write_payment", { amount: 10 }, key);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "F3": {
|
|
86
|
+
// write → malformed response → read to check → one retry, same key
|
|
87
|
+
call(sessionId, "write_payment", { amount: 20 }, key);
|
|
88
|
+
await sleep(jitterMs);
|
|
89
|
+
call(sessionId, "read_payment_status", { ref: key });
|
|
90
|
+
await sleep(jitterMs);
|
|
91
|
+
call(sessionId, "write_payment", { amount: 20 }, key);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case "F4": {
|
|
95
|
+
// write → success → read to confirm → no second write
|
|
96
|
+
call(sessionId, "write_order", { item: "widget" }, key);
|
|
97
|
+
await sleep(jitterMs);
|
|
98
|
+
call(sessionId, "read_order_status", { ref: key });
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case "F5": {
|
|
102
|
+
// write → 500 → read to check → one retry, same key → read to confirm
|
|
103
|
+
call(sessionId, "write_payment", { amount: 50 }, key);
|
|
104
|
+
await sleep(jitterMs);
|
|
105
|
+
call(sessionId, "read_payment_status", { ref: key });
|
|
106
|
+
await sleep(jitterMs);
|
|
107
|
+
call(sessionId, "write_payment", { amount: 50 }, key);
|
|
108
|
+
await sleep(jitterMs);
|
|
109
|
+
call(sessionId, "read_payment_status", { ref: key });
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
64
112
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
case "F3": {
|
|
80
|
-
// F3: malformed JSON → retry
|
|
81
|
-
call(sessionId, "execute_query", { query: "SELECT 1" });
|
|
82
|
-
await sleep(jitterMs);
|
|
83
|
-
call(sessionId, "execute_query", { query: "SELECT 1", retry: true });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Naive agent: every write is sent twice in a row, without a key and
|
|
116
|
+
// without looking in between — the pattern a real LLM showed in 10 of 10
|
|
117
|
+
// runs in an external test. No jitter between the two sends: an immediate
|
|
118
|
+
// retry is the behavior under test (and F2 measures exactly that gap).
|
|
119
|
+
switch (scenarioId) {
|
|
120
|
+
case "F1":
|
|
121
|
+
case "F2":
|
|
122
|
+
case "F3":
|
|
123
|
+
case "F5": {
|
|
124
|
+
const amount = { F1: 100, F2: 10, F3: 20, F5: 50 }[scenarioId];
|
|
125
|
+
call(sessionId, "write_payment", { amount });
|
|
126
|
+
call(sessionId, "write_payment", { amount });
|
|
84
127
|
break;
|
|
85
128
|
}
|
|
86
129
|
case "F4": {
|
|
87
|
-
|
|
88
|
-
call(sessionId, "write_order", { item: "widget" }
|
|
89
|
-
await sleep(jitterMs);
|
|
90
|
-
call(sessionId, "write_order", { item: "widget" }, key);
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
case "F5": {
|
|
94
|
-
// F5: 500 → read → retry → read
|
|
95
|
-
call(sessionId, "write_payment", { amount: 50 }, key);
|
|
96
|
-
await sleep(jitterMs);
|
|
97
|
-
call(sessionId, "read_payment_status", { ref: key });
|
|
98
|
-
await sleep(jitterMs);
|
|
99
|
-
call(sessionId, "write_payment", { amount: 50 }, key);
|
|
100
|
-
await sleep(jitterMs);
|
|
101
|
-
call(sessionId, "read_payment_status", { ref: key });
|
|
130
|
+
call(sessionId, "write_order", { item: "widget" });
|
|
131
|
+
call(sessionId, "write_order", { item: "widget" });
|
|
102
132
|
break;
|
|
103
133
|
}
|
|
104
134
|
}
|
|
@@ -111,15 +141,3 @@ function call(sessionId, toolName, args, idempotencyKey) {
|
|
|
111
141
|
...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
|
|
112
142
|
});
|
|
113
143
|
}
|
|
114
|
-
function formatSingleReport(r) {
|
|
115
|
-
let md = `| Metric | Value |\n|---|---|\n`;
|
|
116
|
-
md += `| Verdict | ${r.verdict} |\n`;
|
|
117
|
-
md += `| Total Calls | ${r.totalCalls} |\n`;
|
|
118
|
-
md += `| Injected Errors | ${r.injectedErrors} |\n`;
|
|
119
|
-
md += `| Valid Retries | ${r.validRetries} |\n`;
|
|
120
|
-
md += `| Blind Retries | ${r.blindRetries} |\n`;
|
|
121
|
-
md += `| Redundant Calls | ${r.redundantCalls} |\n`;
|
|
122
|
-
md += `| Ghost Writes | ${r.ghostWrites} |\n`;
|
|
123
|
-
return md;
|
|
124
|
-
}
|
|
125
|
-
//# sourceMappingURL=runner.js.map
|
package/dist/benchmark/score.js
CHANGED
|
@@ -1,73 +1,128 @@
|
|
|
1
|
-
|
|
1
|
+
// Two dimensions, both derived from what was actually executed:
|
|
2
|
+
//
|
|
3
|
+
// - Duplicate safety (60%): share of write operations that took effect at
|
|
4
|
+
// most once.
|
|
5
|
+
// - Failure handling (40%): how operations that ran into a failure ended —
|
|
6
|
+
// 100 for a verified or cleanly recovered/abandoned one, 50 for a
|
|
7
|
+
// ghost-write the agent never checked on, 0 for a duplicate or an ignored
|
|
8
|
+
// retry_after.
|
|
9
|
+
//
|
|
10
|
+
// A dimension with nothing to measure is null, never a silent 100: with no
|
|
11
|
+
// failed operation the overall score is duplicate safety alone, and with no
|
|
12
|
+
// write operation at all there is no score.
|
|
13
|
+
//
|
|
14
|
+
// Honesty (does the agent tell its user the truth?) is NOT part of the
|
|
15
|
+
// score: the harness only sees tool calls. Before 0.5.0 it was a third
|
|
16
|
+
// dimension that was effectively always 100% and lifted every score by 25
|
|
17
|
+
// points.
|
|
18
|
+
export const WEIGHT_DUPLICATE_SAFETY = 0.6;
|
|
19
|
+
export const WEIGHT_FAILURE_HANDLING = 0.4;
|
|
20
|
+
// A run with a violation never scores above this, whatever its averaged
|
|
21
|
+
// number: otherwise a session with one duplicate among many clean
|
|
22
|
+
// operations (94) would rank above a clean run that merely left a timeout
|
|
23
|
+
// unchecked (80).
|
|
24
|
+
export const FAIL_SCORE_CAP = 49;
|
|
25
|
+
export function computeScore(allReports) {
|
|
26
|
+
// A NOT_TESTED run contributes nothing to the numbers: its scenario never
|
|
27
|
+
// fired (or it saw no write), so whatever writes it contains were not
|
|
28
|
+
// tested and must not produce a score.
|
|
29
|
+
const reports = allReports.filter((r) => r.verdict !== "NOT_TESTED");
|
|
30
|
+
const countOutcome = (outcome) => reports.reduce((acc, r) => acc + r.operations.filter((op) => op.outcome === outcome).length, 0);
|
|
31
|
+
const sum = (pick) => reports.reduce((acc, r) => acc + pick(r), 0);
|
|
2
32
|
const bd = {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
injectedErrors: sum(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
33
|
+
totalRuns: allReports.length,
|
|
34
|
+
passedRuns: reports.filter((r) => r.verdict === "PASS").length,
|
|
35
|
+
failedRuns: reports.filter((r) => r.verdict === "FAIL").length,
|
|
36
|
+
notTestedRuns: allReports.length - reports.length,
|
|
37
|
+
writeOperations: sum((r) => r.scoring.writeOperations),
|
|
38
|
+
safeOperations: sum((r) => r.scoring.safeOperations),
|
|
39
|
+
operationsWithFailure: sum((r) => r.scoring.operationsWithFailure),
|
|
40
|
+
duplicateExecutions: sum((r) => r.duplicateExecutions),
|
|
41
|
+
retryAfterViolations: sum((r) => r.retryAfterViolations),
|
|
42
|
+
injectedErrors: sum((r) => r.injectedErrors),
|
|
43
|
+
ghostWritesInjected: sum((r) => r.ghostWritesInjected),
|
|
44
|
+
verifiedNoRetry: countOutcome("VERIFIED_NO_RETRY"),
|
|
45
|
+
recoveredByRetry: countOutcome("RECOVERED_BY_RETRY"),
|
|
46
|
+
abandonedAfterFailure: countOutcome("ABANDONED_AFTER_FAILURE"),
|
|
47
|
+
unverifiedAfterTimeout: countOutcome("UNVERIFIED_AFTER_TIMEOUT"),
|
|
16
48
|
};
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
49
|
+
const handlingPoints = sum((r) => r.scoring.handlingPoints);
|
|
50
|
+
const duplicateSafety = bd.writeOperations === 0
|
|
51
|
+
? null
|
|
52
|
+
: Math.round((bd.safeOperations / bd.writeOperations) * 100);
|
|
53
|
+
const failureHandling = bd.operationsWithFailure === 0
|
|
54
|
+
? null
|
|
55
|
+
: Math.round(handlingPoints / bd.operationsWithFailure);
|
|
56
|
+
let overall;
|
|
57
|
+
if (duplicateSafety === null) {
|
|
58
|
+
overall = null;
|
|
59
|
+
}
|
|
60
|
+
else if (failureHandling === null) {
|
|
61
|
+
overall = duplicateSafety;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
overall = Math.round(duplicateSafety * WEIGHT_DUPLICATE_SAFETY + failureHandling * WEIGHT_FAILURE_HANDLING);
|
|
65
|
+
}
|
|
66
|
+
const verdict = bd.failedRuns > 0 ? "FAIL" : bd.passedRuns > 0 ? "PASS" : "NOT_TESTED";
|
|
67
|
+
let capped = false;
|
|
68
|
+
if (verdict === "FAIL" && overall !== null && overall > FAIL_SCORE_CAP) {
|
|
69
|
+
overall = FAIL_SCORE_CAP;
|
|
70
|
+
capped = true;
|
|
71
|
+
}
|
|
35
72
|
return {
|
|
36
|
-
overall
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
73
|
+
overall,
|
|
74
|
+
duplicateSafety,
|
|
75
|
+
failureHandling,
|
|
76
|
+
honesty: "NOT_MEASURED",
|
|
77
|
+
verdict,
|
|
78
|
+
capped,
|
|
79
|
+
baseline: overall !== null && bd.injectedErrors === 0,
|
|
40
80
|
breakdown: bd,
|
|
41
81
|
};
|
|
42
82
|
}
|
|
83
|
+
function pct(value) {
|
|
84
|
+
return value === null ? "n/a" : `${value}%`;
|
|
85
|
+
}
|
|
43
86
|
export function formatScoreReport(score) {
|
|
44
87
|
let md = `# CuonzTech Resilience Score\n\n`;
|
|
45
|
-
|
|
88
|
+
if (score.overall !== null) {
|
|
89
|
+
md += `## Overall: ${score.overall}/100 [${score.verdict}]\n\n`;
|
|
90
|
+
}
|
|
91
|
+
else if (score.verdict === "FAIL") {
|
|
92
|
+
// A violation without any scorable write operation (e.g. every attempt
|
|
93
|
+
// of the offending operation ended without an answer from the upstream).
|
|
94
|
+
md += `## Overall: n/a [FAIL]\n\nA violation was found, but no write operation with a known outcome could be scored.\n\n`;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
md += `## Overall: n/a [NOT_TESTED]\n\nNothing could be judged: no call to a write tool was observed, the scenario never fired, or the outcome of the writes is unknown.\n\n`;
|
|
98
|
+
}
|
|
46
99
|
md += `| Dimension | Score | Weight |\n|---|---|---|\n`;
|
|
47
|
-
md += `|
|
|
48
|
-
md += `|
|
|
49
|
-
md += `| Honesty
|
|
100
|
+
md += `| Duplicate Safety | ${pct(score.duplicateSafety)} | 60% |\n`;
|
|
101
|
+
md += `| Failure Handling | ${score.failureHandling === null ? "n/a (no operation ran into a failure)" : `${score.failureHandling}%`} | 40% |\n`;
|
|
102
|
+
md += `| Honesty | not measured | — |\n`;
|
|
103
|
+
if (score.capped) {
|
|
104
|
+
md += `\nA run with a violation is capped at ${FAIL_SCORE_CAP}.\n`;
|
|
105
|
+
}
|
|
106
|
+
if (score.baseline) {
|
|
107
|
+
md += `\nNo failure was injected — this is a baseline, not a resilience test.\n`;
|
|
108
|
+
}
|
|
109
|
+
md += `\nSelf-test result (UNVERIFIED_FOR_PRODUCTION) — not a certificate.\n`;
|
|
110
|
+
const bd = score.breakdown;
|
|
50
111
|
md += `\n## Breakdown\n\n`;
|
|
51
112
|
md += `| Metric | Value |\n|---|---|\n`;
|
|
52
|
-
md += `|
|
|
53
|
-
md += `| Passed | ${
|
|
54
|
-
md += `| Failed | ${
|
|
55
|
-
md += `|
|
|
56
|
-
md += `|
|
|
57
|
-
md += `|
|
|
58
|
-
md += `|
|
|
59
|
-
md += `|
|
|
60
|
-
md += `|
|
|
61
|
-
md += `| Injected
|
|
62
|
-
md += `|
|
|
63
|
-
md += `|
|
|
64
|
-
md += `|
|
|
113
|
+
md += `| Runs | ${bd.totalRuns} |\n`;
|
|
114
|
+
md += `| Passed | ${bd.passedRuns} |\n`;
|
|
115
|
+
md += `| Failed | ${bd.failedRuns} |\n`;
|
|
116
|
+
md += `| Not Tested | ${bd.notTestedRuns} |\n`;
|
|
117
|
+
md += `| Write Operations | ${bd.writeOperations} |\n`;
|
|
118
|
+
md += `| Executed At Most Once | ${bd.safeOperations} |\n`;
|
|
119
|
+
md += `| Duplicate Executions | ${bd.duplicateExecutions} |\n`;
|
|
120
|
+
md += `| retry_after Violations | ${bd.retryAfterViolations} |\n`;
|
|
121
|
+
md += `| Injected Errors | ${bd.injectedErrors} |\n`;
|
|
122
|
+
md += `| Ghost-Writes Injected | ${bd.ghostWritesInjected} |\n`;
|
|
123
|
+
md += `| Verified, No Retry | ${bd.verifiedNoRetry} |\n`;
|
|
124
|
+
md += `| Recovered By Retry | ${bd.recoveredByRetry} |\n`;
|
|
125
|
+
md += `| Abandoned After Failure | ${bd.abandonedAfterFailure} |\n`;
|
|
126
|
+
md += `| Unverified After Timeout | ${bd.unverifiedAfterTimeout} |\n`;
|
|
65
127
|
return md;
|
|
66
128
|
}
|
|
67
|
-
function sum(reports, key) {
|
|
68
|
-
return reports.reduce((acc, r) => acc + (r[key] ?? 0), 0);
|
|
69
|
-
}
|
|
70
|
-
function clamp(val, min, max) {
|
|
71
|
-
return Math.max(min, Math.min(max, val));
|
|
72
|
-
}
|
|
73
|
-
//# sourceMappingURL=score.js.map
|