Files
openclaw/scripts/mantis/build-web-ui-chat-evidence.mjs
Ayaan Zaidi 352095882e fix(mantis): enforce verdict-expectation coherence and publish agent analysis files (#128124)
* fix(mantis): enforce verdict-expectation coherence and publish agent analysis files

Mantis run 32619081130 on #127989 published an overall `pass` while its own
manifest recorded that the candidate expectation was not observed: per-lane
`status` was mechanical capture success and the agent's judgment lived only
in `expected` prose, so nothing reconciled the two before publication.

- `mantis-evidence.json` schemaVersion 2: each comparison lane carries a
  required boolean `expectationMet`. The desktop agent sets it in the same
  manifest edit as `expected`; mechanical producers (Telegram live, web UI,
  Slack, Discord) derive it from lane status.
- `scripts/mantis/publish-pr-evidence.mjs` is the single enforcement owner:
  it requires the booleans, recomputes `pass`/`outcome`, downgrades a
  contradictory pass claim to `fail`, and renders a visible "verdict
  downgraded" note. The desktop workflow invokes it with `--validate-only`
  before upload or comment.
- Agent top-level `*.json`/`*.md` analysis files (assertions, comparisons,
  recipe suggestion) now survive the quarantine rebuild and upload, so cited
  evidence actually exists in the artifact.

* fix(mantis): derive expectations from trusted lane facts
2026-08-23 12:42:34 +05:30

177 lines
5.4 KiB
JavaScript

#!/usr/bin/env node
// Builds a Mantis evidence manifest from Control UI web chat proof artifacts.
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
function parseArgs(argv) {
const args = {};
for (let index = 0; index < argv.length; index += 1) {
const key = argv[index];
if (!key.startsWith("--")) {
throw new Error(`Unexpected argument: ${key}`);
}
const name = key.slice(2).replaceAll("-", "_");
const value = argv[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`Missing value for ${key}`);
}
args[name] = value;
index += 1;
}
return args;
}
function normalizeStatus(value) {
const normalized = String(value ?? "")
.trim()
.toLowerCase();
if (normalized === "pass") {
return "pass";
}
if (normalized === "fail") {
return "fail";
}
throw new Error(`Unsupported web UI chat proof status: ${value}`);
}
function artifactEntry({ inline = false, kind, label, path: artifactPath, required, targetPath }) {
return {
kind,
lane: "candidate",
label,
path: artifactPath,
targetPath,
required,
...(inline ? { alt: label, inline: true, width: 900 } : {}),
};
}
export function buildWebUiChatEvidenceManifest({ candidateRef, candidateSha, status }) {
const passed = status === "pass";
return {
schemaVersion: 2,
id: "web-ui-chat-proof",
title: "Mantis Web UI Chat Proof",
summary:
"Mantis ran the OpenClaw Control UI chat proof against the candidate ref, sent a message through the mocked Gateway, rendered the final reply in the browser, and captured browser artifacts for review.",
scenario: "web-ui-chat-proof",
comparison: {
candidate: {
...(candidateSha ? { sha: candidateSha } : {}),
...(candidateRef ? { ref: candidateRef } : {}),
expected: "Control UI chat sends through the Gateway and renders the final reply",
expectationMet: passed,
status,
fixed: passed,
},
outcome: passed ? "pass" : "fail",
pass: passed,
},
artifacts: [
artifactEntry({
inline: true,
kind: "desktopScreenshot",
label: "Control UI web chat proof",
path: "web-ui-chat.png",
required: passed,
targetPath: "web-ui-chat.png",
}),
artifactEntry({
kind: "fullVideo",
label: "Control UI web chat recording",
path: "web-ui-chat.webm",
required: false,
targetPath: "web-ui-chat.webm",
}),
artifactEntry({
kind: "metadata",
label: "Control UI web chat proof metadata",
path: "web-ui-chat-proof.json",
required: passed,
targetPath: "web-ui-chat-proof.json",
}),
artifactEntry({
kind: "metadata",
label: "Control UI web chat Vitest log",
path: "vitest.log",
required: false,
targetPath: "vitest.log",
}),
{
kind: "report",
lane: "run",
label: "Mantis web UI chat report",
path: "mantis-report.md",
targetPath: "mantis-report.md",
},
],
};
}
function renderReport({ candidateRef, candidateSha, outputDir, status }) {
const artifactStatus = (artifactPath) =>
existsSync(path.join(outputDir, artifactPath)) ? "present" : "missing";
return [
"# Mantis Web UI Chat Proof",
"",
`Status: ${status}`,
`Candidate ref: ${candidateRef || "unspecified"}`,
`Candidate SHA: ${candidateSha || "unspecified"}`,
"",
"## Scenario",
"",
"OpenClaw Control UI chat was loaded in a browser with the mocked Gateway harness. The proof sends a chat message through the GUI, verifies the `chat.send` request, emits a final Gateway reply, and waits for the reply to render in the web chat thread.",
"",
"## Artifacts",
"",
`- Screenshot: \`web-ui-chat.png\` (${artifactStatus("web-ui-chat.png")})`,
`- Recording: \`web-ui-chat.webm\` (${artifactStatus("web-ui-chat.webm")})`,
`- Proof metadata: \`web-ui-chat-proof.json\` (${artifactStatus("web-ui-chat-proof.json")})`,
`- Vitest log: \`vitest.log\` (${artifactStatus("vitest.log")})`,
"",
].join("\n");
}
export function writeWebUiChatEvidence(rawArgs = process.argv.slice(2)) {
const args = parseArgs(rawArgs);
if (!args.output_dir) {
throw new Error("Missing --output-dir.");
}
if (!args.status) {
throw new Error("Missing --status.");
}
const outputDir = path.resolve(args.output_dir);
mkdirSync(outputDir, { recursive: true });
const status = normalizeStatus(args.status);
const manifest = buildWebUiChatEvidenceManifest({
candidateRef: args.candidate_ref,
candidateSha: args.candidate_sha,
status,
});
const reportPath = path.join(outputDir, "mantis-report.md");
writeFileSync(
reportPath,
renderReport({
candidateRef: args.candidate_ref,
candidateSha: args.candidate_sha,
outputDir,
status,
}),
"utf8",
);
const manifestPath = path.join(outputDir, "mantis-evidence.json");
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
return { manifest, manifestPath, reportPath };
}
const executedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
if (executedPath === fileURLToPath(import.meta.url)) {
try {
writeWebUiChatEvidence();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}