feat(intake): constrain what a draft may contain (#223)

The SDK validates the model's response against this before any of it reaches the database, so a model that answers in prose or invents a field becomes a caught error rather than a row full of nonsense.

Everything the model may decline to answer is nullable, because it is told to say nothing rather than guess. A null category is a better answer than a wrong one, and resolving it is what the review queue is for. The name and description are not nullable: a draft without them is not a partial success worth storing.

The price is an integer, bounded at both ends. A fractional, negative or absurd figure reaching the review queue is a number somebody has to notice is wrong, and being trustworthy at a glance is that queue's whole job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 08:32:40 -05:00
co-authored by Claude Opus 5
parent 1b87e08262
commit 22910ce9e2
2 changed files with 101 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
import { z } from 'zod';
/**
* What the model must return, enforced rather than hoped for.
*
* Constraining the shape is the difference between a bad draft and a crash:
* the SDK validates the response against this before any of it reaches the
* database, so a model that answers in prose or invents a field becomes a
* caught error rather than a row full of nonsense.
*
* Every field the model may decline to answer is nullable, because it is told
* to say nothing rather than guess. A null category means it did not recognise
* one, which is a better answer than a wrong one and is exactly what the review
* queue exists to resolve.
*/
/** Beyond anything this shop sells, so an absurd number is caught here. */
const MAX_SUGGESTED_PRICE_CENTS = 1_000_000;
export const DraftSchema = z.object({
/** A short title. Until this lands, the item is named for its submission. */
name: z.string().min(1).max(200),
/** The storefront body, rendered with html:false like every other stored body. */
description: z.string().min(1).max(4000),
/**
* Chosen from the categories it was given, or null. The prompt supplies the
* closed set; this cannot enforce membership, so applyDraft checks the answer
* against the real table before writing anything.
*/
category: z.string().nullable(),
/** Also from a supplied set, and also checked on the way in rather than here. */
tags: z.array(z.string()),
/**
* Cents, or null when it will not guess. Integer and bounded at both ends,
* because a fractional, negative or absurd figure reaching the review queue
* is a number somebody has to notice is wrong — and being trustworthy at a
* glance is that queue's whole job.
*/
suggestedPriceCents: z
.number()
.int()
.min(0)
.max(MAX_SUGGESTED_PRICE_CENTS)
.nullable()
});
export type DraftResult = z.infer<typeof DraftSchema>;