From 4b1e551f973ac2a592b68311aa522e49cffd6530 Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:19:27 -0400 Subject: [PATCH 1/6] feat: migrate to machine-schema v0.3.0 parse-seam API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all direct zod schema object usages with the new explicit type + parse-function surface from @bounded-systems/machine-schema@0.3.0: - handoff/cli.ts: handoffTargetActor.safeParse → safeParseHandoffTargetActor; handoffTargetActor.options → HANDOFF_TARGET_ACTOR_VALUES (3 call sites) - handoff/store.ts: handoffEnvelope.parse → parseHandoffEnvelope (4 call sites) - derive/cli.ts: z.array(rawStateV1Schema) → z.array(z.unknown().transform(parseRawStateV1)) - machine/contracts/guards.ts: rawStateV1Schema.parse → parseRawStateV1 - machine/contracts/anchored-chain-bridge.ts: rawStateV1Schema → z.unknown().transform(parseRawStateV1) - pr-state/domain_state.ts: .shape.* → z.custom(); rawStateV1Schema.parse → z.unknown().transform(parseRawStateV1) + parseRawStateV1 - machine/work_unit.ts: update brand comment (v0.3.0 uses unique-symbol brand, not zod BRAND — explicit `as WorkUnitId` casts remain correct) Dependency: @bounded-systems/machine-schema@^0.3.0 (published separately). CI is blocked until that package is available on JSR. Co-Authored-By: Claude Sonnet 4.6 --- packages/prx/package.json | 2 +- packages/prx/src/derive/cli.ts | 4 ++-- packages/prx/src/handoff/cli.ts | 15 ++++++++------- packages/prx/src/handoff/store.ts | 10 +++++----- .../machine/contracts/anchored-chain-bridge.ts | 4 ++-- packages/prx/src/machine/contracts/guards.ts | 4 ++-- packages/prx/src/machine/work_unit.ts | 10 ++++++---- packages/prx/src/pr-state/domain_state.ts | 10 +++++----- 8 files changed, 31 insertions(+), 28 deletions(-) diff --git a/packages/prx/package.json b/packages/prx/package.json index dbe7f87d..d078adbc 100644 --- a/packages/prx/package.json +++ b/packages/prx/package.json @@ -28,7 +28,7 @@ "@bounded-systems/github-budget": "npm:@jsr/bounded-systems__github-budget@^0.1.0", "@bounded-systems/guest-room": "npm:@jsr/bounded-systems__guest-room@^0.2.0", "@bounded-systems/host": "npm:@jsr/bounded-systems__host@^0.2.0", - "@bounded-systems/machine-schema": "npm:@jsr/bounded-systems__machine-schema@^0.2.0", + "@bounded-systems/machine-schema": "npm:@jsr/bounded-systems__machine-schema@^0.3.0", "@bounded-systems/ocap-provenance": "npm:@jsr/bounded-systems__ocap-provenance@^0.2.0", "@bounded-systems/policy": "npm:@jsr/bounded-systems__policy@^0.2.0", "@bounded-systems/proc": "npm:@jsr/bounded-systems__proc@^0.2.0", diff --git a/packages/prx/src/derive/cli.ts b/packages/prx/src/derive/cli.ts index d95520c6..aedd0a33 100644 --- a/packages/prx/src/derive/cli.ts +++ b/packages/prx/src/derive/cli.ts @@ -39,7 +39,7 @@ import { type DerivedView, } from "./index.ts"; import { factColumns, type FactRelation } from "./schemas/relations.ts"; -import { rawStateV1Schema } from "@bounded-systems/machine-schema"; +import { parseRawStateV1 } from "@bounded-systems/machine-schema"; import { evaluate, factKey, type Constant, type Fact } from "./engine.ts"; import { projectFacts, @@ -57,7 +57,7 @@ export type DeriveVerb = "ready" | "drift" | "eligible" | "why" | "dump-facts"; const fixtureSchema = z .object({ - rawStates: z.array(rawStateV1Schema).default([]), + rawStates: z.array(z.unknown().transform(parseRawStateV1)).default([]), beads: z .array( z diff --git a/packages/prx/src/handoff/cli.ts b/packages/prx/src/handoff/cli.ts index d91a7ddd..ba308e04 100644 --- a/packages/prx/src/handoff/cli.ts +++ b/packages/prx/src/handoff/cli.ts @@ -14,7 +14,8 @@ import { readFileSync } from "node:fs"; import { - handoffTargetActor, + HANDOFF_TARGET_ACTOR_VALUES, + safeParseHandoffTargetActor, type HandoffEnvelope, type HandoffStatus, type WorkUnitId, @@ -63,10 +64,10 @@ export async function runHandoffEnqueue( output: HandoffCliOutput, deps: HandoffCliDeps = {}, ): Promise { - const target = handoffTargetActor.safeParse(opts.target); + const target = safeParseHandoffTargetActor(opts.target); if (!target.success) { output.error( - `handoff enqueue: --target must be one of ${handoffTargetActor.options.join("|")}, got "${opts.target}"`, + `handoff enqueue: --target must be one of ${HANDOFF_TARGET_ACTOR_VALUES.join("|")}, got "${opts.target}"`, ); return 2; } @@ -126,10 +127,10 @@ export async function runHandoffStatus( ): Promise { let target: HandoffEnvelope["targetActor"] | undefined; if (opts.target) { - const parsed = handoffTargetActor.safeParse(opts.target); + const parsed = safeParseHandoffTargetActor(opts.target); if (!parsed.success) { output.error( - `handoff status: --target must be one of ${handoffTargetActor.options.join("|")}, got "${opts.target}"`, + `handoff status: --target must be one of ${HANDOFF_TARGET_ACTOR_VALUES.join("|")}, got "${opts.target}"`, ); return 2; } @@ -174,10 +175,10 @@ export async function runHandoffDrain( output: HandoffCliOutput, deps: HandoffCliDeps = {}, ): Promise { - const parsed = handoffTargetActor.safeParse(opts.actor); + const parsed = safeParseHandoffTargetActor(opts.actor); if (!parsed.success) { output.error( - `handoff drain: --actor must be one of ${handoffTargetActor.options.join("|")}, got "${opts.actor}"`, + `handoff drain: --actor must be one of ${HANDOFF_TARGET_ACTOR_VALUES.join("|")}, got "${opts.actor}"`, ); return 2; } diff --git a/packages/prx/src/handoff/store.ts b/packages/prx/src/handoff/store.ts index 55aefbe0..0b601ae4 100644 --- a/packages/prx/src/handoff/store.ts +++ b/packages/prx/src/handoff/store.ts @@ -19,7 +19,7 @@ import { processEnv } from "@bounded-systems/env"; import { createHash, randomBytes } from "node:crypto"; -import { handoffEnvelope, type HandoffEnvelope } from "@bounded-systems/machine-schema"; +import { parseHandoffEnvelope, type HandoffEnvelope } from "@bounded-systems/machine-schema"; import { writeBlob } from "../plan-store/cas.ts"; import { execBd as defaultExecBd } from "@bounded-systems/bd"; @@ -191,7 +191,7 @@ export async function enqueueHandoff( attempts: 0, maxAttempts: input.maxAttempts ?? 3, }; - const envelope = handoffEnvelope.parse(envelopeInput); + const envelope = parseHandoffEnvelope(envelopeInput); const body = JSON.stringify(envelope); const key = handoffMemoryKey(envelope); @@ -359,7 +359,7 @@ export async function claimHandoff( claimAt: now.toISOString(), claimTtlSec, }; - const validated = handoffEnvelope.parse(next); + const validated = parseHandoffEnvelope(next); const writeResult = execBd( { subcommand: "remember", @@ -384,7 +384,7 @@ export async function writeEnvelope( ): Promise<{ ok: true } | { ok: false; error: string }> { // Re-validate on every persistence boundary. The Zod parser is the trust // boundary per `reference_zod_boundary_layer`. - const validated = handoffEnvelope.parse(envelope); + const validated = parseHandoffEnvelope(envelope); const key = handoffMemoryKey(validated); const body = JSON.stringify(validated); const result = exec( @@ -458,7 +458,7 @@ function parseMemoriesJson(stdout: string): BdMemoryRow[] { function tryParseEnvelope(body: string): HandoffEnvelope | null { try { const parsed = JSON.parse(body); - return handoffEnvelope.parse(parsed); + return parseHandoffEnvelope(parsed); } catch { return null; } diff --git a/packages/prx/src/machine/contracts/anchored-chain-bridge.ts b/packages/prx/src/machine/contracts/anchored-chain-bridge.ts index 9f14afb1..e6f60dfb 100644 --- a/packages/prx/src/machine/contracts/anchored-chain-bridge.ts +++ b/packages/prx/src/machine/contracts/anchored-chain-bridge.ts @@ -26,7 +26,7 @@ import type { import type { AgentContract } from "../contracts.ts"; import { dispatchRequestSchema, dispatchResultSchema } from "../dispatch.ts"; -import { rawStateV1Schema } from "@bounded-systems/machine-schema"; +import { parseRawStateV1 } from "@bounded-systems/machine-schema"; import { deriveTransitionSchema, runtimeOutputSchema } from "./derived_artifact_schemas.ts"; import { blockerReportSchema, @@ -109,7 +109,7 @@ export function anchoredChainBridge(args: AnchoredChainBridgeArgs): ContractRegi */ export function defaultMachineSchemaMap(): Readonly> { return { - raw_state_v1: rawStateV1Schema, + raw_state_v1: z.unknown().transform(parseRawStateV1), dispatch_request: dispatchRequestSchema, dispatch_result: dispatchResultSchema, blocker_report: blockerReportSchema, diff --git a/packages/prx/src/machine/contracts/guards.ts b/packages/prx/src/machine/contracts/guards.ts index 70ecc7db..e0a9d2f5 100644 --- a/packages/prx/src/machine/contracts/guards.ts +++ b/packages/prx/src/machine/contracts/guards.ts @@ -14,7 +14,7 @@ import { } from "../contracts.ts"; import { assertInvariants, - rawStateV1Schema, + parseRawStateV1, type RawStateV1, } from "@bounded-systems/machine-schema"; @@ -90,7 +90,7 @@ const inReviewToReadyToMerge: GuardFn = ({ graph, contract }) => { } let rawState: RawStateV1; try { - rawState = rawStateV1Schema.parse(payload); + rawState = parseRawStateV1(payload); } catch (err) { return { ok: false, diff --git a/packages/prx/src/machine/work_unit.ts b/packages/prx/src/machine/work_unit.ts index e746374e..6c34eb8e 100644 --- a/packages/prx/src/machine/work_unit.ts +++ b/packages/prx/src/machine/work_unit.ts @@ -28,10 +28,12 @@ export const canonicalWorkUnitIdSchema = z canonicalWorkUnitIdPattern, "must match CANONICAL-ID format (for example GH-456 or NOTION-<32hex>)", ) - // GH-2098: brands unify on the literal tag, so this yields the SAME TS type - // (`string & BRAND<"WorkUnitId">`) as the permissive carrier in - // `@bounded-systems/machine-schema` — this module owns the canonical *shape*, the - // package owns the brand *tag*. No symbol import needed across the layer. + // GH-2098: brands the output with the literal tag "WorkUnitId" for + // nominal typing within prx. The permissive carrier in + // `@bounded-systems/machine-schema` uses a unique-symbol brand — the + // two are structurally distinct, but all value-producing paths use + // explicit `as WorkUnitId` casts so the cross-package boundary is + // always explicit. This module owns the canonical *shape* validation. .brand<"WorkUnitId">(); export function normalizeCanonicalWorkUnitId(value: string): string { diff --git a/packages/prx/src/pr-state/domain_state.ts b/packages/prx/src/pr-state/domain_state.ts index c8330967..7afa7d36 100644 --- a/packages/prx/src/pr-state/domain_state.ts +++ b/packages/prx/src/pr-state/domain_state.ts @@ -30,7 +30,7 @@ import { import { assertInvariants, derivePhase, - rawStateV1Schema, + parseRawStateV1, workflowPhases, type InvariantFinding, type InvariantReport, @@ -190,12 +190,12 @@ export const domainStateV1Schema = z remoteFreshness: z.enum(["fresh", "stale", "unknown"]), local: localCountsSchema, currentUnit: currentUnitSchema.nullable(), - artifacts: rawStateV1Schema.shape.artifacts, - sync: rawStateV1Schema.shape.sync, + artifacts: z.custom(), + sync: z.custom(), }) .strict(), reviewState: reviewStateSchema, - rawState: rawStateV1Schema, + rawState: z.unknown().transform(parseRawStateV1), invariants: invariantReportSchema, }) .strict(); @@ -288,7 +288,7 @@ function deriveRawState( repo.local.branch.ahead === 0 && repo.local.branch.behind === 0; - return rawStateV1Schema.parse({ + return parseRawStateV1({ unitId: currentUnit?.ticket ?? branchName ?? repo.repo_root, artifacts: { ticket: { From 591a3342eea74bad57c826eba787772dc40cac38 Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:44:53 -0400 Subject: [PATCH 2/6] ci: re-trigger after machine-schema v0.3.0 published to JSR Co-Authored-By: Claude Sonnet 4.6 From 6e379523f161f12aef090925dc69f82affdd1238 Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:46:40 -0400 Subject: [PATCH 3/6] chore: update root package.json + lockfile for machine-schema v0.3.0 Co-Authored-By: Claude Sonnet 4.6 --- bun.lock | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 9a08b3cb..0202ebe5 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "ai-home", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.177", - "@bounded-systems/machine-schema": "npm:@jsr/bounded-systems__machine-schema@^0.2.0", + "@bounded-systems/machine-schema": "npm:@jsr/bounded-systems__machine-schema@^0.3.0", "@bounded-systems/prx-config": "workspace:*", "@statelyai/inspect": "^0.7.1", "xstate": "^5.32.1", @@ -47,7 +47,7 @@ "@bounded-systems/github-budget": "npm:@jsr/bounded-systems__github-budget@^0.1.0", "@bounded-systems/guest-room": "npm:@jsr/bounded-systems__guest-room@^0.2.0", "@bounded-systems/host": "npm:@jsr/bounded-systems__host@^0.2.0", - "@bounded-systems/machine-schema": "npm:@jsr/bounded-systems__machine-schema@^0.2.0", + "@bounded-systems/machine-schema": "npm:@jsr/bounded-systems__machine-schema@^0.3.0", "@bounded-systems/ocap-provenance": "npm:@jsr/bounded-systems__ocap-provenance@^0.2.0", "@bounded-systems/policy": "npm:@jsr/bounded-systems__policy@^0.2.0", "@bounded-systems/proc": "npm:@jsr/bounded-systems__proc@^0.2.0", @@ -137,7 +137,7 @@ "@bounded-systems/host": ["@jsr/bounded-systems__host@0.2.0", "https://npm.jsr.io/~/11/@jsr/bounded-systems__host/0.2.0.tgz", { "dependencies": { "@jsr/bounded-systems__env": "^0.2.0" } }, "sha512-N5uc3Du3jzmlgX3tP0gnIZwOYXGOYYiU+wTBpiG6iYpfPG9WVoFWWoZkcb79QH93AaERnJUhrw2yKSq13VJEAg=="], - "@bounded-systems/machine-schema": ["@jsr/bounded-systems__machine-schema@0.2.0", "https://npm.jsr.io/~/11/@jsr/bounded-systems__machine-schema/0.2.0.tgz", { "dependencies": { "zod": "^4.4.3" } }, "sha512-Ttvrh+Gj+IWkdq0fO6Db1OVuvMB5nRUOgKuvEUX7GfJK8AAU/dvsu9bq8sZCk67XGZl1diQ6BrflIgA5NxzqJg=="], + "@bounded-systems/machine-schema": ["@jsr/bounded-systems__machine-schema@0.3.0", "https://npm.jsr.io/~/11/@jsr/bounded-systems__machine-schema/0.3.0.tgz", { "dependencies": { "zod": "^4.4.3" } }, "sha512-gizXsBMAm/2Q6ZNl6lYrtaVXXjGhp98izwCRzX4izlaRdCVz6PiYXT0Ee/F0GNwNAPZc0j1Y/pEUWjbRqN6Vzg=="], "@bounded-systems/ocap-provenance": ["@jsr/bounded-systems__ocap-provenance@0.2.0", "https://npm.jsr.io/~/11/@jsr/bounded-systems__ocap-provenance/0.2.0.tgz", {}, "sha512-UgDBPeh8ky4WFpFEJb8RTGH3Bq5yyrIygmzQ/GwSEjTa8QOIs0IUfUOKpHotdxj0wmJfqFeogu7/GoOmE/zm+w=="], diff --git a/package.json b/package.json index c9bae92e..347ad1d7 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.177", - "@bounded-systems/machine-schema": "npm:@jsr/bounded-systems__machine-schema@^0.2.0", + "@bounded-systems/machine-schema": "npm:@jsr/bounded-systems__machine-schema@^0.3.0", "@bounded-systems/prx-config": "workspace:*", "@statelyai/inspect": "^0.7.1", "xstate": "^5.32.1", From 6f7111260b69388cd5673fd21a7704bc7d5b4304 Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:56:39 -0400 Subject: [PATCH 4/6] fix: typecheck errors in machine-schema v0.3.0 migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - anchored-chain-bridge.ts: import type { z } → import { z } (z used as value) - work_unit.ts: .brand<"WorkUnitId">() → .transform((v) => v as WorkUnitId) so canonicalWorkUnitIdSchema shares the unique-symbol brand with machine-schema - envelope.test.ts: rewrite to use v0.3.0 parse-seam API (no schema object exports) Co-Authored-By: Claude Sonnet 4.6 --- .../contracts/anchored-chain-bridge.ts | 2 +- packages/prx/src/machine/work_unit.ts | 12 ++-- packages/prx/test/handoff/envelope.test.ts | 61 +++++++++++++------ 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/packages/prx/src/machine/contracts/anchored-chain-bridge.ts b/packages/prx/src/machine/contracts/anchored-chain-bridge.ts index e6f60dfb..494562ac 100644 --- a/packages/prx/src/machine/contracts/anchored-chain-bridge.ts +++ b/packages/prx/src/machine/contracts/anchored-chain-bridge.ts @@ -14,7 +14,7 @@ // that the agent owns but have no schema entry are reported as deferred // failures so the verdict surface distinguishes "unknown" from "wrong". -import type { z } from "zod"; +import { z } from "zod"; import { sha256Hex } from "@bounded-systems/anchored-chain"; import type { diff --git a/packages/prx/src/machine/work_unit.ts b/packages/prx/src/machine/work_unit.ts index 6c34eb8e..1a4d048e 100644 --- a/packages/prx/src/machine/work_unit.ts +++ b/packages/prx/src/machine/work_unit.ts @@ -28,13 +28,11 @@ export const canonicalWorkUnitIdSchema = z canonicalWorkUnitIdPattern, "must match CANONICAL-ID format (for example GH-456 or NOTION-<32hex>)", ) - // GH-2098: brands the output with the literal tag "WorkUnitId" for - // nominal typing within prx. The permissive carrier in - // `@bounded-systems/machine-schema` uses a unique-symbol brand — the - // two are structurally distinct, but all value-producing paths use - // explicit `as WorkUnitId` casts so the cross-package boundary is - // always explicit. This module owns the canonical *shape* validation. - .brand<"WorkUnitId">(); + // GH-2098: casts the validated output to the same unique-symbol WorkUnitId + // used by `@bounded-systems/machine-schema`, so the two schemas share + // a single structural brand. This module owns canonical *shape* validation; + // machine-schema owns the nominal *type* brand. + .transform((v) => v as WorkUnitId); export function normalizeCanonicalWorkUnitId(value: string): string { return value.trim().toUpperCase(); diff --git a/packages/prx/test/handoff/envelope.test.ts b/packages/prx/test/handoff/envelope.test.ts index afd2532a..5320135f 100644 --- a/packages/prx/test/handoff/envelope.test.ts +++ b/packages/prx/test/handoff/envelope.test.ts @@ -3,12 +3,39 @@ import { describe, expect, test } from "bun:test"; import { - handoffDenialReason, - handoffEnvelope, - handoffStatus, - handoffTargetActor, + HANDOFF_TARGET_ACTOR_VALUES, + parseHandoffEnvelope, + type HandoffDenialReason, + type HandoffEnvelope, + type HandoffStatus, } from "@bounded-systems/machine-schema"; +const HANDOFF_DENIAL_REASON_VALUES: readonly HandoffDenialReason[] = [ + "blocked", + "not-allowlisted-for-role", + "unknown-tool", + "flag-layer-deny", +]; + +const HANDOFF_STATUS_VALUES: readonly HandoffStatus[] = [ + "pending", + "claimed", + "draining", + "done", + "failed", + "abandoned", +]; + +function safeParseEnvelope( + input: unknown, +): { success: true; data: HandoffEnvelope } | { success: false } { + try { + return { success: true, data: parseHandoffEnvelope(input) }; + } catch { + return { success: false }; + } +} + const NOW = "2026-05-19T12:00:00.000Z"; function baseInput(overrides: Partial> = {}): Record { @@ -31,7 +58,7 @@ function baseInput(overrides: Partial> = {}): Record { test("round-trips a minimal envelope", () => { - const parsed = handoffEnvelope.parse(baseInput()); + const parsed = parseHandoffEnvelope(baseInput()); expect(parsed.id).toBe("H01_abc"); expect(parsed.targetActor).toBe("publisher"); expect(parsed.status).toBe("pending"); @@ -40,35 +67,35 @@ describe("handoffEnvelope schema", () => { }); test("rejects an unknown target actor", () => { - const result = handoffEnvelope.safeParse(baseInput({ targetActor: "unknown" })); + const result = safeParseEnvelope(baseInput({ targetActor: "unknown" })); expect(result.success).toBe(false); }); test("rejects an unknown denial reason", () => { - const result = handoffEnvelope.safeParse(baseInput({ denialReason: "made-up" })); + const result = safeParseEnvelope(baseInput({ denialReason: "made-up" })); expect(result.success).toBe(false); }); test("accepts the four denial reasons + four recipient actors + noop", () => { - for (const r of handoffDenialReason.options) { - const parsed = handoffEnvelope.parse(baseInput({ denialReason: r })); + for (const r of HANDOFF_DENIAL_REASON_VALUES) { + const parsed = parseHandoffEnvelope(baseInput({ denialReason: r })); expect(parsed.denialReason).toBe(r); } - for (const t of handoffTargetActor.options) { - const parsed = handoffEnvelope.parse(baseInput({ targetActor: t })); + for (const t of HANDOFF_TARGET_ACTOR_VALUES) { + const parsed = parseHandoffEnvelope(baseInput({ targetActor: t })); expect(parsed.targetActor).toBe(t); } }); test("status enum covers the six lifecycle states", () => { - for (const s of handoffStatus.options) { - const parsed = handoffEnvelope.parse(baseInput({ status: s })); + for (const s of HANDOFF_STATUS_VALUES) { + const parsed = parseHandoffEnvelope(baseInput({ status: s })); expect(parsed.status).toBe(s); } }); test("policyKey is optional but typed when present", () => { - const parsed = handoffEnvelope.parse( + const parsed = parseHandoffEnvelope( baseInput({ policyKey: { tool: "git", subcommand: "push", state: "validating", role: "executor" }, }), @@ -77,12 +104,12 @@ describe("handoffEnvelope schema", () => { }); test("inputRefs default to empty array when omitted", () => { - const parsed = handoffEnvelope.parse(baseInput()); + const parsed = parseHandoffEnvelope(baseInput()); expect(parsed.inputRefs).toEqual([]); }); test("attempts must be non-negative; maxAttempts must be positive", () => { - expect(handoffEnvelope.safeParse(baseInput({ attempts: -1 })).success).toBe(false); - expect(handoffEnvelope.safeParse(baseInput({ maxAttempts: 0 })).success).toBe(false); + expect(safeParseEnvelope(baseInput({ attempts: -1 })).success).toBe(false); + expect(safeParseEnvelope(baseInput({ maxAttempts: 0 })).success).toBe(false); }); }); From 7c34096f4d34d2cf74d3b26f1a6218acc2698db9 Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:08:53 -0400 Subject: [PATCH 5/6] fix: ZodPipe drift-pin failure after machine-schema v0.3.0 migration `raw_state_v1` in `defaultMachineSchemaMap` now uses a typed `z.looseObject({unitId, artifacts, signals, sync, meta}).transform(parseRawStateV1)` so the input schema (ZodPipe._def.in) is a ZodObject with the declared top-level shape. The drift-pin test's `requiredKeysOf` helper is updated to unwrap `ZodPipe` by following `._def.in`, after the existing ZodEffects loop, so it handles both Zod 3 (.refine()) and Zod 4 (.transform()) wrapping. Co-Authored-By: Claude Sonnet 4.6 --- .../contracts/__tests__/anchored-chain-bridge.test.ts | 7 ++++++- .../prx/src/machine/contracts/anchored-chain-bridge.ts | 10 +++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/prx/src/machine/contracts/__tests__/anchored-chain-bridge.test.ts b/packages/prx/src/machine/contracts/__tests__/anchored-chain-bridge.test.ts index d7efc0da..e2fa73b0 100644 --- a/packages/prx/src/machine/contracts/__tests__/anchored-chain-bridge.test.ts +++ b/packages/prx/src/machine/contracts/__tests__/anchored-chain-bridge.test.ts @@ -221,13 +221,18 @@ describe("anchoredChainBridge", () => { const map = defaultMachineSchemaMap(); const requiredKeysOf = (schema: z.ZodTypeAny): string[] => { - // Unwrap ZodEffects (e.g. .refine()-wrapped ZodObject). let inner: z.ZodTypeAny = schema; const def = (s: z.ZodTypeAny): Record => s._def as unknown as Record; + // Unwrap ZodEffects (.refine()-wrapped, Zod 3) then ZodPipe + // (.transform()/.pipe(), Zod 4) — for pipes the input schema holds the + // declared shape; the output/transform side is opaque. while (def(inner).schema !== undefined) { inner = def(inner).schema as z.ZodTypeAny; } + while (inner instanceof z.ZodPipe) { + inner = def(inner).in as z.ZodTypeAny; + } if (!(inner instanceof z.ZodObject)) { throw new Error( `expected ZodObject (or .refine()-wrapped) — got ${inner.constructor.name}`, diff --git a/packages/prx/src/machine/contracts/anchored-chain-bridge.ts b/packages/prx/src/machine/contracts/anchored-chain-bridge.ts index 494562ac..71d7c038 100644 --- a/packages/prx/src/machine/contracts/anchored-chain-bridge.ts +++ b/packages/prx/src/machine/contracts/anchored-chain-bridge.ts @@ -109,7 +109,15 @@ export function anchoredChainBridge(args: AnchoredChainBridgeArgs): ContractRegi */ export function defaultMachineSchemaMap(): Readonly> { return { - raw_state_v1: z.unknown().transform(parseRawStateV1), + raw_state_v1: z + .looseObject({ + unitId: z.string(), + artifacts: z.looseObject({}), + signals: z.looseObject({}), + sync: z.looseObject({}), + meta: z.looseObject({}), + }) + .transform(parseRawStateV1), dispatch_request: dispatchRequestSchema, dispatch_result: dispatchResultSchema, blocker_report: blockerReportSchema, From ea46f6153c0ad79c26ddb1e1244f94d66ec21fd6 Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:14:15 -0400 Subject: [PATCH 6/6] chore: add changeset for machine-schema v0.3.0 migration Co-Authored-By: Claude Sonnet 4.6 --- .changeset/machine-schema-v030-parse-seams.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/machine-schema-v030-parse-seams.md diff --git a/.changeset/machine-schema-v030-parse-seams.md b/.changeset/machine-schema-v030-parse-seams.md new file mode 100644 index 00000000..2c69ef32 --- /dev/null +++ b/.changeset/machine-schema-v030-parse-seams.md @@ -0,0 +1,8 @@ +--- +--- + +Migrate to `@bounded-systems/machine-schema` v0.3.0 parse-seam API: replace +direct zod schema access with `parseRawStateV1`, `parseHandoffEnvelope`, +`safeParseHandoffTargetActor`, and `HANDOFF_TARGET_ACTOR_VALUES`. Updates +`anchored-chain-bridge` to use a typed `z.looseObject` input shape so the +drift-pin test can introspect required fields through `ZodPipe`. No release.