Lucentive Labs
Lucentive Labs Docs
Packages / API

@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 Config

validateConfig(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 asset key not present in config.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));
dump it from the CLI
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:

FieldTypeNotes
version1literal
titlestring?shown in header + brief
assetsRecord<string, Asset>defaults to {}
themeThemeTokens?--loupe-* tokens (no prefix)
groupsGroup[]at least 1
previewPreview?band → slot mapping
bannedstring[]?flows into the brief
notesstring[]?author notes
workflowstring[]?flows into the brief

Nested shapes

Group · Option
type Group = {
  id: string;
  title: string;
  prompt?: string;
  options: Option[];   // 2..6
};

type Option = {
  id: string;
  label: string;
  caption?: string;
  recommended?: boolean;
  specimen: Specimen;
};
Asset · the crop Rect
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 <= 1
Specimen (discriminated union on kind)
type 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";
Preview
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:

ExportKind
Config, Group, Option, Specimen, Asset, Rect, PreviewZod schema and inferred type
MotionPreset, LayoutPlan, ThemeTokensZod schema
TGroup, TOption, TSpecimen, TSpecimenKind, TRect, TAsset, TPreview, TThemeTokens, TMotionPreset, TLayoutPlaninferred types

ThemeTokens is z.record(z.string(), z.string()) — any kebab key (without the --loupe- prefix) mapped to any CSS value. See Theming.