Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/REPRODUCIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ The verification gate covers TypeScript compilation, schema reconciliation, pack
secret scanning, and CLI smoke tests. The exact package version resolved by a consumer should be
recorded separately with `npm view @agentbiz/quant-research dist-tags versions --json`.

The distributed [`examples/reproducibility-manifest.json`](../examples/reproducibility-manifest.json)
pins the SHA-256 bytes of every JSON example and the receipt artifact digests produced by the
replay commands recorded in that manifest. The test suite checks both the fixture bytes and those
digests; the package content check also requires the manifest to be present in the published archive.

## Deterministic examples

```sh
Expand Down
76 changes: 76 additions & 0 deletions examples/reproducibility-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"manifestVersion": "agentbiz.reproducibility.v1",
"package": {
"name": "@agentbiz/quant-research",
"version": "0.1.0-alpha.5"
},
"fixtures": [
{
"path": "examples/tam-bundle.json",
"sha256": "sha256:b2a95a6360ab34b81303678882742a5186600148cd546c9166ccec1f21f1d103"
},
{
"path": "examples/tam-dataset.json",
"sha256": "sha256:5986366b91b3860a4d4bb90b4934b2efb38f4744dd5f0454af58cf2b0c0dc84a"
},
{
"path": "examples/tam-instrument.json",
"sha256": "sha256:24c63c514d8ecbfdb3eda3ce82a4ca6b907c0cf98022365809077236e1b32cbc"
},
{
"path": "examples/tam-measurement.json",
"sha256": "sha256:a03a599192466a1e464f51b736beaba3953847963df7e7b3b08129685b0be3a7"
},
{
"path": "examples/tam-observed.json",
"sha256": "sha256:5481cba9000d627513df7f66fd06177c9d753a1906c1d128755aad79d5b7c22d"
},
{
"path": "examples/tam-recode.json",
"sha256": "sha256:89c070bac8eef30ed1b587dad191e0a247275c61fd7a850f4b71a420cb0463e5"
},
{
"path": "examples/tam-topic-card.json",
"sha256": "sha256:5ee4ca7bfc930c85fb9bbb7d956a5c13cb6b6c054c343d454ffe2f0619bea98a"
}
],
"replays": [
{
"name": "compile-instrument",
"argv": ["compile", "examples/tam-instrument.json"],
"artifactDigest": "sha256:a43b6de17fb5577ca267016c0a9c6dc981e00c2ebc4edc9bf681cc9b958e8729"
},
{
"name": "compile-topic-card",
"argv": ["compile", "examples/tam-topic-card.json", "--instrument", "examples/tam-instrument.json"],
"artifactDigest": "sha256:efe2b3e03508d297c7a5df3f694bc43eb064c6709775082e773ff56c8313a052"
},
{
"name": "compile-measurement",
"argv": [
"compile",
"examples/tam-measurement.json",
"--instrument",
"examples/tam-instrument.json",
"--topic-card",
"examples/tam-topic-card.json"
],
"artifactDigest": "sha256:91123f48e88e03da71bb08e71ab1c83167f24ecc5adda180e574860dd9e283f8"
},
{
"name": "gap-map",
"argv": ["gap", "examples/tam-instrument.json", "examples/tam-observed.json"],
"artifactDigest": "sha256:7cc03bd9a4030da54634cbd13ede33e81dec7eb9d38535148cd866b0b0233ce6"
},
{
"name": "dataset-audit",
"argv": ["audit", "examples/tam-instrument.json", "examples/tam-dataset.json"],
"artifactDigest": "sha256:36987e45fb97a662fc83e95d085610959714921d8e9af7ef1036bfa132ca809b"
},
{
"name": "recode",
"argv": ["recode", "examples/tam-instrument.json", "examples/tam-dataset.json"],
"artifactDigest": "sha256:d6b7385dd4b5b2334eaced86ed17979f7930576eb64cba5f4a117ee811734784"
}
]
}
1 change: 1 addition & 0 deletions scripts/check-pack.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const required = [
"schemas/receipt.v1.schema.json",
"examples/tam-instrument.json",
"examples/tam-bundle.json",
"examples/reproducibility-manifest.json",
];
const missing = required.filter((path) => !paths.includes(path));
if (missing.length > 0) {
Expand Down
76 changes: 76 additions & 0 deletions test/reproducibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { createHash } from "node:crypto";
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { runCli, type CliIO } from "../src/cli.js";

interface ReproducibilityManifest {
readonly manifestVersion: string;
readonly package: { readonly name: string; readonly version: string };
readonly fixtures: readonly { readonly path: string; readonly sha256: string }[];
readonly replays: readonly {
readonly name: string;
readonly argv: readonly string[];
readonly artifactDigest: string;
}[];
}

class MemoryStream {
public chunks: string[] = [];

public write(chunk: string): boolean {
this.chunks.push(chunk);
return true;
}

public text(): string {
return this.chunks.join("");
}
}

const examplesRoot = fileURLToPath(new URL("../examples/", import.meta.url));
const manifestPath = join(examplesRoot, "reproducibility-manifest.json");
const packagePath = fileURLToPath(new URL("../package.json", import.meta.url));

async function loadJson(path: string): Promise<unknown> {
return JSON.parse(await readFile(path, "utf8")) as unknown;
}

async function sha256(path: string): Promise<string> {
return `sha256:${createHash("sha256").update(await readFile(path)).digest("hex")}`;
}

describe("reproducibility manifest", () => {
it("pins every distributed example fixture", async () => {
const manifest = (await loadJson(manifestPath)) as ReproducibilityManifest;
const packageJson = (await loadJson(packagePath)) as { name: string; version: string };
const fixturePaths = manifest.fixtures.map((fixture) => fixture.path).sort();
const exampleFiles = (await readdir(examplesRoot))
.filter((file) => file.endsWith(".json") && file !== "reproducibility-manifest.json")
.map((file) => `examples/${file}`)
.sort();

expect(manifest.manifestVersion).toBe("agentbiz.reproducibility.v1");
expect(manifest.package).toEqual({ name: packageJson.name, version: packageJson.version });
expect(fixturePaths).toEqual(exampleFiles);

for (const fixture of manifest.fixtures) {
expect(fixture.path.startsWith("examples/")).toBe(true);
expect(fixture.path.includes("..")).toBe(false);
expect(await sha256(join(examplesRoot, fixture.path.slice("examples/".length)))).toBe(fixture.sha256);
}
});

it("pins deterministic receipt artifact digests for replay commands", async () => {
for (const replay of (await loadJson(manifestPath) as ReproducibilityManifest).replays) {
const stdout = new MemoryStream();
const stderr = new MemoryStream();
const io: CliIO = { stdout, stderr };
const code = await runCli(replay.argv, io);
expect(code, `${replay.name}: ${stderr.text()}`).toBe(0);
const payload = JSON.parse(stdout.text()) as { receipt?: { artifactDigest?: string } };
expect(payload.receipt?.artifactDigest, replay.name).toBe(replay.artifactDigest);
}
});
});
Loading