refactor: remove schemaDefaults, use caller-provided dryRunMeta

Cursor completed removal of schemaDefaults. All dry-run paths now
use explicit dryRunMeta from the caller.
This commit is contained in:
2026-05-06 08:10:40 +00:00
parent 82d3478895
commit 6e62c7458d
15 changed files with 96 additions and 223 deletions
@@ -58,7 +58,7 @@ describe("createCommitterRole", () => {
}; };
const role = createCommitterRole( const role = createCommitterRole(
agent, agent,
{ provider, dryRun: null }, { provider, dryRun: null, dryRunMeta: { branch: "dry-run", message: "chore: dry run" } },
{ cwd: repo, remote: "origin", threadId: null }, { cwd: repo, remote: "origin", threadId: null },
); );
const out = await role(makeCtx()); const out = await role(makeCtx());
@@ -69,7 +69,11 @@ describe("createCommitterRole", () => {
const agent: AgentFn = async () => { const agent: AgentFn = async () => {
throw new Error("agent should not run"); throw new Error("agent should not run");
}; };
const role = createCommitterRole(agent, { provider, dryRun: true }); const role = createCommitterRole(agent, {
provider,
dryRun: true,
dryRunMeta: { branch: "dry-run", message: "chore: dry run" },
});
const out = await role(makeCtx()); const out = await role(makeCtx());
expect(out.content).toBe("[dry-run] committer skipped"); expect(out.content).toBe("[dry-run] committer skipped");
expect(out.meta).toEqual({ committed: true }); expect(out.meta).toEqual({ committed: true });
@@ -87,7 +91,7 @@ describe("createCommitterRole", () => {
const agent: AgentFn = async () => "plan text"; const agent: AgentFn = async () => "plan text";
const role = createCommitterRole( const role = createCommitterRole(
agent, agent,
{ provider, dryRun: null }, { provider, dryRun: null, dryRunMeta: { branch: "dry-run", message: "chore: dry run" } },
{ cwd: repo, remote: "origin", threadId: null }, { cwd: repo, remote: "origin", threadId: null },
); );
@@ -18,6 +18,8 @@ const committerPlanSchema = z.object({
message: z.string().describe("Single-line conventional commit subject"), message: z.string().describe("Single-line conventional commit subject"),
}); });
export type CommitterPlanMeta = z.infer<typeof committerPlanSchema>;
export type CommitterGitConfig = { export type CommitterGitConfig = {
cwd: string; cwd: string;
remote: string; remote: string;
@@ -90,7 +92,7 @@ Reply with enough detail that a maintainer understands the change; structured ex
async function runCommitterPipeline( async function runCommitterPipeline(
ctx: ThreadContext, ctx: ThreadContext,
agent: AgentFn, agent: AgentFn,
extract: { provider: LlmProvider; dryRun: boolean | null }, extract: { provider: LlmProvider; dryRun: boolean | null; dryRunMeta: CommitterPlanMeta },
gitConfig: CommitterGitConfig, gitConfig: CommitterGitConfig,
): Promise<RoleResult<CommitterMeta>> { ): Promise<RoleResult<CommitterMeta>> {
const cwd = gitConfig.cwd; const cwd = gitConfig.cwd;
@@ -107,6 +109,7 @@ async function runCommitterPipeline(
const plan = await extractMetaOrThrow("committer-plan", raw, committerPlanSchema, { const plan = await extractMetaOrThrow("committer-plan", raw, committerPlanSchema, {
provider: extract.provider, provider: extract.provider,
dryRun: resolveExtractDryRun(extract.dryRun), dryRun: resolveExtractDryRun(extract.dryRun),
dryRunMeta: extract.dryRunMeta,
}); });
const branch = sanitizeBranch(plan.branch); const branch = sanitizeBranch(plan.branch);
@@ -129,7 +132,7 @@ async function runCommitterPipeline(
*/ */
export function createCommitterRole( export function createCommitterRole(
adapter: AgentFn, adapter: AgentFn,
extract: { provider: LlmProvider; dryRun: boolean | null }, extract: { provider: LlmProvider; dryRun: boolean | null; dryRunMeta: CommitterPlanMeta },
gitConfig: CommitterGitConfig = DEFAULT_COMMITTER_GIT_CONFIG, gitConfig: CommitterGitConfig = DEFAULT_COMMITTER_GIT_CONFIG,
): Role<CommitterMeta> { ): Role<CommitterMeta> {
const inner: Role<CommitterMeta> = async (ctx) => const inner: Role<CommitterMeta> = async (ctx) =>
@@ -1,6 +1,7 @@
export { export {
type CommitterGitConfig, type CommitterGitConfig,
type CommitterMeta, type CommitterMeta,
type CommitterPlanMeta,
committerMetaSchema, committerMetaSchema,
createCommitterRole, createCommitterRole,
DEFAULT_COMMITTER_GIT_CONFIG, DEFAULT_COMMITTER_GIT_CONFIG,
@@ -64,7 +64,7 @@ describe("createRole", () => {
schema, schema,
systemPrompt: "hello", systemPrompt: "hello",
agent, agent,
extract: { provider, dryRun: null }, extract: { provider, dryRun: null, dryRunMeta: { n: 0 } },
}); });
const out = await role(makeCtx()); const out = await role(makeCtx());
@@ -85,7 +85,7 @@ describe("createRole", () => {
schema: z.object({ n: z.number() }), schema: z.object({ n: z.number() }),
systemPrompt: "p", systemPrompt: "p",
agent, agent,
extract: { provider, dryRun: null }, extract: { provider, dryRun: null, dryRunMeta: { n: 0 } },
}); });
await role(makeCtx()); await role(makeCtx());
@@ -103,7 +103,7 @@ describe("createRole", () => {
schema, schema,
systemPrompt: async (ctx) => `rounds=${ctx.steps.length}`, systemPrompt: async (ctx) => `rounds=${ctx.steps.length}`,
agent, agent,
extract: { provider, dryRun: null }, extract: { provider, dryRun: null, dryRunMeta: { n: 0 } },
}); });
const ctx = makeCtx(); const ctx = makeCtx();
@@ -121,7 +121,7 @@ describe("createRole", () => {
schema: z.object({ n: z.number() }), schema: z.object({ n: z.number() }),
systemPrompt: "p", systemPrompt: "p",
agent, agent,
extract: { provider, dryRun: null }, extract: { provider, dryRun: null, dryRunMeta: { n: 0 } },
}); });
await role(makeCtx()); await role(makeCtx());
@@ -129,7 +129,7 @@ describe("createRole", () => {
"r1", "r1",
"raw", "raw",
expect.anything(), expect.anything(),
expect.objectContaining({ provider, dryRun: false }), expect.objectContaining({ provider, dryRun: false, dryRunMeta: { n: 0 } }),
); );
}); });
@@ -142,7 +142,7 @@ describe("createRole", () => {
schema: z.object({ n: z.number() }), schema: z.object({ n: z.number() }),
systemPrompt: "p", systemPrompt: "p",
agent, agent,
extract: { provider, dryRun: true }, extract: { provider, dryRun: true, dryRunMeta: { n: 0 } },
}); });
await role(makeCtx()); await role(makeCtx());
@@ -150,7 +150,7 @@ describe("createRole", () => {
"r2", "r2",
"raw", "raw",
expect.anything(), expect.anything(),
expect.objectContaining({ dryRun: true }), expect.objectContaining({ dryRun: true, dryRunMeta: { n: 0 } }),
); );
}); });
}); });
@@ -12,7 +12,7 @@ const provider = {
describe("extractMetaOrThrow", () => { describe("extractMetaOrThrow", () => {
const originalFetch = globalThis.fetch; const originalFetch = globalThis.fetch;
test("dryRun returns schema-shaped defaults without calling fetch", async () => { test("dryRun returns dryRunMeta without calling fetch", async () => {
let calls = 0; let calls = 0;
globalThis.fetch = () => { globalThis.fetch = () => {
calls += 1; calls += 1;
@@ -23,12 +23,13 @@ describe("extractMetaOrThrow", () => {
const out = await extractMetaOrThrow("r", "raw", schema, { const out = await extractMetaOrThrow("r", "raw", schema, {
provider, provider,
dryRun: true, dryRun: true,
dryRunMeta: { n: 7 },
}); });
globalThis.fetch = originalFetch; globalThis.fetch = originalFetch;
expect(calls).toBe(0); expect(calls).toBe(0);
expect(out).toEqual({ n: 0 }); expect(out).toEqual({ n: 7 });
}); });
test("throws when extraction fails after retry", async () => { test("throws when extraction fails after retry", async () => {
@@ -53,7 +54,7 @@ describe("extractMetaOrThrow", () => {
const schema = z.object({ n: z.number() }); const schema = z.object({ n: z.number() });
await expect( await expect(
extractMetaOrThrow("plan", "text", schema, { provider, dryRun: false }), extractMetaOrThrow("plan", "text", schema, { provider, dryRun: false, dryRunMeta: { n: 0 } }),
).rejects.toThrow(/structured extraction failed after retry/); ).rejects.toThrow(/structured extraction failed after retry/);
globalThis.fetch = originalFetch; globalThis.fetch = originalFetch;
@@ -91,6 +92,7 @@ describe("extractMetaOrThrow", () => {
const out = await extractMetaOrThrow("committer-plan", "plan text", schema, { const out = await extractMetaOrThrow("committer-plan", "plan text", schema, {
provider, provider,
dryRun: false, dryRun: false,
dryRunMeta: { branch: "", message: "" },
}); });
globalThis.fetch = originalFetch; globalThis.fetch = originalFetch;
@@ -55,6 +55,7 @@ describe("llmExtract", () => {
model: "m", model: "m",
}, },
dryRun: false, dryRun: false,
dryRunMeta: { name: "", description: "" },
}); });
globalThis.fetch = originalFetch; globalThis.fetch = originalFetch;
@@ -105,6 +106,7 @@ describe("llmExtract", () => {
schema, schema,
provider: { baseUrl: "https://example.com", apiKey: "k", model: "m" }, provider: { baseUrl: "https://example.com", apiKey: "k", model: "m" },
dryRun: false, dryRun: false,
dryRunMeta: { n: 0 },
}); });
globalThis.fetch = originalFetch; globalThis.fetch = originalFetch;
@@ -116,7 +118,7 @@ describe("llmExtract", () => {
expect(result.error.kind).toBe("schema_validation_failed"); expect(result.error.kind).toBe("schema_validation_failed");
}); });
test("dryRun skips fetch and returns schema-shaped stub values", async () => { test("dryRun skips fetch and returns dryRunMeta", async () => {
let calls = 0; let calls = 0;
globalThis.fetch = () => { globalThis.fetch = () => {
calls += 1; calls += 1;
@@ -129,6 +131,7 @@ describe("llmExtract", () => {
schema, schema,
provider: { baseUrl: "https://example.com", apiKey: "k", model: "m" }, provider: { baseUrl: "https://example.com", apiKey: "k", model: "m" },
dryRun: true, dryRun: true,
dryRunMeta: { n: 42 },
}); });
globalThis.fetch = originalFetch; globalThis.fetch = originalFetch;
@@ -138,6 +141,6 @@ describe("llmExtract", () => {
if (!result.ok) { if (!result.ok) {
return; return;
} }
expect(result.value).toEqual({ n: 0 }); expect(result.value).toEqual({ n: 42 });
}); });
}); });
@@ -10,8 +10,9 @@ export type CreateRoleArgs<M extends Record<string, unknown>> = {
agent: AgentFn; agent: AgentFn;
extract: { extract: {
provider: LlmProvider; provider: LlmProvider;
/** When `true`, structured extract returns schema-shaped defaults. When `null`, live API extract. */ /** When `true`, structured extract returns `dryRunMeta`. When `null`, live API extract. */
dryRun: boolean | null; dryRun: boolean | null;
dryRunMeta: M;
}; };
}; };
@@ -28,6 +29,7 @@ export function createRole<M extends Record<string, unknown>>(args: CreateRoleAr
const meta = await extractMetaOrThrow(args.name, raw, args.schema, { const meta = await extractMetaOrThrow(args.name, raw, args.schema, {
provider: args.extract.provider, provider: args.extract.provider,
dryRun: resolveExtractDryRun(args.extract.dryRun), dryRun: resolveExtractDryRun(args.extract.dryRun),
dryRunMeta: args.extract.dryRunMeta,
}); });
return { content: raw, meta }; return { content: raw, meta };
}; };
@@ -6,13 +6,14 @@ export async function extractMetaOrThrow<T extends Record<string, unknown>>(
roleName: string, roleName: string,
raw: string, raw: string,
schema: z.ZodType<T>, schema: z.ZodType<T>,
options: { provider: LlmProvider; dryRun: boolean }, options: { provider: LlmProvider; dryRun: boolean; dryRunMeta: T },
): Promise<T> { ): Promise<T> {
const result = await llmExtractWithRetry({ const result = await llmExtractWithRetry({
text: raw, text: raw,
schema, schema,
provider: options.provider, provider: options.provider,
dryRun: options.dryRun, dryRun: options.dryRun,
dryRunMeta: options.dryRunMeta,
}); });
if (!result.ok) { if (!result.ok) {
throw new Error( throw new Error(
-1
View File
@@ -3,7 +3,6 @@ export {
type OnFailOptions, type OnFailOptions,
onFail, onFail,
type RoleDecorator, type RoleDecorator,
schemaDefaults,
type WithDryRunOptions, type WithDryRunOptions,
withDryRun, withDryRun,
} from "@uncaged/workflow-util-role"; } from "@uncaged/workflow-util-role";
@@ -1,5 +1,4 @@
import { err, ok, type Result } from "@uncaged/workflow"; import { err, ok, type Result } from "@uncaged/workflow";
import { schemaDefaults } from "@uncaged/workflow-util-role";
import * as z from "zod/v4"; import * as z from "zod/v4";
import type { LlmProvider } from "./types.js"; import type { LlmProvider } from "./types.js";
@@ -10,6 +9,8 @@ export type LlmExtractArgs<T> = {
schema: z.ZodType<T>; schema: z.ZodType<T>;
provider: LlmProvider; provider: LlmProvider;
dryRun: boolean; dryRun: boolean;
/** Returned when `dryRun` is true (ignored for live extract). */
dryRunMeta: T;
}; };
export type LlmError = export type LlmError =
@@ -128,7 +129,7 @@ async function performLlmExtract<T>(
options: LlmExtractArgs<T> & { userContent: string }, options: LlmExtractArgs<T> & { userContent: string },
): Promise<Result<T, LlmError>> { ): Promise<Result<T, LlmError>> {
if (options.dryRun) { if (options.dryRun) {
return ok(schemaDefaults(options.schema) as T); return ok(options.dryRunMeta);
} }
const rawJsonSchema = z.toJSONSchema(options.schema) as Record<string, unknown>; const rawJsonSchema = z.toJSONSchema(options.schema) as Record<string, unknown>;
@@ -58,7 +58,11 @@ describe("createReviewerRole", () => {
return "review done"; return "review done";
}; };
const role = createReviewerRole(agent, { provider, dryRun: null }); const role = createReviewerRole(agent, {
provider,
dryRun: null,
dryRunMeta: { approved: true },
});
const out = await role(makeCtx()); const out = await role(makeCtx());
expect(out.meta).toEqual({ approved: true }); expect(out.meta).toEqual({ approved: true });
}); });
@@ -74,7 +78,7 @@ describe("createReviewerRole", () => {
const role = createReviewerRole( const role = createReviewerRole(
agent, agent,
{ provider, dryRun: null }, { provider, dryRun: null, dryRunMeta: { approved: false } },
{ {
cwd: "/proj", cwd: "/proj",
conventionsPath: null, conventionsPath: null,
@@ -90,7 +90,7 @@ or
*/ */
export function createReviewerRole( export function createReviewerRole(
adapter: AgentFn, adapter: AgentFn,
extract: { provider: LlmProvider; dryRun: boolean | null }, extract: { provider: LlmProvider; dryRun: boolean | null; dryRunMeta: ReviewerMeta },
config: ReviewerConfig = DEFAULT_REVIEWER_CONFIG, config: ReviewerConfig = DEFAULT_REVIEWER_CONFIG,
): Role<ReviewerMeta> { ): Role<ReviewerMeta> {
return createRole({ return createRole({
@@ -98,6 +98,10 @@ export function createReviewerRole(
schema: reviewerMetaSchema, schema: reviewerMetaSchema,
systemPrompt: async (ctx) => reviewerPrompt(config, ctx), systemPrompt: async (ctx) => reviewerPrompt(config, ctx),
agent: adapter, agent: adapter,
extract: { provider: extract.provider, dryRun: extract.dryRun }, extract: {
provider: extract.provider,
dryRun: extract.dryRun,
dryRunMeta: extract.dryRunMeta,
},
}); });
} }
@@ -1,5 +1,9 @@
import type { AgentFn, Role } from "@uncaged/workflow"; import type { AgentFn, Role } from "@uncaged/workflow";
import { type CommitterMeta, createCommitterRole } from "@uncaged/workflow-role-committer"; import {
type CommitterMeta,
type CommitterPlanMeta,
createCommitterRole,
} from "@uncaged/workflow-role-committer";
import { createRole, type LlmProvider } from "@uncaged/workflow-role-llm"; import { createRole, type LlmProvider } from "@uncaged/workflow-role-llm";
import { createReviewerRole, type ReviewerMeta } from "@uncaged/workflow-role-reviewer"; import { createReviewerRole, type ReviewerMeta } from "@uncaged/workflow-role-reviewer";
import * as z from "zod/v4"; import * as z from "zod/v4";
@@ -33,6 +37,26 @@ export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
export type CoderMeta = z.infer<typeof coderMetaSchema>; export type CoderMeta = z.infer<typeof coderMetaSchema>;
const PLANNER_DRY_RUN_META: PlannerMeta = {
plan: "",
files: [],
approach: "",
};
const CODER_DRY_RUN_META: CoderMeta = {
filesChanged: [],
summary: "",
};
const REVIEWER_DRY_RUN_META: ReviewerMeta = {
approved: true,
};
const COMMITTER_PLAN_DRY_RUN_META: CommitterPlanMeta = {
branch: "dry-run",
message: "chore: dry run",
};
export type SolveIssueMeta = { export type SolveIssueMeta = {
planner: PlannerMeta; planner: PlannerMeta;
coder: CoderMeta; coder: CoderMeta;
@@ -40,7 +64,7 @@ export type SolveIssueMeta = {
committer: CommitterMeta; committer: CommitterMeta;
}; };
/** Wiring for workflow-role LLM structured extraction. Use null for schema-default dry runs (tests / stubs). */ /** Wiring for workflow-role LLM structured extraction. Use `null` for stub extract (dry-run meta from built-in placeholders). */
export type SolveIssueRolesConfig = { export type SolveIssueRolesConfig = {
agent: AgentFn; agent: AgentFn;
workdir: string; workdir: string;
@@ -83,7 +107,11 @@ export function createSolveIssueRoles(config: SolveIssueRolesConfig): SolveIssue
schema: plannerMetaSchema, schema: plannerMetaSchema,
systemPrompt: PLANNER_SYSTEM, systemPrompt: PLANNER_SYSTEM,
agent: config.agent, agent: config.agent,
extract: { provider: extract.provider, dryRun: extract.dryRun }, extract: {
provider: extract.provider,
dryRun: extract.dryRun,
dryRunMeta: PLANNER_DRY_RUN_META,
},
}); });
const coder: Role<CoderMeta> = createRole({ const coder: Role<CoderMeta> = createRole({
@@ -91,18 +119,30 @@ export function createSolveIssueRoles(config: SolveIssueRolesConfig): SolveIssue
schema: coderMetaSchema, schema: coderMetaSchema,
systemPrompt: CODER_SYSTEM, systemPrompt: CODER_SYSTEM,
agent: config.agent, agent: config.agent,
extract: { provider: extract.provider, dryRun: extract.dryRun }, extract: {
provider: extract.provider,
dryRun: extract.dryRun,
dryRunMeta: CODER_DRY_RUN_META,
},
}); });
const reviewer: Role<ReviewerMeta> = createReviewerRole( const reviewer: Role<ReviewerMeta> = createReviewerRole(
config.agent, config.agent,
{ provider: extract.provider, dryRun: extract.dryRun }, {
provider: extract.provider,
dryRun: extract.dryRun,
dryRunMeta: REVIEWER_DRY_RUN_META,
},
reviewerGit, reviewerGit,
); );
const committer: Role<CommitterMeta> = createCommitterRole( const committer: Role<CommitterMeta> = createCommitterRole(
config.agent, config.agent,
{ provider: extract.provider, dryRun: extract.dryRun }, {
provider: extract.provider,
dryRun: extract.dryRun,
dryRunMeta: COMMITTER_PLAN_DRY_RUN_META,
},
committerGit, committerGit,
); );
-1
View File
@@ -6,4 +6,3 @@ export {
type WithDryRunOptions, type WithDryRunOptions,
withDryRun, withDryRun,
} from "./decorators.js"; } from "./decorators.js";
export { schemaDefaults } from "./schema-defaults.js";
@@ -1,190 +0,0 @@
import type * as z from "zod/v4";
type ZodTypeAny = z.ZodType;
type Def = Record<string, unknown> & { type: string };
type TypeHandler = (schema: ZodTypeAny, def: Def) => unknown;
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isZodExactOptional(s: ZodTypeAny): boolean {
return s.constructor.name === "ZodExactOptional";
}
function resolveDefaultValue(defaultValue: unknown | (() => unknown)): unknown {
if (typeof defaultValue === "function") {
return (defaultValue as () => unknown)();
}
return defaultValue;
}
function mergeIntersection(left: unknown, right: unknown): unknown {
if (isPlainObject(left) && isPlainObject(right)) {
return { ...left, ...right };
}
return right;
}
function defaultsForObject(_schema: ZodTypeAny, def: Def): unknown {
const shape = def.shape as Record<string, ZodTypeAny> | undefined;
if (shape === undefined) {
return {};
}
const out: Record<string, unknown> = {};
for (const key of Object.keys(shape)) {
const child = shape[key];
const cdef = child.def as { type: string };
if (cdef.type === "optional") {
if (isZodExactOptional(child)) {
continue;
}
out[key] = undefined;
} else {
out[key] = schemaDefaultsInner(child);
}
}
return out;
}
function firstUnionOption(_schema: ZodTypeAny, def: Def): unknown {
const options = def.options as readonly ZodTypeAny[] | undefined;
if (options === undefined || options.length === 0) {
return null;
}
return schemaDefaultsInner(options[0]);
}
function defaultsFromNullable(_schema: ZodTypeAny, _def: Def): unknown {
return null;
}
function defaultsFromInner(_schema: ZodTypeAny, def: Def): unknown {
const inner = def.innerType as ZodTypeAny | undefined;
if (inner === undefined) {
return null;
}
return schemaDefaultsInner(inner);
}
function defaultsForPipe(_schema: ZodTypeAny, def: Def): unknown {
const out = def.out as ZodTypeAny | undefined;
if (out === undefined) {
return null;
}
return schemaDefaultsInner(out);
}
function defaultsForIntersection(_schema: ZodTypeAny, def: Def): unknown {
const left = def.left as ZodTypeAny | undefined;
const right = def.right as ZodTypeAny | undefined;
if (left === undefined || right === undefined) {
return null;
}
return mergeIntersection(schemaDefaultsInner(left), schemaDefaultsInner(right));
}
function defaultsForTuple(_schema: ZodTypeAny, def: Def): unknown {
const items = def.items as readonly ZodTypeAny[] | undefined;
if (items === undefined) {
return [];
}
return items.map((item) => schemaDefaultsInner(item));
}
function defaultsForLazy(schema: ZodTypeAny, def: Def): unknown {
const inner =
(schema as { _zod?: { innerType?: ZodTypeAny } })._zod?.innerType ??
(def.getter as (() => ZodTypeAny) | undefined)?.();
if (inner === undefined) {
return null;
}
return schemaDefaultsInner(inner);
}
function defaultsForPromise(_schema: ZodTypeAny, def: Def): unknown {
const inner = def.innerType as ZodTypeAny | undefined;
if (inner === undefined) {
return Promise.resolve(null);
}
return Promise.resolve(schemaDefaultsInner(inner));
}
function firstEnumValue(_schema: ZodTypeAny, def: Def): unknown {
const entries = def.entries as Record<string, string | number> | undefined;
if (entries === undefined) {
return null;
}
const values = Object.values(entries);
return values[0] ?? null;
}
function firstLiteralValue(_schema: ZodTypeAny, def: Def): unknown {
const values = def.values as unknown[] | undefined;
if (values === undefined || values.length === 0) {
return null;
}
return values[0];
}
const TYPE_HANDLERS: Record<string, TypeHandler> = {
string: () => "",
number: () => 0,
boolean: () => false,
bigint: () => 0n,
date: () => new Date(0),
symbol: () => Symbol(),
undefined: () => undefined,
null: () => null,
void: () => undefined,
any: () => null,
unknown: () => null,
never: () => undefined,
nan: () => Number.NaN,
array: () => [],
object: defaultsForObject,
record: () => ({}),
map: () => new Map(),
set: () => new Set(),
enum: firstEnumValue,
literal: firstLiteralValue,
optional: () => undefined,
nullable: defaultsFromNullable,
default: (_s, def) => resolveDefaultValue(def.defaultValue as unknown | (() => unknown)),
prefault: (_s, def) => resolveDefaultValue(def.defaultValue as unknown | (() => unknown)),
nonoptional: defaultsFromInner,
catch: defaultsFromInner,
success: () => false,
readonly: defaultsFromInner,
union: firstUnionOption,
xor: firstUnionOption,
intersection: defaultsForIntersection,
pipe: defaultsForPipe,
transform: () => null,
tuple: defaultsForTuple,
lazy: defaultsForLazy,
promise: defaultsForPromise,
file: () => new File([], ""),
function: () => null,
custom: () => null,
template_literal: () => "",
};
/**
* Produces a structurally valid placeholder that mirrors primitive/array/object
* shape for a Zod schema. Used for `llmExtract` dry runs so downstream code
* does not throw on `undefined` fields.
*/
export function schemaDefaults(schema: z.ZodType): unknown {
return schemaDefaultsInner(schema as ZodTypeAny);
}
function schemaDefaultsInner(schema: ZodTypeAny): unknown {
const def = schema.def as Def;
const run = TYPE_HANDLERS[def.type];
if (run === undefined) {
return null;
}
return run(schema, def);
}