feat: add collectCasRefs — extract CAS refs from schema meta annotations

Replaces manual extractRefs functions with declarative schema-level
casRef annotations. Walks Zod v4 schemas recursively, collecting
string values from fields marked with .meta({ casRef: true }).

Supports: objects, arrays, discriminatedUnion, nullable/optional.

8/8 test cases pass (flat, nested, union, null, mixed).

Refs #285, addresses #286
This commit is contained in:
2026-05-16 10:34:53 +00:00
parent 9584a86fb7
commit 93b7947d7c
3 changed files with 237 additions and 0 deletions
@@ -0,0 +1,114 @@
import { describe, expect, test } from "bun:test";
import * as z from "zod/v4";
import { collectCasRefs } from "../src/collect-cas-refs.js";
const phaseSchema = z.object({
hash: z.string().meta({ casRef: true }),
title: z.string(),
});
const plannerMetaSchema = z.discriminatedUnion("status", [
z.object({
status: z.literal("planned"),
phases: z.array(phaseSchema),
}),
z.object({
status: z.literal("aborted"),
reason: z.string(),
}),
]);
describe("collectCasRefs", () => {
test("1. flat field with casRef annotation", () => {
const schema = z.object({
completedPhase: z.string().meta({ casRef: true }),
});
expect(collectCasRefs(schema, { completedPhase: "BHAAAAAAAAAAA" })).toEqual(["BHAAAAAAAAAAA"]);
});
test("2. plain string without annotation is ignored", () => {
const schema = z.object({
summary: z.string(),
completedPhase: z.string().meta({ casRef: true }),
});
expect(
collectCasRefs(schema, {
summary: "done",
completedPhase: "BHBBBBBBBBBBB",
}),
).toEqual(["BHBBBBBBBBBBB"]);
});
test("3. nested array of objects collects each annotated hash", () => {
const schema = z.object({
phases: z.array(phaseSchema),
});
expect(
collectCasRefs(schema, {
phases: [
{ hash: "BH11111111111", title: "setup" },
{ hash: "BH22222222222", title: "impl" },
],
}),
).toEqual(["BH11111111111", "BH22222222222"]);
});
test("4. discriminatedUnion — planner planned branch", () => {
expect(
collectCasRefs(plannerMetaSchema, {
status: "planned",
phases: [
{ hash: "BH33333333333", title: "a" },
{ hash: "BH44444444444", title: "b" },
],
}),
).toEqual(["BH33333333333", "BH44444444444"]);
});
test("4b. discriminatedUnion — planner aborted branch", () => {
expect(
collectCasRefs(plannerMetaSchema, {
status: "aborted",
reason: "missing workspace",
}),
).toEqual([]);
});
test("5. null and undefined annotated fields are skipped", () => {
const schema = z.object({
ref: z.string().meta({ casRef: true }).nullable(),
optionalRef: z.string().meta({ casRef: true }).optional(),
});
expect(collectCasRefs(schema, { ref: null, optionalRef: undefined })).toEqual([]);
expect(collectCasRefs(schema, { ref: "BH55555555555", optionalRef: undefined })).toEqual([
"BH55555555555",
]);
});
test("6. mixed annotated and plain fields at multiple levels", () => {
const schema = z.object({
label: z.string(),
phase: z.object({
hash: z.string().meta({ casRef: true }),
title: z.string(),
}),
tags: z.array(z.string()),
});
expect(
collectCasRefs(schema, {
label: "coder",
phase: { hash: "BH66666666666", title: "fix" },
tags: ["a", "b"],
}),
).toEqual(["BH66666666666"]);
});
test("7. empty phases array yields no refs", () => {
expect(
collectCasRefs(plannerMetaSchema, {
status: "planned",
phases: [],
}),
).toEqual([]);
});
});
@@ -0,0 +1,122 @@
import * as z from "zod/v4";
type ZodSchema = z.ZodType;
type DefPipeIn = { in: ZodSchema };
function hasCasRef(schema: ZodSchema): boolean {
const meta = z.globalRegistry.get(schema);
return meta !== undefined && meta.casRef === true;
}
function walkOptional(schema: z.ZodOptional<ZodSchema>, data: unknown): string[] {
if (data === undefined) {
return [];
}
return walkCasRefs(schema.unwrap(), data);
}
function walkNullable(schema: z.ZodNullable<ZodSchema>, data: unknown): string[] {
if (data === null) {
return [];
}
return walkCasRefs(schema.unwrap(), data);
}
function walkDefault(schema: z.ZodDefault<ZodSchema>, data: unknown): string[] {
return walkCasRefs(schema.unwrap(), data);
}
function walkPrefault(schema: z.ZodPrefault<ZodSchema>, data: unknown): string[] {
return walkCasRefs(schema.unwrap(), data);
}
function walkCatch(schema: z.ZodCatch<ZodSchema>, data: unknown): string[] {
return walkCasRefs(schema.unwrap(), data);
}
function walkReadonly(schema: z.ZodReadonly<ZodSchema>, data: unknown): string[] {
return walkCasRefs(schema.unwrap(), data);
}
function walkPipe(def: DefPipeIn, data: unknown): string[] {
return walkCasRefs(def.in, data);
}
function walkString(schema: ZodSchema, data: unknown): string[] {
if (hasCasRef(schema) && typeof data === "string") {
return [data];
}
return [];
}
function walkObject(schema: z.ZodObject<z.ZodRawShape>, data: unknown): string[] {
if (typeof data !== "object" || data === null || Array.isArray(data)) {
return [];
}
const record = data as Record<string, unknown>;
const shape = schema.shape;
const refs: string[] = [];
for (const [key, fieldSchema] of Object.entries(shape)) {
refs.push(...walkCasRefs(fieldSchema as ZodSchema, record[key]));
}
return refs;
}
function walkArray(schema: z.ZodArray<ZodSchema>, data: unknown): string[] {
if (!Array.isArray(data)) {
return [];
}
const element = schema.element;
const refs: string[] = [];
for (const item of data) {
refs.push(...walkCasRefs(element, item));
}
return refs;
}
function walkUnion(schema: z.ZodUnion<readonly ZodSchema[]>, data: unknown): string[] {
for (const option of schema.options) {
const parsed = option.safeParse(data);
if (parsed.success) {
return walkCasRefs(option, data);
}
}
return [];
}
function walkCasRefs(schema: ZodSchema, data: unknown): string[] {
const def = schema.def;
switch (def.type) {
case "optional":
return walkOptional(schema as z.ZodOptional<ZodSchema>, data);
case "nullable":
return walkNullable(schema as z.ZodNullable<ZodSchema>, data);
case "default":
return walkDefault(schema as z.ZodDefault<ZodSchema>, data);
case "prefault":
return walkPrefault(schema as z.ZodPrefault<ZodSchema>, data);
case "catch":
return walkCatch(schema as z.ZodCatch<ZodSchema>, data);
case "readonly":
return walkReadonly(schema as z.ZodReadonly<ZodSchema>, data);
case "pipe":
return walkPipe(def as unknown as DefPipeIn, data);
case "string":
return walkString(schema, data);
case "object":
return walkObject(schema as z.ZodObject<z.ZodRawShape>, data);
case "array":
return walkArray(schema as z.ZodArray<ZodSchema>, data);
case "union":
return walkUnion(schema as z.ZodUnion<readonly ZodSchema[]>, data);
default:
return [];
}
}
/** Collect CAS content hashes from meta using `casRef` annotations on the Zod schema. */
export function collectCasRefs(schema: ZodSchema, data: unknown): string[] {
return walkCasRefs(schema, data);
}
+1
View File
@@ -26,5 +26,6 @@ export type {
} from "@uncaged/workflow-protocol";
export { END, START } from "@uncaged/workflow-protocol";
export { buildThreadContext } from "./build-context.js";
export { collectCasRefs } from "./collect-cas-refs.js";
export { createWorkflow } from "./create-workflow.js";
export { err, ok } from "./result.js";