@lucentive-labs/loupe-schema
The Zod config contract a human or agent fills — Config, parseConfig, toJsonSchema, validateConfig.
The single source of truth for what a Loupe config is. Zod is canonical; TypeScript types are inferred from it, and toJsonSchema() emits the contract an agent fills. Everything downstream consumes these types — nothing redefines the shape.
import {
Config,
parseConfig,
toJsonSchema,
validateConfig,
} from "@lucentive-labs/loupe-schema";Functions
parseConfig(input: unknown): Config
Structural parse. Runs the Zod schema and throws a ZodError on any structural problem — wrong version, missing required fields, a crop rect with zero area or out of bounds. Returns a fully-typed Config (with defaults like assets: {} applied) on success.
const cfg = parseConfig(raw); // throws if raw is not a valid ConfigvalidateConfig(cfg: Config): string[]
Semantic validation beyond structure. Takes an already-parsed Config and returns an array of human-readable error strings — empty means valid. It catches what types cannot:
- duplicate group ids, and duplicate option ids within a group;
- options whose specimen references an
assetkey not present inconfig.assets; - preview bands (and
headlineFrom) that point at a missing group.
const errs = validateConfig(cfg);
if (errs.length) throw new Error("Invalid:\n- " + errs.join("\n- "));Use both, in order: parseConfig first (structure), then validateConfig (references). The generator runs exactly this pair before building, and the interactive tutorial runs it on every render.
toJsonSchema(): Record<string, unknown>
Emits the JSON Schema for the Config, pinned to input mode — the shape an author writes, before defaults are applied. This is the contract you hand an agent so it can fill a config without writing TypeScript.
import { toJsonSchema } from "@lucentive-labs/loupe-schema";
console.log(JSON.stringify(toJsonSchema(), null, 2));tsx -e "import('@lucentive-labs/loupe-schema').then(m => console.log(JSON.stringify(m.toJsonSchema(), null, 2)))"See the agent method for the JSON-Schema-as-contract workflow.
The Config type
Inferred from the Zod schema. The full shape:
| Field | Type | Notes |
|---|---|---|
version | 1 | literal |
title | string? | shown in header + brief |
assets | Record<string, Asset> | defaults to {} |
theme | ThemeTokens? | --loupe-* tokens (no prefix) |
groups | Group[] | at least 1 |
preview | Preview? | band → slot mapping |
banned | string[]? | flows into the brief |
notes | string[]? | author notes |
workflow | string[]? | flows into the brief |
Nested shapes
type Group = {
id: string;
title: string;
prompt?: string;
options: Option[]; // 2..6
};
type Option = {
id: string;
label: string;
caption?: string;
recommended?: boolean;
specimen: Specimen;
};type Asset = { src: string; width: number; height: number }; // intrinsic px
type Rect = { x: number; y: number; w: number; h: number }; // each 0..1
// enforced: w > 0, h > 0, x + w <= 1, y + h <= 1type Specimen =
| { kind: "imageCrop"; asset: string; crop: Rect; alt: string }
| { kind: "palette"; colors: string[]; over?: string } // 1..8 colors
| { kind: "type"; family: string; weight?: number; sample: string; kicker?: string }
| { kind: "motion"; preset: "breathe" | "pan" | "field"; asset?: string; crop?: Rect; asset2?: string; crop2?: Rect }
| { kind: "layoutMock"; plan: LayoutPlan; asset?: string; crop?: Rect; asset2?: string; crop2?: Rect };
type LayoutPlan =
| "portrait" | "threshold" | "sparse" | "chapters" | "belowfold" | "stack";type Preview = {
bands: { slot: string; fromGroup: string; as?: "feature" | "band" | "swatch" | "headline" | "systems" }[];
headlineFrom?: string;
};Exported schemas + types
The package exports both the Zod schemas (for composition) and the inferred types:
| Export | Kind |
|---|---|
Config, Group, Option, Specimen, Asset, Rect, Preview | Zod schema and inferred type |
MotionPreset, LayoutPlan, ThemeTokens | Zod schema |
TGroup, TOption, TSpecimen, TSpecimenKind, TRect, TAsset, TPreview, TThemeTokens, TMotionPreset, TLayoutPlan | inferred types |
ThemeTokens is z.record(z.string(), z.string()) — any kebab key (without the --loupe- prefix) mapped to any CSS value. See Theming.