.claude/skills/rule-schemas/SKILL.md
Rule mapping for schemas
npx skillsauth add carrot-foundation/methodology-rules rule-schemasInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Apply this rule whenever work touches:
*.tsZod is the runtime validation library for this project. Schemas serve a dual purpose: they validate data at runtime boundaries and provide the canonical type definition via z.infer.
Define schemas close to the types they describe. A common pattern is to export both from the same file:
import { z } from 'zod';
export const VehicleSchema = z.object({
plate: z.string().min(7),
weightKg: z.number().positive(),
type: z.enum(['truck', 'van', 'car']),
});
export type Vehicle = z.infer<typeof VehicleSchema>;
Never create a separate interface Vehicle that duplicates the schema shape.
Choose the right parse method based on the trust level of the data:
// External input (API payload, S3 object, SQS message) - handle errors
const result = VehicleSchema.safeParse(rawPayload);
if (!result.success) {
logger.warn('Invalid vehicle payload', result.error.flatten());
return { error: 'INVALID_INPUT' };
}
const vehicle = result.data;
// Internal data (already validated upstream) - let it throw
const vehicle = VehicleSchema.parse(trustedData);
Reuse schemas through composition rather than copy-pasting fields:
const BaseDocumentSchema = z.object({
id: z.string().uuid(),
createdAt: z.string().datetime(),
});
const CertificateSchema = BaseDocumentSchema.extend({
issuer: z.string(),
validUntil: z.string().datetime(),
});
Use zocker and the shared testing utilities to generate test data from schemas:
import { createStubFromSchema } from '@carrot-fndn/shared/testing';
const stubVehicle = createStubFromSchema(VehicleSchema);
This ensures test data always conforms to the current schema shape and evolves automatically when the schema changes.
Document schemas are organized in five layers with progressive strictness:
| Layer | Naming convention | Zod strategy | Purpose |
|-------|------------------|--------------|---------|
| Envelope | LoadedDocumentEnvelopeSchema | z.object | Wrapper for loaded documents |
| Inbound | Inbound*Schema | z.object (strips unknown fields) | Boundary contract for external data |
| Normalized | *Schema | z.object (strips unknown fields) | Strict internal representation |
| Domain | Bold*Schema, MassID*Schema, RewardsDistribution*Schema | z.object with methodology-specific fields | Methodology-specific extensions |
| Rule | *RuleSubjectSchema | z.object with exact fields needed | Exact contract per rule processor |
Key conventions:
z.object: Every layer strips unknown fields on parse. Do not use z.looseObject in layer schemas — each layer should be more specific, not more permissive.z.looseObject for predicate/getter validators only: Narrow structural checks in predicate or getter helpers (e.g. "does this event have a metadata.attributes array?") may use z.looseObject to avoid rejecting objects that carry extra fields they don't inspect. These are not layer schemas.*RuleSubjectSchema that describes the exact data it needs.validateRuleSubjectOrThrow: Standard entry point for rule subject validation. Use this instead of calling .parse() directly.z.enum() for value sets: Define allowed values as a const object, then derive a z.enum() from its values. Use .extract() to narrow to a subset and .literal() for single-value constraints in deeper layers. Domain-scoped enums use declaration merging (const + type + schema) to provide both dot-notation access and a Zod schema:// Domain enum with declaration merging (schema-first)
export const BoldVehicleType = {
BICYCLE: 'Bicycle',
TRUCK: 'Truck',
} as const;
export const BoldVehicleTypeSchema = z.enum(
Object.values(BoldVehicleType) as [string, ...string[]],
);
// eslint-disable-next-line no-redeclare
export type BoldVehicleType = z.infer<typeof BoldVehicleTypeSchema>;
// Usage: dot-notation in value position, type in type position
const vehicle = BoldVehicleType.TRUCK;
const isValid: BoldVehicleType = 'Bicycle';
// Narrower layer
const TruckOnlySchema = BoldVehicleTypeSchema.extract(['Truck']);
Do not use TypeScript enum — always use the const-object + z.enum() pattern above.
databases
Create and modify Zod schemas for runtime validation with proper type inference.
testing
Write Vitest unit tests following project conventions with proper stubs and assertions.
tools
Autonomously implement a task following project conventions with iterative verification.
testing
Analyze a pull request diff and provide structured feedback on correctness, conventions, and quality.