mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 11:56:12 +00:00
* feat(artifacts): compare artifacts by Evidence reference and from the CLI
compare_artifacts made agents send back the complete inventory Evidence
that REA already retains, and the CLI had no artifact comparison.
Each side of compare_artifacts now takes exactly one of: complete inline
Evidence (unchanged), {kind: "retained-evidence", evidence_id}, or
{kind: "evidence-file", path, evidence_id?}. The two references are the
sources inspect_analysis_view accepts; both tools now compose one
definition in evidenceReference.ts. A summary still resolves to its exact
retained parent, and every source form gives the same comparison Evidence.
An unresolvable retained reference is refused with a typed missing
reference error.
The application layer owns source resolution and the comparison Evidence,
so MCP and the new `rea compare-artifacts` command share one workflow.
readEvidenceFileSource reports bundle-selection refusals at the caller's
own field path.
* refactor(artifacts): resolve compare_artifacts sides like compare_functions
compare_functions, compare_wasm_interfaces and the JavaScript application
comparisons name each side with evidenceInputSchema and resolve it with
resolvePairedEvidenceRequest. compare_artifacts reached the same result
for inline and retained sides through its own dispatch.
evidenceSourceInputSchema now extends evidenceInputSchema with the
Evidence-file reference, and resolvePairedEvidenceSources sends inline and
retained sides to the same resolveEvidenceInput and file sides to the
shared Evidence file reader. compare_artifacts uses both, so the forms it
shares with compare_functions are parsed and resolved by one owner. The
advertised input schema is unchanged.
* docs(cli): point rea compare to compare-artifacts for inventory contents
rea compare (compare_bundles) compares records by Evidence identity and
explicit pairs and never reads artifact contents, so two build
inventories stay unresolved there. Its help and the CLI guide now name
compare-artifacts for that comparison.
* fix(artifacts): resolve summary parents within Evidence source bundles
* perf(evidence): reuse file-owned immutable authentication proofs
* test(evidence): inject failures at both authentication boundaries
---------
Co-authored-by: Entrpi <entrpi@proton.me>
115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
import { expect, it } from "vitest";
|
|
|
|
import { ARTIFACT_COMPARISON_EXAMPLE } from "../../../src/contracts/artifactComparisonExample.js";
|
|
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
|
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
|
|
import { advertisedSchemaErrors } from "../../fixtures/mcpAdvertisedSchemaValidity.js";
|
|
|
|
const evidence = ARTIFACT_COMPARISON_EXAMPLE.left;
|
|
const retained = {
|
|
kind: "retained-evidence",
|
|
evidence_id: evidence.evidence_id,
|
|
} as const;
|
|
const file = {
|
|
kind: "evidence-file",
|
|
path: join(tmpdir(), "evidence.json"),
|
|
} as const;
|
|
|
|
it("advertises one spelling per compare_artifacts source and rejects the same sides the parser rejects", async () => {
|
|
const contract = toolContract("compare_artifacts");
|
|
const server = new McpServer({ name: "artifact-sources", version: "0" });
|
|
const client = new Client({ name: "artifact-sources", version: "0" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
server.registerTool(
|
|
contract.name,
|
|
toolRegistrationOptions(contract),
|
|
async () => ({ content: [] }),
|
|
);
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const { tools } = await client.listTools();
|
|
expect(advertisedSchemaErrors(tools)).toEqual([]);
|
|
const tool = tools.find(({ name }) => name === contract.name);
|
|
if (tool === undefined) throw new Error("compare_artifacts not advertised");
|
|
expect(tool.inputSchema.examples).toEqual([
|
|
{
|
|
left: {
|
|
kind: "retained-evidence",
|
|
evidence_id: ARTIFACT_COMPARISON_EXAMPLE.left.evidence_id,
|
|
},
|
|
right: {
|
|
kind: "retained-evidence",
|
|
evidence_id: ARTIFACT_COMPARISON_EXAMPLE.right.evidence_id,
|
|
},
|
|
},
|
|
]);
|
|
const validate = new Ajv2020({
|
|
strict: false,
|
|
validateFormats: false,
|
|
}).compile(tool.inputSchema);
|
|
const sides: readonly (readonly [string, unknown, boolean])[] = [
|
|
["complete Evidence", evidence, true],
|
|
["retained reference", retained, true],
|
|
["Evidence file", file, true],
|
|
[
|
|
"selected bundle record",
|
|
{ ...file, evidence_id: evidence.evidence_id },
|
|
true,
|
|
],
|
|
["second inline spelling", { kind: "inline", evidence }, false],
|
|
["retained reference without ID", { kind: "retained-evidence" }, false],
|
|
["two references at once", { ...retained, path: file.path }, false],
|
|
[
|
|
"Evidence carrying a source kind",
|
|
{ ...evidence, kind: "retained-evidence" },
|
|
false,
|
|
],
|
|
["relative Evidence file", { ...file, path: "evidence.json" }, false],
|
|
["empty side", {}, false],
|
|
];
|
|
for (const [label, side, accepted] of sides)
|
|
for (const input of [
|
|
{ left: side, right: retained },
|
|
{ left: retained, right: side },
|
|
]) {
|
|
expect(contract.inputSchema.safeParse(input).success, label).toBe(
|
|
accepted,
|
|
);
|
|
expect(validate(input), label).toBe(accepted);
|
|
}
|
|
expect(contract.inputSchema.safeParse({ left: retained }).success).toBe(
|
|
false,
|
|
);
|
|
expect(validate({ left: retained })).toBe(false);
|
|
// compare_functions is the precedent for the inline and retained forms;
|
|
// only the Evidence-file reference extends it.
|
|
const functions = toolContract("compare_functions");
|
|
const shared = sides.filter(
|
|
([, side]) =>
|
|
typeof side !== "object" ||
|
|
side === null ||
|
|
!("kind" in side) ||
|
|
side.kind !== "evidence-file",
|
|
);
|
|
expect(shared).toHaveLength(sides.length - 3);
|
|
for (const [label, side] of shared)
|
|
expect(
|
|
functions.inputSchema.safeParse({ left: side, right: retained })
|
|
.success,
|
|
label,
|
|
).toBe(
|
|
contract.inputSchema.safeParse({ left: side, right: retained }).success,
|
|
);
|
|
} finally {
|
|
await Promise.allSettled([client.close(), server.close()]);
|
|
}
|
|
});
|