mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 17:32:41 -10:00
feat(slice-05): provenance capture in v1+v2 bundle assemblers (Item 1 part 2) (OPR.0.3.2.5)
Extends AssembleOptions and PodAssembleOptions with optional provenance input; assemblers populate manifest.provenance at create time. provenance.createdAt mirrors root manifest.createdAt unless caller pre-sets it (caller value wins for test determinism). Missing opts.provenance keeps backward compat: no provenance recorded on the manifest. Builds on82d39715(Item 1 part 1: type layer). Route + CLI plumbing land in next commit (Item 1 close-out). Files touched: - packages/daemon/src/domain/bundle-assembler.ts (v1) — AssembleOptions extended with optional provenance; capture at the assemble() manifest-construction site - packages/daemon/src/domain/pod-bundle-assembler.ts (v2) — same shape for PodAssembleOptions and the v2 manifest-construction site - packages/daemon/test/bundle-assembler.test.ts — 3 new tests covering full capture, caller-set-createdAt-wins, omits-when-missing - packages/daemon/test/pod-bundle-assembler.test.ts — 3 mirrored tests for v2 with the existing mockFs harness Diff: 4 files, +188 / -5 lines. 4572 tests pass post-change (no regressions). PRD bounce conditions verified: - No schema bump (consumes optional manifest field added in82d39715) - No new SQLite migrations - No event types - No core-routing or bootstrap mutation - No sidebar entries touched - scope-and-versioning sections 1 and 3 coherence re-audit clean Watched risk applied — audit-every-layer (function-level call sites AND module-level constants): the opts-injection pattern means assemblers READ provenance only from opts at assemble() call time. NO module-level constants read os.hostname, process.env, or daemon or cli versions at import time in either assembler. Verified via import grep: bundle-assembler imports node:path + 3 internal types; pod-bundle-assembler imports node:path + node:crypto + 5 internal types. Caller-side reads (route handler, CLI) land in the next commit where call-time injection is the correct boundary. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
82d397156f
commit
3b7432e41c
4 files changed
+188
-5
No files matched your search
@@ -2,7 +2,7 @@ import nodePath from "node:path";
|
||||
import { LegacyRigSpecCodec as RigSpecCodec } from "./rigspec-codec.js"; // TODO: AS-T08b — migrate to pod-aware RigSpec
|
||||
import { LegacyRigSpecSchema as RigSpecSchema } from "./rigspec-schema.js"; // TODO: AS-T08b — migrate to pod-aware RigSpec
|
||||
// TODO: AS-T12 — migrate to pod-aware bundle types
|
||||
import { serializeLegacyBundleManifest as serializeBundleManifest, type LegacyBundleManifest as BundleManifest } from "./bundle-types.js";
|
||||
import { serializeLegacyBundleManifest as serializeBundleManifest, type LegacyBundleManifest as BundleManifest, type BundleProvenance } from "./bundle-types.js";
|
||||
|
||||
export interface AssemblerFsOps {
|
||||
readFile: (path: string) => string;
|
||||
@@ -26,6 +26,15 @@ export interface AssembleOptions {
|
||||
outputDir: string;
|
||||
bundleName: string;
|
||||
bundleVersion: string;
|
||||
/**
|
||||
* Optional Item-1 provenance input. Caller fills the fields it knows
|
||||
* (sourceHost, authorSession, daemonVersion, cliVersion, sourceRigId,
|
||||
* sourceRigName, notes). The assembler mirrors root createdAt into
|
||||
* provenance.createdAt unless caller pre-set it (caller value wins
|
||||
* for test determinism). Missing block = no provenance recorded
|
||||
* (backward compat).
|
||||
*/
|
||||
provenance?: BundleProvenance;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,14 +113,21 @@ export class LegacyBundleAssembler {
|
||||
}
|
||||
|
||||
// Generate manifest (no integrity — P7-T02 adds it)
|
||||
const createdAt = new Date().toISOString();
|
||||
const manifest: BundleManifest = {
|
||||
schemaVersion: 1,
|
||||
name: opts.bundleName,
|
||||
version: opts.bundleVersion,
|
||||
createdAt: new Date().toISOString(),
|
||||
createdAt,
|
||||
rigSpec: "rig.yaml",
|
||||
packages: packageEntries,
|
||||
};
|
||||
if (opts.provenance) {
|
||||
manifest.provenance = {
|
||||
...opts.provenance,
|
||||
createdAt: opts.provenance.createdAt ?? createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
// Write bundle.yaml
|
||||
this.fs.writeFile(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
||||
import { RigSpecCodec } from "./rigspec-codec.js";
|
||||
import { RigSpecSchema } from "./rigspec-schema.js";
|
||||
import { resolveAgentRef, type AgentResolverFsOps } from "./agent-resolver.js";
|
||||
import { serializePodBundleManifest, type PodBundleManifest, type PodBundleAgentEntry, type PodBundleAgentImportEntry } from "./bundle-types.js";
|
||||
import { serializePodBundleManifest, type PodBundleManifest, type PodBundleAgentEntry, type PodBundleAgentImportEntry, type BundleProvenance } from "./bundle-types.js";
|
||||
import type { RigSpec, StartupBlock } from "./types.js";
|
||||
|
||||
export interface PodAssemblerFsOps extends AgentResolverFsOps {
|
||||
@@ -19,6 +19,15 @@ export interface PodAssembleOptions {
|
||||
outputDir: string;
|
||||
bundleName: string;
|
||||
bundleVersion: string;
|
||||
/**
|
||||
* Optional Item-1 provenance input. Caller fills the fields it knows
|
||||
* (sourceHost, authorSession, daemonVersion, cliVersion, sourceRigId,
|
||||
* sourceRigName, notes). The assembler mirrors root createdAt into
|
||||
* provenance.createdAt unless caller pre-set it (caller value wins
|
||||
* for test determinism). Missing block = no provenance recorded
|
||||
* (backward compat).
|
||||
*/
|
||||
provenance?: BundleProvenance;
|
||||
}
|
||||
|
||||
export interface PodAssembleResult {
|
||||
@@ -163,15 +172,22 @@ export class PodBundleAssembler {
|
||||
this.fs.writeFile(nodePath.join(opts.outputDir, "rig.yaml"), rewrittenRigYaml);
|
||||
|
||||
// 4. Build manifest
|
||||
const createdAt = new Date().toISOString();
|
||||
const manifest: PodBundleManifest = {
|
||||
schemaVersion: 2,
|
||||
name: opts.bundleName,
|
||||
version: opts.bundleVersion,
|
||||
createdAt: new Date().toISOString(),
|
||||
createdAt,
|
||||
rigSpec: "rig.yaml",
|
||||
agents: agentEntries,
|
||||
cultureFile: rigSpec.cultureFile,
|
||||
};
|
||||
if (opts.provenance) {
|
||||
manifest.provenance = {
|
||||
...opts.provenance,
|
||||
createdAt: opts.provenance.createdAt ?? createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
// Write manifest
|
||||
this.fs.writeFile(
|
||||
|
||||
@@ -5,7 +5,7 @@ import os from "node:os";
|
||||
// TODO: AS-T12 — migrate to pod-aware bundle assembler
|
||||
import { LegacyBundleAssembler as BundleAssembler, type AssemblerFsOps } from "../src/domain/bundle-assembler.js";
|
||||
// TODO: AS-T12 — migrate to pod-aware bundle types
|
||||
import { parseLegacyBundleManifest as parseBundleManifest, validateLegacyBundleManifest as validateBundleManifest, normalizeLegacyBundleManifest as normalizeBundleManifest } from "../src/domain/bundle-types.js";
|
||||
import { parseLegacyBundleManifest as parseBundleManifest, validateLegacyBundleManifest as validateBundleManifest, normalizeLegacyBundleManifest as normalizeBundleManifest, type BundleProvenance } from "../src/domain/bundle-types.js";
|
||||
|
||||
const VALID_SPEC = `
|
||||
schema_version: 1
|
||||
@@ -241,4 +241,80 @@ describe("BundleAssembler", () => {
|
||||
const normalized = normalizeBundleManifest(parsed);
|
||||
expect(normalized.packages[0]!.originalSources).toEqual(["local:./a", "local:./b"]);
|
||||
});
|
||||
|
||||
// Item 1 — provenance capture (slice-05 Checkpoint 2 part 2)
|
||||
it("captures full provenance from opts into manifest", () => {
|
||||
const specPath = writeSpec();
|
||||
const pkgDir = writePkg("pkg", { "package.yaml": "name: pkg" });
|
||||
const outputDir = path.join(tmpDir, "staging");
|
||||
const assembler = new BundleAssembler({ fsOps: realFsOps() });
|
||||
|
||||
const provenance: BundleProvenance = {
|
||||
sourceHost: "test-host.local",
|
||||
authorSession: "velocity-driver@openrig-velocity",
|
||||
sourceRigId: "01KQEQPN4MQJN0DHBM5CQ0N8D7",
|
||||
sourceRigName: "openrig-velocity",
|
||||
daemonVersion: "0.3.2",
|
||||
cliVersion: "0.3.2",
|
||||
notes: "checkpoint-2-part-2 fixture",
|
||||
};
|
||||
const manifest = assembler.assemble({
|
||||
specPath, outputDir, bundleName: "test", bundleVersion: "1.0",
|
||||
packages: [{ name: "pkg", version: "1.0", sourcePath: pkgDir, originalSource: "local:./pkg", manifestHash: "h1" }],
|
||||
provenance,
|
||||
});
|
||||
|
||||
expect(manifest.provenance).toBeDefined();
|
||||
expect(manifest.provenance?.sourceHost).toBe("test-host.local");
|
||||
expect(manifest.provenance?.authorSession).toBe("velocity-driver@openrig-velocity");
|
||||
expect(manifest.provenance?.daemonVersion).toBe("0.3.2");
|
||||
expect(manifest.provenance?.cliVersion).toBe("0.3.2");
|
||||
expect(manifest.provenance?.notes).toBe("checkpoint-2-part-2 fixture");
|
||||
// createdAt mirrors root when opts.provenance.createdAt unset
|
||||
expect(manifest.provenance?.createdAt).toBe(manifest.createdAt);
|
||||
|
||||
// Round-trip via the on-disk bundle.yaml
|
||||
const diskYaml = fs.readFileSync(path.join(outputDir, "bundle.yaml"), "utf-8");
|
||||
const parsed = parseBundleManifest(diskYaml);
|
||||
const validation = validateBundleManifest(parsed, { requireIntegrity: false });
|
||||
expect(validation.valid).toBe(true);
|
||||
const normalized = normalizeBundleManifest(parsed);
|
||||
expect(normalized.provenance?.sourceHost).toBe("test-host.local");
|
||||
expect(normalized.provenance?.notes).toBe("checkpoint-2-part-2 fixture");
|
||||
});
|
||||
|
||||
it("respects opts.provenance.createdAt when caller pre-sets it (test determinism)", () => {
|
||||
const specPath = writeSpec();
|
||||
const pkgDir = writePkg("pkg", { "package.yaml": "name: pkg" });
|
||||
const outputDir = path.join(tmpDir, "staging");
|
||||
const assembler = new BundleAssembler({ fsOps: realFsOps() });
|
||||
|
||||
const fixedCreatedAt = "2026-01-01T00:00:00Z";
|
||||
const manifest = assembler.assemble({
|
||||
specPath, outputDir, bundleName: "test", bundleVersion: "1.0",
|
||||
packages: [{ name: "pkg", version: "1.0", sourcePath: pkgDir, originalSource: "local:./pkg", manifestHash: "h1" }],
|
||||
provenance: { createdAt: fixedCreatedAt, sourceHost: "h" },
|
||||
});
|
||||
|
||||
expect(manifest.provenance?.createdAt).toBe(fixedCreatedAt);
|
||||
// root createdAt is independent (real-time stamp)
|
||||
expect(manifest.createdAt).not.toBe(fixedCreatedAt);
|
||||
});
|
||||
|
||||
it("omits provenance when opts.provenance not provided (backward compat)", () => {
|
||||
const specPath = writeSpec();
|
||||
const pkgDir = writePkg("pkg", { "package.yaml": "name: pkg" });
|
||||
const outputDir = path.join(tmpDir, "staging");
|
||||
const assembler = new BundleAssembler({ fsOps: realFsOps() });
|
||||
|
||||
const manifest = assembler.assemble({
|
||||
specPath, outputDir, bundleName: "test", bundleVersion: "1.0",
|
||||
packages: [{ name: "pkg", version: "1.0", sourcePath: pkgDir, originalSource: "local:./pkg", manifestHash: "h1" }],
|
||||
});
|
||||
|
||||
expect(manifest.provenance).toBeUndefined();
|
||||
// on-disk yaml must not contain provenance section
|
||||
const diskYaml = fs.readFileSync(path.join(outputDir, "bundle.yaml"), "utf-8");
|
||||
expect(diskYaml).not.toContain("provenance:");
|
||||
});
|
||||
});
|
||||
@@ -505,6 +505,81 @@ describe("PodBundleAssembler", () => {
|
||||
|
||||
// Deferred: full golden-path integration (assemble -> validate -> preflight -> instantiate)
|
||||
// will be verified at Checkpoint 2 when AS-T11 + AS-T08b land
|
||||
|
||||
// Item 1 — provenance capture (slice-05 Checkpoint 2 part 2)
|
||||
it("v2: captures provenance from opts into manifest", () => {
|
||||
const spec = makeRigSpec();
|
||||
const files: Record<string, string> = {
|
||||
[`${RIG_ROOT}/rig.yaml`]: rigSpecYaml(spec),
|
||||
[`${RIG_ROOT}/agents/impl/agent.yaml`]: validAgentYaml("impl"),
|
||||
};
|
||||
const fs = mockFs(files);
|
||||
const assembler = new PodBundleAssembler({ fsOps: fs });
|
||||
|
||||
const result = assembler.assemble({
|
||||
rigRoot: RIG_ROOT,
|
||||
rigSpecPath: `${RIG_ROOT}/rig.yaml`,
|
||||
outputDir: "/tmp/bundle-staging-prov",
|
||||
bundleName: "test-bundle",
|
||||
bundleVersion: "1.0.0",
|
||||
provenance: {
|
||||
sourceHost: "test-host",
|
||||
authorSession: "velocity-driver@openrig-velocity",
|
||||
daemonVersion: "0.3.2",
|
||||
cliVersion: "0.3.2",
|
||||
notes: "v2 capture fixture",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.manifest.provenance).toBeDefined();
|
||||
expect(result.manifest.provenance?.sourceHost).toBe("test-host");
|
||||
expect(result.manifest.provenance?.authorSession).toBe("velocity-driver@openrig-velocity");
|
||||
expect(result.manifest.provenance?.daemonVersion).toBe("0.3.2");
|
||||
expect(result.manifest.provenance?.notes).toBe("v2 capture fixture");
|
||||
// createdAt mirrors root
|
||||
expect(result.manifest.provenance?.createdAt).toBe(result.manifest.createdAt);
|
||||
});
|
||||
|
||||
it("v2: respects opts.provenance.createdAt when pre-set (test determinism)", () => {
|
||||
const spec = makeRigSpec();
|
||||
const files: Record<string, string> = {
|
||||
[`${RIG_ROOT}/rig.yaml`]: rigSpecYaml(spec),
|
||||
[`${RIG_ROOT}/agents/impl/agent.yaml`]: validAgentYaml("impl"),
|
||||
};
|
||||
const fs = mockFs(files);
|
||||
const assembler = new PodBundleAssembler({ fsOps: fs });
|
||||
|
||||
const fixedCreatedAt = "2026-01-01T00:00:00Z";
|
||||
const result = assembler.assemble({
|
||||
rigRoot: RIG_ROOT,
|
||||
rigSpecPath: `${RIG_ROOT}/rig.yaml`,
|
||||
outputDir: "/tmp/bundle-staging-fixed-createdat",
|
||||
bundleName: "test", bundleVersion: "1.0",
|
||||
provenance: { createdAt: fixedCreatedAt, sourceHost: "h" },
|
||||
});
|
||||
|
||||
expect(result.manifest.provenance?.createdAt).toBe(fixedCreatedAt);
|
||||
expect(result.manifest.createdAt).not.toBe(fixedCreatedAt);
|
||||
});
|
||||
|
||||
it("v2: omits provenance when opts.provenance not provided (backward compat)", () => {
|
||||
const spec = makeRigSpec();
|
||||
const files: Record<string, string> = {
|
||||
[`${RIG_ROOT}/rig.yaml`]: rigSpecYaml(spec),
|
||||
[`${RIG_ROOT}/agents/impl/agent.yaml`]: validAgentYaml("impl"),
|
||||
};
|
||||
const fs = mockFs(files);
|
||||
const assembler = new PodBundleAssembler({ fsOps: fs });
|
||||
|
||||
const result = assembler.assemble({
|
||||
rigRoot: RIG_ROOT,
|
||||
rigSpecPath: `${RIG_ROOT}/rig.yaml`,
|
||||
outputDir: "/tmp/bundle-staging-no-prov",
|
||||
bundleName: "test", bundleVersion: "1.0",
|
||||
});
|
||||
|
||||
expect(result.manifest.provenance).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("PodBundleManifest validation", () => {
|
||||
|
||||
Reference in new issue
Block a user