Repository navigation
Releases: C9up/rune
Release list
v0.2.1
release: rune 0.2.1
Require Node 24, and build the crates for production
Node 24, not because it is the current LTS — that is AdonisJS v7's own
stated reason and it is not one for us, since 22 still receives security
fixes until 2027, npm 11 is irrelevant under pnpm, and node:sqlite is
not what atlas uses. The reason is measurable and it is the framework's:
AsyncLocalStorage is on the request hot path, and Node 24 backs it with
AsyncContextFrame by default — 0.61 us per request instead of 1.55 us on
that exact pattern. Before 24 the same mechanism sat behind an
experimental flag, and a framework cannot base its performance on a flag
the application has to remember to pass. The reason travels with the
constraint, in a "//engines" key beside it.
Where there are crates: the default release profile leaves lto = false
and codegen-units = 16, so nothing inlines across crate boundaries —
and here the hot loop and the N-API binding that calls it are always two
different crates. Measured on atom, a scalar call through the binding
went from 18.85 ms to 13.59 ms for 50 000 operations. No panic = "abort": napi-rs catches panics and turns them into JavaScript
exceptions.
CI moves to Node 24 with them, since that is what the packages now ask
for.
Read the artifact where cargo actually wrote it
The NAPI copy script looked under <package>/target unconditionally. Cargo
writes elsewhere whenever CARGO_TARGET_DIR is set — a shared cache, a CI mount,
a read-only external directory — so the build produced the library and then
failed to find it, or silently copied a stale one from a previous run.
CARGO_TARGET_DIR is honoured now, resolved against the package root when it
is relative, as cargo resolves it. All fourteen scripts had the same line; an
audit reported it in ream-mcp alone.
Verified end to end, not by reading: a real cargo build redirected to a
temporary directory, the artifact copied out of it, and the package suite green
on that binary.
Keep the dev-dependency alignment, drop the workspace: protocol
The internal ranges had been rewritten to workspace:^. That resolves inside
this monorepo and nowhere else: every package CI checks out its own repository
alone and runs pnpm install, where the protocol has no workspace to point at
and fails with ERR_PNPM_WORKSPACE_PKG_NOT_FOUND before a single test runs. The
concrete ranges are back; the dev-dependency bumps that came with the same edit
are kept, and now match what the lockfile already resolved.
Changes since v0.2.0.
v0.2.0
Let a namespaced rule reach a translator, and hand it the rule's arguments
Namespacing a rule (array.minLength, date.after) took it out of
STANDARD_RULES, which is what gated the translator lookup — so every one of
them silently stopped being translatable the moment it was namespaced. The
gate now asks the catalogue instead, which is the actual definition of "a rule
this package ships".
The arguments were worse. Only min and max, and only for four hard-coded
rule names, were ever passed to the translator. A translator that validates its
variables — rosetta's does — does not degrade on a placeholder it was not
given, it THROWS: a translated array.minLength blew up in the middle of
validating. Every argument the rule carries goes through now, and {{ field }}
resolves to the same last path segment the default templates use, instead of
the full dotted path on one route and the segment on the other.
Found by executing the documented i18n snippet rather than reading it. Three
mutations, each felling its own test.
Close the coverage gate, and the three format bugs chasing it exposed
The publication gate was failing on three of its four thresholds (statements
90 < 91, functions 91.32 < 94, lines 92.38 < 94). The deficit sat almost
entirely in code added without tests of its own: the ported helpers surface
and the normalize-url transcription.
Covering them was not bookkeeping. Running each uncovered helper against the
reference implementation on the same inputs turned up three real defects, and
all three were in the RULES, not just the helpers:
ascii()accepted an empty string. A scan over "" is vacuously true, so a
field with nothing in it passed a format check.url()refusedftp://whilehelpers.isURL()accepted it — the same
string got two verdicts depending on which half of the package was asked.
The allow-list still refusesjavascript:,data:andmailto:, which is
what it is for.coordinates()refused(12.34, 56.78), the shape a map widget hands back.
Two answers that differ from the reference are kept and now named in tests:
an encoded reserved character survives the query sort (decoding %2F to /
changes what a URL means), and malformed percent-encoding is made well-formed
rather than passed through broken.
534 tests. Coverage now 92.53 / 83.96 / 95.57 / 95.01, all four above their
thresholds. Five more mutations, each felling its own test — one of them fell
nothing at first, which is how the url() RULE turned out to be reachable
only through a path no test took.
Give every default message one catalogue, keyed by what the rule reports
A default message was written inline at each rule, already formatted, and a
second copy of the same text lived in the Rust engine. Three consequences,
each of them user-visible:
- the text never named its field ("Must be a string"), which is unreadable
once six fields render their errors in one list; - a rule shared between types reported the bare name, so
minLengthon an
array both said "characters" and made a message written against
array.minLengthunreachable — the rule name IS the provider's lookup key; - the message depended on which engine ran, because a simple schema goes to
Rust and Rust had its own strings.
The catalogue in src/defaults.ts now owns the text and is the sole source:
the TypeScript path interpolates it, and the native path is re-rendered from
it so the engine's own copy can never surface. Rules shared between types are
prefixed by the type that owns them (array.minLength, record.maxLength,
date.after, nativeFile.minSize); rules that are not shared are left alone.
Exposed as @c9up/rune/defaults so a caller can read the keys they write
against.
Fixes a bug none of this set out to find: field.report() pushed its text
verbatim, so a messages provider reached a value rule but NOT a .use() one.
Every cross-field rule — sameAs, notSameAs, confirmed, the date comparisons —
and every rule built with createRule was permanently English, untranslatable
and unoverridable, while a value rule on the same chain honoured the provider.
Reports now resolve through provider, then translator, then the catalogue. An
explicit .message() still wins over all of it.
distinct carried its compared property as an arg named field, which
overwrote the {{ field }} token and made the message name the property
instead of the failing field; it is keyed fields now.
Also drops the reference implementation's name from the package: comments,
doc-comments, test labels and six test filenames now say "upstream". Parity is
shape, never product identity. Every occurrence was a comment or a test label —
no identifier and no runtime string — and the third-party licence file keeps
both reproduced MIT notices untouched, since neither belongs to that project.
Three test labels left in French were translated on the way past.
Verified against the reference implementation run side by side: 41 message
divergences down to 3, all three deliberate and named. 515 tests, 14 Rust,
typecheck, lint. Eight mutations, each felling its own test.
Port the whole helpers surface and align the coercions with upstream
rune.helpers carried 10 of the 39 predicates a custom rule is meant to reuse,
and four of the ten answered differently from upstream. Verified by running
@vinejs/vine 4.4.0 side by side: 63 of 64 behavioural checks now agree, the
last being rune's own wider country tables.
- 29 helpers added. Most are re-exports of predicates rune already had, under
the names a rule copied from upstream expects. New primitives: isSlug,
isDecimal and per-version isUUID, transcribed from validator.js 13.15.35. - getNestedValue reads the PARENT for a bare name. Reading
datareturned
undefined for every rule inside an array item or a nested object. - isDistinct compares by identity, and counts a null-valued key as a value.
Two rows that both left a field empty passed as distinct. - The boolean lists are exact: no trim, no case folding. "TRUE", " true ",
"off" and "yes" were all being swallowed. The Rust engine had its own copy of
the lax table, so the same schema decided differently depending on whether the
native binary loaded — both sides now read one list. - accepted() is case-sensitive, so "YES" no longer passes a consent checkbox.
- enum() takes a native TypeScript enum. It compiles to an object, and
iterating it as an array threw a TypeError. - withMetaData() exposes compile() beside create(), and the wrapper copies
property descriptors instead of spreading — a spread flattened errorReporter
and messagesProvider into dead plain properties. - A validator captures the messages provider and error reporter installed when
it was built, so a later global swap cannot reach back into it.
Named deviations, each with its reason in the code: asDate instead of asDayJS
(no date library in the public contract), null instead of a throw for an
unknown country code, an empty string refused by number() rather than read as
0, accepted() returning the true its own type promises, the Standard Schema
issue path staying an ARRAY as that spec requires, and the provider capture
yielding to a later global when none existed at build time — which is the order
a service provider installs the i18n one in.
Name the deviation behind rune's /testing subpath
VineJS ships the same capability as /factories. rune keeps the capability
and drops the name, because every package here puts its test surface on
/testing — a lone /factories would make rune the exception. The mapping is
now written down next to the helpers, along with why there is no VineString /
VineNumber equivalent to export: rune has no per-type class, and those names
are upstream's product identity.
Honour the async spelling Vine documents, and scope the messages provider
VineJS documents createRule(fn, { async: true }) but its implementation reads
only isAsync, so a rule written from the documentation is built synchronous
and its Promise is dropped: the payload validates while the rule is still
refusing it. Verified against @vinejs/vine 4.4.0, which returns the value
unchanged. rune now honours both spellings.
A validator can carry its own messages provider, as VineJS does with
validator.messagesProvider. It sits between the per-call provider and the
process-wide one, on both the sync and the async path.
~standard.jsonSchema.input() now reads the Standard JSON Schema target and
refuses a dialect rune does not emit. openapi-3.0 spells nullability
nullable: true rather than a type array, and tuples use prefixItems,
which is draft-2020-12 only — a silent best-effort would describe the validator
wrongly.
The /testing helper dropped the third argument of field.report(), so a rule
blaming another field was reported against the current one — the helper
disagreed with the runtime it stands in for.
Await an async rule that never said it was one, and hand the provider a context
An async validator passed to createRule() without { isAsync: true } was
run as a synchronous rule: its Promise was dropped, the run answered
valid: true, and the rule reported its refusal afterwards, into nothing.
A silent bypass of whatever that rule was guarding. It is detected now, the
way it is upstream — the option stays isAsync, and an async function is
routed whether or not it was passed. A validator that merely RETURNS a
thenable cannot be detected before it runs, so the sync path refuses it
loudly rather than reporting a pass nobody checked.
The messages provider was handed the field PATH where the contract says
FieldContext, so a provider reading getFieldPath(), name or wildCardPath got
undefined three times over. Rosetta had already worked around it by
synthesising a context from the string — which is why its wildcard message
keys never matched an array item.
The field-label lookup was one step where it is three: a...
v0.1.15
Run cargo with --locked in CI
Without it, cargo rewrites Cargo.lock in place when it has drifted from the
manifests — so CI resolves dependencies fresh and tests a graph nobody
committed, then the release is built from it. The workspace lock had drifted
by 382 lines before the same flag caught it locally.
Every package here commits a Cargo.lock, so --locked is meaningful: it fails
loudly instead of silently updating. Verified against the current lock before
the flag went in.
The Node side is deliberately left alone: these repositories ship no
pnpm-lock.yaml, so --frozen-lockfile has nothing to freeze against, and
resolving from the registry is what a consumer gets anyway.
Regenerate the vendored loader after the canon gained supportedTargets
The list of built targets belongs with the table: a package that names what
it supports in an error must read it from the same place, or the message and
the resolution can disagree.
Take the NAPI platform table from the vendored copy
Every package with a Rust engine repeated the same map from Node's
platform/arch to the suffix napi-rs builds under. Adding a target means adding
it everywhere, and a package that missed the edit fails only on that platform
— the failure nobody reproduces.
The table, the path and the require are vendored from
scripts/vendor/nativeBinary.ts. The policy is not: some packages degrade when
the binary is absent and answer undefined, others refuse loudly with build
instructions, and both are right for their package. The loader reports what
happened and the caller decides.
Measure coverage once, not once per platform
Lint this package the way its own repository will
biome's configuration lived only at the workspace root. This package is
built from its own repository, where that file does not exist and biome
falls back to its defaults — so lint in CI has been checking a different
set of rules from lint here, and the bans this project actually cares
about were never enforced where it counts.
The config is now the package's own, and says the same thing the root one
did.
Declare what CI has to install
Each package is its own repository: pnpm install there sees only this
file, so a dependency the workspace happened to hoist locally is simply
absent in CI. --coverage needs @vitest/coverage-v8 named here, and an
optional peer a test imports has to be a devDependency as well — optional
is exactly what keeps it from being installed.
Run the gates the package already declared
Three guard-rails were configured and never reached CI, so each one was a
gate nothing ran:
tsconfig.jsonincludestests, but CI typechecked only
tsconfig.build.json— every type a test relied on went unchecked.vitest.config.tsdeclares coverage thresholds, but CI ran plain
vitest run, which does not read them.lintpointed atsrc/alone, so no test file was ever linted.
CI now runs pnpm typecheck, pnpm test:coverage and a lint that covers
tests/ as well.
Changes since v0.1.14.
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.
v0.1.13
Format the Rust crates, and gate it so they stay formatted
Twelve of the thirteen crates had drifted — 949 differences in all, atlas
alone 345, and build.rs files that had never been through the formatter.
None of their workflows checked, so nothing ever said so; the drift only
surfaced when it took a publish job down.
cargo fmt applied throughout, and a cargo fmt --check step added to each
workflow so this cannot happen again. Formatting only: the Rust tests pass
unchanged in every crate.
One spot in atlas needed a real edit rather than the formatter: cargo fmt
rewrote a return Err(format!(…)) arm back to the inline form on every run
while --check kept asking for the block form, so the file could never
converge. The message is bound to a name, which fits the line budget and
settles it.
Release 0.1.13
Namespace every error code as E__
The convention was announced and not kept: 115 framework codes across ten
packages carried no E_ prefix, so an application filtering on the documented
rule handled some failures and missed others.
Blanket-prefixing them was the wrong fix, and trying it proved why — it made
E_FORBIDDEN mean three different things across ream, relay and warden, which
is worse than the inconsistency it replaced. The namespace after E_ is what
tells them apart.
Where a package already prefixed inside its error constructor — atlas, rune,
warden, and ream's module classes — the constructor now emits E__ and the
call sites stay bare, so the rule lives in one place per package instead of at
every throw. Each of those constructors passes through a code that already
starts with E_, which is how the upstream identifiers keep their exact
spelling: E_UNAUTHORIZED_ACCESS, E_INVALID_CREDENTIALS and E_VALIDATION_ERROR
are the ones a consumer branches on, and two packages naming the same upstream
failure legitimately share one.
Ignore a relocated cargo target
target/ matches a directory only. When the build output is moved elsewhere
and a symlink named target is left in its place, that pattern does not catch
it — it shows up untracked, and a stray git add -A commits a path that only
resolves on one machine.
Changes since v0.1.12.
v0.1.12
Complete escape(), carry the regex source, and stop the pattern drifting
escape() covered five characters where the reference covers eight: a slash,
a backslash and a backtick survived, and each of them breaks out of a
context a quote-only escape leaves standing.
regex() never recorded its source, so the branch that emits a pattern into
the JSON Schema was unreachable and the exported schema quietly accepted
anything. It also called test() directly, which advances lastIndex on a
global or sticky expression — the same value alternated between valid and
invalid depending on how many times it had been asked.
The identifier formats are checked against published real-world numbers, so
the checksums are measured against the outside world rather than themselves.
Let a schema refuse a key it does not declare
A consumer reported that unknown fields are dropped silently where their
contract requires the call to fail. Checked against upstream by running it:
vine.object({ email }) given { email, extra } answers valid and returns
{ email }. Its own type declaration claims the opposite — "objects with
properties not defined in the schema will fail validation" — and is wrong.
So the behaviour is right and stays the default: dropping is what makes a
validated payload safe to hand to a mass assignment.
What was missing is the third option. Upstream offers keep-or-drop and nothing
else, and an API whose contract is to refuse what it does not understand had to
guard the keys itself before delegating the values. denyUnknownProperties()
is that, named as the addition it is:
unknown field emial, expected one of email, password
Every undeclared key is reported, not just the first, and the declared ones are
still validated alongside — a typo is the common case, and the alternatives are
part of the answer.
The same report noted uuid() and the drop behaviour were undocumented. Both
are now, along with url, regex and in, which were missing from the string
rules for the same reason.
Changes since v0.1.11.
v0.1.11
Release the work that landed after the last published version
The registry now carries the version this package.json was still on, so
everything committed since ships under the next one rather than
retroactively changing what a published version means.
Build the cross-platform matrix only when the version moves
The five-runner matrix exists to produce the prebuilt binaries a release
ships. It ran on every push to main, rebuilding artefacts nobody
downloads — five runners, every time, for a comment fix.
A version-gate job now compares the package version at HEAD^ with
the one at HEAD and the matrix runs only when they differ. Anything
that is not a push passes the gate unconditionally, so workflow_dispatch
— how a release is actually cut — is unaffected, and so is publish,
which still waits on the full matrix. A missing HEAD^ reads as a bump:
erring towards building is the safe direction.
The test signal deliberately does NOT move with it. quality and the
cargo/integration jobs were already independent of the matrix, but
vitest ran INSIDE it, so gating the matrix alone would have quietly
taken the TypeScript suite off every ordinary push. A ts-tests job now
runs it on ubuntu, building its own napi binary rather than waiting on a
gated artefact. On a push without a bump that leaves typecheck, lint,
cargo and vitest — one runner instead of five.
Derive the native TypeScript surface from the Rust
The hand-written interface describing the .node binary was a second
description of the same thing, and nothing on this side noticed when the
first one changed: a pub fn could gain a parameter, stop being async
or change its return with the declaration still claiming otherwise.
napi-derive can emit the declarations itself. Its type-def feature
writes one JSON line per #[napi] item while cargo compiles;
scripts/build-napi-types.mjs collects them and
scripts/generate-napi-types.mjs turns them into
src/native/generated.ts. build:napi regenerates it, and the
TypeScript side consumes it instead of restating it.
Three things the generation had to handle:
type-defAPPENDS to its output file, so a parallel cargo build
interleaves the writes and definitions go missing, silently, leaving
the generated file short. Crates are built one at a time, and a crate
that emits nothing fails the script rather than producing a partial
surface.- A Rust doc example holding a cron expression (
0 */5 * * *) closes
the generated comment early. Every*/is escaped except the one
that legitimately closes the block — escaping that one breaks the
file just as thoroughly. - The driver is Node rather than bash, because
build:napialso runs
on the Windows prebuild runner: a Git Bashmktemppath is not
something the native proc-macro can write to, and the type-def file
would come back empty with nothing to explain why.
The generated file is a .ts holding only ambient declarations rather
than a .d.ts, so tsc carries it into dist/native/ and the
reference from the emitted declarations still resolves for consumers.
Changes since v0.1.10.
v0.1.10
Remove the double casts, by fixing what they were hiding
as unknown as T tells the compiler to stop checking. None of the ten in this
workspace turned out to be a real limit of the type system — each hid something
that could simply be described correctly:
- writing a property onto an object: Reflect.set does it without claiming the
object is something else - a synthetic root built only to satisfy a parameter that was never read; the
parameter is gone and the object with it - Dirent.parentPath, typed since Node 20.12 — the cast had outlived its reason
- a context interface that omitted url() and raw(), which the real request has;
declaring them made the cast unnecessary - a validator already assignable to the async form, void being assignable to
void | Promise - an AsyncCompiledRule passed off as a CompiledRule: different discriminants,
and the routing depends on which one it is. Overloads now say what each call
really returns - a hand-written connection type instead of the AsyncDatabaseConnection atlas
exports
The last one exposed a real defect: MigrationRunner declared
execute(): Promise<void> while every connection atlas hands out returns
{ rowsAffected }, so the runner rejected its own connections and callers cast
around it.
Create the GitHub release from the publish workflow
A published version arrived with no notes: npm showed a number, GitHub showed
nothing, and the only way to learn what changed was to read a diff. The commit
messages already carry the reasoning, so the release is built from the commits
the tag contains rather than written twice.
Skips a pure version bump, leaves an existing release alone, and does nothing
when the run was not built from a tag. The job takes contents:write for this;
the workflow default stays read.
Changes since v0.1.9.
rune v0.1.9
rune 0.1.9
rune: 0.1.9
016649d
Describe mobile() as it actually behaves
The doc-comments claimed rune carries no per-locale numbering plans and that
mobile() takes no locale. It carries 41 and has taken a locale since; an
unknown one raises UNSUPPORTED_LOCALE naming the ones it knows.
648a267
rune: fail loud without the Rust engine, plus the missing VineJS surface
- assertNativeAvailable() throws RuneNativeRequiredError on the path a schema
WOULD have run natively. A one-time warning used to let the TypeScript
validator take over, so the same code could reach different verdicts on two
machines — the code said so itself: "validation is platform-dependent". The
TypeScript path stays only for what the engine cannot do (custom rules, a
translator, a messages provider), where it is the only implementation - union().otherwise(cb): the name existed but aliased the
elseBRANCH FACTORY,
so the documented union([...]).otherwise(cb) was "not a function" - record().validateKeys(cb), enum(cb) computed per validation, and getChoices()
- RuleDef.validate receives the field context, built lazily. That also fixed
in()/notIn(), whose callback form took NO argument where VineJS passes the
field — a migrated in((field) => ...) read undefined - email({ allow_display_name }) parsed the display name with a pattern where
\s* and [^<>@]*? could both claim a space, so N spaces had N splits and the
engine tried them all: measured cubic, 8 KB of spaces blocking the event loop
for 67 seconds. Replaced by a linear walk, with the length bound applied to
the RAW input before any parsing
b59922d
Changes since v0.1.8.