Luq
Project status
What has shipped and is measured, what to establish before expanding integrations, and why the earlier .luq roadmap was withdrawn rather than rescheduled.
Next priority: one complete adoption path
Focus on projects that already own their TypeScript types. Introduce validation at one boundary, connect it to a form through Standard Schema, and verify that changing the upstream type exposes incompatible rules. The repository's form example is a starting point; it is not evidence of production adoption.
- Keep measured claims and known limits consistent across the package and documentation.
- Evaluate one existing project through setup, invalid input, type changes, and rule maintenance.
- Use that evidence to decide which integration to finish. Code generation remains checkout-only. Before publication, unsupported constraints must be handled explicitly and generated validators must be checked for semantic agreement on supported schemas.
These are acceptance criteria for future work, with no release date promised. Arbitrary-depth recursive JSON Schema validation and transform output inference throughuseField() remain limitations. For type-changing transforms, use direct.v() declarations to retain output inference.
What is in the box
78 plugins across 77 subpaths
Every rule you can call is a plugin you imported by name.
Browse the catalogue100.00% of the Draft-07 suite
929 of 929 required cases, with skipped cases counted as failures. A validator that always said yes would score 59.31%; 1.x scored 57.70%.
See the breakdownA 8,799 B gzipped floor
That is Builder with zero plugins. 1.x charged 17,423 B before you used anything.
See the measurementsCSP-safe, and checked
Zero occurrences of eval and new Function across all 874 emitted modules, verified by a script and by the public-API smoke test.
How it is checked89 export keys, every one resolving
A scratch consumer typechecks all 89 against the published declarations under both node16 and bundler resolution, and proves an unpublished subpath fails.
Get startedStandard Schema v1
toStandardSchema() on the @maroonedog/luq/standard-schema subpath, so anything that accepts a Standard Schema — tRPC, TanStack Form, Hono, t3-env — accepts a Luq validator. A subpath rather than part of build(), because carrying it on every validator costs 312 B gzipped.
How to use itPaths that name the real index
Declare items[*].productId and the issue you get back says items[1].productId.
The path grammarWhat this page used to list as missing
Nine rows, and all nine are closed — the Draft-07 corpus now passes 929 of 929.
| What was missing | Suite cases | What closed it |
|---|---|---|
| External and remote $ref | 57 | Resolved against a map of documents the caller already has. Luq does not fetch: a URI written in a schema can never make the process open a socket, and conversion stays synchronous. |
| __proto__ in a declared path | 14 | Declarable, and prototype pollution still unreachable — the writer uses Object.defineProperty, so __proto__ is a key rather than a setter. Conformance and safety turned out not to be a trade. |
| JSON pointer escaping | 9 | The whole fragment is percent-decoded before it is split, which is the order RFC 6901 §6 specifies. |
| Tuple items with additionalItems | 6 | A branch no longer re-follows the $ref that created it, so a $ref inside a tuple position produces rules instead of silently producing none. |
| null reaching a rule | 5 | Inside a sub-schema, null is a VALUE rather than an absence presence settles first. false, {"not":{}} and an enum without null all forbid null while saying nothing about type. |
| additionalProperties with patternProperties | 3 | The plugin takes the patterns as well as the declared keys, so it no longer over-rejects a key a pattern already matched. |
| Base URIs from $id, and anchors | 3 | A $ref is a URI reference, so resolution carries the base in force and an index of what a URI can name — not just the root document. |
| Code-point string length | 2 | minLength / maxLength count code points, not UTF-16 code units. |
| Second-hop $ref, and boolean sub-schemas | 2 | The same one-line fix as tuple items: the recursion guard was stopping itself. |
Where this release is weak
Slower than 1.x on flat and nested shapes
Measured in one process: ×0.21 on one field, ×0.79 on three fields, ×0.83 nested. Arrays (×4.3) and JSON Schema (×2.1) are the two wins. Recovering that ground is the largest single piece of unfinished engineering in the project.
See the numbersA recursive $ref is unrolled three times, and then stops
A mutually recursive schema has infinitely many declared paths, so the expansion is bounded. Unrolling further is exponential in the number of definitions in the cycle: a three-way cycle builds in 17 ms at one unroll, 55 ms at two, 538 ms at three and 9.3 s at four. Three is what the official corpus needs. Past that depth a document is not checked — the same silence as before, moved further out rather than removed.
See the numbersThe performance gate has a resolution
CI gates on the ratio between Luq and a hand-written validator: it catches a 35% regression and misses a 15% one.
See the numbersThe “universal platform” goal, withdrawn
1.x advertised a .luq DSL that would generate validators for other languages: database and cache adapters, endpoint definitions, a luq generate --lang=java command, OpenAPI emission. This page carried it with dated milestones. Those dates have since passed, and none of it shipped.
No part of it ships in this package and this release makes no claim about when it will. What is in the box is the TypeScript validation library described above.
The milestones are gone rather than moved. When something is built, it appears in the list above with a measurement beside it.