Repository navigation
v0.1.14
Reformat what the strictness pass reflowed
Two files-worth of blank lines and one long call the formatter wraps
differently now that a helper sits above them. CI resolves biome from a
caret range and installs a newer one than the lockfile pins.
Turn on noUncheckedIndexedAccess
It was not missing here — it was explicitly false, in sixteen of the
seventeen tsconfigs. eon alone had it on, which is why nobody had seen
what it finds.
It stays a named deviation from upstream: @adonisjs/tsconfig sets
strictNullChecks and noImplicitAny but not this one. We keep it because
turning it on is what caught an as asserting a possibly-absent regex
group was a known value — the exact shape the flag exists to find. Doing
better than upstream is kept and written down, not reverted to parity.
Every site is restated rather than silenced: no !, no cast, no ?? 0
standing in for a branch that cannot happen. A reversed copy read by
value where an index walked a callback list backwards, the winner of a
scan kept as the value it found rather than its position, destructuring
where a length check was doing the proving, and an explicit break where a
loop condition already bounds the read.
Release 0.1.14
Coerce a number the same way on both engines
The native path parsed a numeric string with Rust's f64 parser, which is not
JavaScript's Number(): it does not read the 0x / 0b / 0o prefixes. So
"0x10" was refused natively and coerced to 16 by the TypeScript path, which
is the VineJS behaviour the coercion documents itself as mirroring. Same
schema, same input, two answers — decided by whether the application installed
a messages provider.
The engine's own comment already says both must agree "otherwise the same
schema behaves differently depending on whether the native binary happened to
load". Coercion now goes through a Number() equivalent, and the parity test
covers the value produced and not only the verdict, since coercion is what the
application actually receives.
Make the two engines say the same thing, and lock it down
A schema carrying nothing TypeScript-only is validated by the Rust engine; the
same schema takes the TypeScript traversal as soon as the application installs
a messages provider, a translator or a custom rule. Which one runs is therefore
an implementation detail of the surrounding app, and six rules did not agree
across it: minLength, maxLength, fixedLength, range, startsWith and endsWith
each named their bound on the TypeScript side and reported "Too short" / "Out
of range" / "Invalid prefix" on the Rust side. The default case — an app with
no i18n — was the one getting the message that does not say what is expected,
while the engine had the value in hand the whole time.
The parity test is the point. The engine's match ends in "unknown rule — skip",
so a rule routed to Rust without an arm there is not an error: it is silently
not enforced, and only the routing allow-list stands in the way. Every routed
rule is now checked to refuse the same value with the same words on both paths,
which is what the allow-list's own comment says kept going wrong.
Run async rules declared inside a conditional group
The sync entry points refuse a schema carrying async rules rather than skipping
them, and that refusal is the only thing between a unique()/exists()/
verifyContent() and a value nobody checked. It is driven by hasAsyncRulesDeep,
which walks nested objects, array items, record values, tuples and union
branches — but not conditional groups, whose branches are merged into the shape
at validation time exactly like the nested schema is.
So a rule in a group.if(...) was invisible: validateResult() saw nothing to
await, did not refuse, and the rule never ran. Verified both halves — the async
path was running the rule correctly all along, only the sync gate was blind, so
the payload came back valid with the check skipped.
Same class as the nested case this getter already documents having fixed.
Drop a proto key from validated output
Dropping keys the schema never declared is what makes a validated payload safe
to hand straight to a mass assignment. Two paths keep undeclared keys on
purpose — the allowUnknownProperties() opt-in and record(), whose keys are
data — and both let a __proto__ key through as an own property. Neither
touched rune's own object, but the consumer pays: Object.assign(model,
validated) writes through the inherited setter and replaces the target's
prototype, which is precisely the operation the payload is promised to be safe
for. Verified end to end before and after.
NAMED DEVIATION (hardening): VineJS keeps the key — its compiler copies
unknown properties with a for…in that assigns straight into the output, so the
output's own prototype is replaced. Parity would mean shipping the bug.
constructor and prototype stay: they are only reachable through a recursive
third-party merge, and they are plausible keys for a record() to carry.
Move the NAPI bindings to napi 3
The Rust needed no change; the toolchain did. napi-derive 3 writes one type-def
file per crate into NAPI_TYPE_DEF_TMP_FOLDER and panics outright when it sees
the old single-file TYPE_DEF_TMP_PATH — that variable is how it detects an
out-of-date toolchain, so the failure reads as "upgrade @napi-rs/cli" even
though the generator here is our own.
It also emits a function as a bare function name(...) where 2 emitted the
signature alone, so concatenating the name onto it produced
function validatefunction validate(...). The generator handles all three
shapes now.
napi-build stays at 2 — there is no 3 on crates.io.
The generated src/native/generated.ts comes out byte-identical to the napi 2
one, and the 388 JS tests pass against the rebuilt .node.
Update the Rust dependencies within their ranges
Everything the existing semver ranges allow, so no manifest changes and no API
surface moves. fmt, strict clippy, tests and advisories all pass.
Clear strict clippy and the advisory list
Nothing in the root gate ran clippy or cargo-deny against a package workspace —
cargo test --all covered the ROOT workspace only, which is a handful of
crates. Five packages were failing strict clippy at the same time and nothing
said so.
Turn on noUnusedLocals/noUnusedParameters
Print the test run, not just its JSON
The workflow ran vitest with --reporter=json alone, so a failing job wrote
its report to a file and nothing to the log: an exit code, and not one word
about which test failed. Twice today that meant finding a failure by deduction
instead of reading it.
The default reporter runs alongside; the JSON one still feeds the smoke gate.
Changes since v0.1.13.