Skip to main content

When to Use

When adding a new prompt unit type, modifying an existing type’s required/optional fields, or extending the base schema shared by all types.

Package Location

packages/schema/ in the stratt-run repo. Source of truth for all unit validation.

Architecture

src/constants.ts          ← enums, literal arrays (add new values here first)
src/types/base.ts         ← BaseSchema + MetaSchema (shared by ALL types)
src/types/contract.ts     ← ContractSchema (inputs/outputs/failure_modes)
src/types/composition.ts  ← CompositionSchema (chain steps)
src/types/{type}.ts       ← Per-type schema extending BaseSchema
src/types/index.ts        ← Discriminated union of all types
src/validate.ts           ← Full validation pipeline

How to Add a New Unit Type

  1. Add to constants.ts: Add the type name to UNIT_TYPES array. Add its SPUH bit value to TYPE_BITS.
  2. Create type file: src/types/{newtype}.ts — extend BaseSchema with type: z.literal("newtype") plus type-specific blocks.
  3. Update types/index.ts: Import the new schema, add to z.discriminatedUnion("type", [...]), export schema and type.
  4. Update validate.ts: Add any forbidden block checks (e.g., composition forbidden for your type).
  5. Update spuh.ts constants: Ensure BITS_TO_TYPE reverse map covers the new bit value.
  6. Write tests: At least 3 tests — valid parse, valid with optionals, deliberate failure.
  7. Update barrel: Re-export from src/index.ts.

Pattern: Extending BaseSchema

import { z } from "zod";
import { BaseSchema } from "./base.js";

export const NewTypeSchema = BaseSchema.extend({
  type: z.literal("newtype"),
  // required fields for this type:
  my_block: z.object({ ... }),
  // optional fields:
  contract: ContractSchema.optional(),
});
The .extend() method overrides the base type: z.enum(UNIT_TYPES) with the specific literal, enabling discriminated union dispatch.

Pattern: Required vs Optional vs Forbidden Blocks

BlockRequired forOptional forForbidden for
contracttask, chainrole, rule, fragment
compositionchainrole, rule, fragment
personarole
rule_blockrule
prompt_bodytask
fragment_bodyfragment
counciltask, chainrole, rule
Forbidden blocks cannot be enforced at the Zod level (Zod strips unknown keys). They are checked in validate.ts against the raw input object.

Gotcha: Zod .default() Behaviour

z.boolean().default(false) injects false during .parse() when the key is absent. This means the parsed output always has the field — test both explicit and omitted cases.

Verification

cd packages/schema
bun run test        # 87+ tests must pass
bun run typecheck   # zero errors