Skip to content

Latest commit

 

History

History
101 lines (88 loc) · 5.3 KB

File metadata and controls

101 lines (88 loc) · 5.3 KB

AGENTS.md — orientation for coding agents

This file is for AI coding agents (and humans who like density) working on @imqueue/validation. It captures how the codebase is built, tested and structured, plus the invariants that are easy to get wrong. Read it before making changes. For contribution process/terms see CONTRIBUTING.md; for end-user docs see the README and https://imqueue.org/.

What this is

@imqueue/validation is the input-validation layer of the @imqueue framework: Zod-backed, field- and method-level validation exposed as native (TC39) decorators (@validate, @validatable, @validated, plus schemaOf). It is used by @imqueue/rpc services and by code generated by @imqueue/pg-prisma to validate RPC inputs.

Toolchain & invariants (do not fight these)

  • ESM only, "type": "module". Use import, not require(). Import sibling modules with the .js extension (NodeNext resolves it to the .ts source), e.g. import { validate } from './validate.js'.
  • TypeScript, module/moduleResolution: nodenext, target: es2024, verbatimModuleSyntax: true, isolatedModules: true, strict: true. Use import type / import { type X } for type-only imports.
  • Node ≥ 22.12.
  • Native TC39 decorators — the code relies on the standard decorator emit, not experimentalDecorators and not reflect-metadata. Do not enable experimentalDecorators; it changes decorator semantics and would break the buffer/seal mechanism.
  • Single runtime dependency: zod. Do not add heavyweight deps; this package is meant to stay small.
  • Lint/format: oxlint + oxfmt. Run npm run format before committing; CI checks npm run format:check.
  • Build emits .js/.d.ts/.js.map next to sources; these are gitignored, not committed (build runs clean-compiled first). Never commit compiled output.
  • removeComments is intentionally false — downstream tooling relies on doc-blocks surviving compilation. Keep it that way.

Commands

npm install
npm run build          # clean-compiled + tsc (emits alongside sources)
npm test               # build + node:test over every test/**/*.spec.js
npm run lint           # oxlint
npm run format         # oxfmt (write)  |  npm run format:check (verify)
npm run test-coverage  # tests + experimental coverage summary
npm run test-lcov      # writes coverage/lcov.info

Layout

Path Role
index.ts Public entry: export * from './src/index.js'
src/index.ts Barrel re-exporting the public API
src/validate.ts The whole implementation: @validate, @validatable, @validated, schemaOf, and the Validator type.
test/** node:test specs (*.spec.ts, run compiled).

Behavioural invariants

  • Field decorators buffer; the class decorator seals. TC39 metadata (Symbol.metadata) is not reliably populated by esbuild/tsx, so @validate writes into a module-level buffer and @validatable() moves that buffer into a WeakMap registry keyed by the class. This is safe only because field decorators run before the class decorator and class bodies evaluate sequentially — do not introduce async between them or reorder.
  • A class field decorated with @validate requires the class to be @validatable() for schemaOf to see it.
  • Validators resolve lazily — but that does not buy mutual references. A @validatable class used as a validator is resolved via schemaOf on first use, which defers the schema assembly. The reference itself is not deferred: the decorator argument is evaluated when the referencing class is defined, so a class named there must be declared above it. Both tsc (TS2449) and the runtime (ReferenceError: Cannot access 'X' before initialization) reject the other order, and two classes therefore cannot reference each other. An earlier version of this file claimed otherwise; verified 2026-08-01.
  • An unsealed class contaminates the next sealed one. Because @validate buffers into module state that @validatable claims, a class using @validate without @validatable leaves its fields for whichever class is sealed next — which then requires properties it never declared, while the class with the actual mistake validates nothing. Test files therefore need care about declaration order, and each spec file gets its own process (node --test), which is what keeps them isolated.
  • @validated validates without substituting. schema.parse()'s return value is discarded and the original argument is passed to the method, so transforming schemas (z.coerce.number(), .trim(), .default(...)) change nothing the body sees, and object schemas do not strip unknown properties.
  • Invalid input throws the raw ZodError. Do not wrap or swallow it. Note what a remote caller actually receives, though: @imqueue/rpc's IMQService converts anything a method throws into an IMQRPCError payload, so the client sees code: 'IMQ_RPC_CALL_ERROR' (a ZodError carries no code) with Zod's issue list as the message string. instanceof ZodError holds in-process only.

License

GPL-3.0. Commercial licensing for closed-source products: https://imqueue.com/.