Update after learnings or mistakes — when a correction, new convention, or hard-won lesson emerges during development, append it to the relevant section of this file immediately. AGENTS.md is the source of truth for project conventions and should grow as the project does.
- Exact optional properties —
exactOptionalPropertyTypesis enabled in tsconfig. Optional properties must include| undefinedin their type if they can be assignedundefined(e.g.foo?: string | undefined, notfoo?: string). - No
readonly— skipreadonlyon type properties. typeoverinterface— always usetypefor type definitions..jsextensions — all imports include.jsfor ESM compatibility.- Classes for errors only — all other APIs use factory functions.
- No enums — use
as constobjects for fixed sets. constgeneric modifier — use to preserve literal types for full inference.- camelCase generics —
<const args extends z.ZodObject<any>>not<T>. - Options default
= {}— useoptions: Options = {}notoptions?: Options. - Minimal variable names — prefer short, obvious names. Use
optionsnotserveOptions,fnnotcallbackFunction, etc. Context makes meaning clear. - No redundant type annotations — if the return type of a function already covers it, don't annotate intermediate variables. Let the return type do the work (e.g.
const cli = { ... }notconst cli: ReturnType = { ... }). - Return directly — don't declare a variable just to return it. Use
return { ... }unless the variable is needed (e.g. self-reference for chaining). - Skip braces for single-statement blocks — omit
{}for single-statementif,for, etc. - Destructure when accessing multiple properties — prefer
const { a, b } = optionsover repeatedoptions.a,options.b. - IIFE for multi-branch assignment — use an IIFE instead of nested ternaries when assigning a value from multiple conditions. Add a comment to every branch explaining the case.
z.output<>overz.infer<>— usez.output<schema>for types after transforms/defaults are applied (whatschema.parse()returns at runtime). Usez.input<schema>only when representing pre-validation types.constgenerics on definitions — any function that accepts Zod schemas and passes them to callbacks must useconstgeneric parameters to preserve literal types (e.g.<const args extends z.ZodObject<any>>).- Flow schemas through generics — when a factory function accepts Zod schemas, use generics to flow
z.output<>through to callbacks (run,next), return types, and constraint types (alias). Never fall back toanyin callback signatures. - Type tests in
.test-d.ts— use vitest'sexpectTypeOfin colocated.test-d.tsfiles to assert generic inference works. Type tests are first-class — write them alongside implementation, not as an afterthought. - No
anyleakage — Zod schemas may usez.ZodObject<any>as a generic bound, but inferred types flowing to user-facing callbacks must be narrowed viaz.output<typeof schema>. The user should never seeanyin their IDE. - Type inference after every feature — after implementing any feature, check if new types can be narrowed. If a new property, callback, or return type touches a Zod schema, add generics to flow the inferred type through. Add or update
.test-d.tstype tests alongside.
- JSDoc on all exports — every exported function, type, and constant gets a JSDoc comment. Type properties get JSDoc too. Namespace types (e.g.
declare namespace create { type Options }) get JSDoc too. Doc-driven development: write the JSDoc before or alongside the implementation, not after.
- Snapshot tests for deterministic output — prefer
toMatchInlineSnapshot()for deterministic string outputs (TOON, JSON, etc.). If output is mostly deterministic with a few dynamic properties (e.g.duration), extract and assert those separately, then snapshot the rest.
- Conventional commits — use
feat:,fix:,refactor:,docs:,test:,chore:prefixes. Scope is optional (e.g.feat(parser): add array coercion).