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
8 changes: 8 additions & 0 deletions .changeset/machine-schema-v030-parse-seams.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/prx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/prx/src/derive/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
15 changes: 8 additions & 7 deletions packages/prx/src/handoff/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
import { readFileSync } from "node:fs";

import {
handoffTargetActor,
HANDOFF_TARGET_ACTOR_VALUES,
safeParseHandoffTargetActor,
type HandoffEnvelope,
type HandoffStatus,
type WorkUnitId,
Expand Down Expand Up @@ -63,10 +64,10 @@ export async function runHandoffEnqueue(
output: HandoffCliOutput,
deps: HandoffCliDeps = {},
): Promise<number> {
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;
}
Expand Down Expand Up @@ -126,10 +127,10 @@ export async function runHandoffStatus(
): Promise<number> {
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;
}
Expand Down Expand Up @@ -174,10 +175,10 @@ export async function runHandoffDrain(
output: HandoffCliOutput,
deps: HandoffCliDeps = {},
): Promise<number> {
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;
}
Expand Down
10 changes: 5 additions & 5 deletions packages/prx/src/handoff/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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",
Expand All @@ -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(
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> =>
s._def as unknown as Record<string, unknown>;
// 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}`,
Expand Down
14 changes: 11 additions & 3 deletions packages/prx/src/machine/contracts/anchored-chain-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -109,7 +109,15 @@ export function anchoredChainBridge(args: AnchoredChainBridgeArgs): ContractRegi
*/
export function defaultMachineSchemaMap(): Readonly<Record<string, z.ZodTypeAny>> {
return {
raw_state_v1: rawStateV1Schema,
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,
Expand Down
4 changes: 2 additions & 2 deletions packages/prx/src/machine/contracts/guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from "../contracts.ts";
import {
assertInvariants,
rawStateV1Schema,
parseRawStateV1,
type RawStateV1,
} from "@bounded-systems/machine-schema";

Expand Down Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions packages/prx/src/machine/work_unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ 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.
.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();
Expand Down
10 changes: 5 additions & 5 deletions packages/prx/src/pr-state/domain_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
import {
assertInvariants,
derivePhase,
rawStateV1Schema,
parseRawStateV1,
workflowPhases,
type InvariantFinding,
type InvariantReport,
Expand Down Expand Up @@ -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<RawStateV1["artifacts"]>(),
sync: z.custom<RawStateV1["sync"]>(),
})
.strict(),
reviewState: reviewStateSchema,
rawState: rawStateV1Schema,
rawState: z.unknown().transform(parseRawStateV1),
invariants: invariantReportSchema,
})
.strict();
Expand Down Expand Up @@ -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: {
Expand Down
Loading