Repository navigation
v5.0.0
The final-shape major. 5.0.0 closes the validation contract (leading-zero padding is rejected by the acceptance predicates), makes equals() answer the question its name asks (validity is now checked by default), and drops the v3-era dead weight. The Modulo 11 algorithm is unchanged throughout.
API stability commitment
This is the release after which the contract freezes. After three majors in about a year, no further breaking changes are planned: the validate() acceptance contract (three canonical shapes, no leading zeros, 64-char cap) is final, and the throwOnError: true defaults of the safe helpers will not change until a hypothetical v6 β which is not planned. Any new capability will arrive as an addition in a minor release, never as a mutation of existing behavior.
Why
A canonical Chilean RUT body never carries leading zeros β 12.345.678-5, never 012.345.678-5. Leading zeros only ever appear as a fixed-width storage artifact. Unlike the three accepted shapes (which differ only in separators over the same digits), padding forms an unbounded family of distinct strings for one RUT (12345678 = 012345678 = 0012345678 = β¦). Accepting them let a zero-padded variant slip past a strict uniqueness/identity gate β the exact class of bug the strict acceptance path exists to prevent. 4.x silently stripped the zeros and returned true; 5.0.0 refuses the input instead, and points you at clean() to normalize it explicitly.
Do I need to change anything?
For most projects, no. If you validate normally-formatted RUTs β compact (123456785), compact + hyphen (12345678-5) or canonical dotted (12.345.678-5) β and treat equals() as "same RUT?", nothing changes. You need to act only if:
- you feed zero-padded values (fixed-width exports from legacy/mainframe systems) directly into
validate()/isValidRut()/isRutLike(), - you relied on
equals()matching invalid RUT-shaped strings (dirty-data dedup), or - you still call the long-deprecated
getInvalidRutError(), or run Node < 20.
Upgrade notes
-
Ingest dirty/legacy input with the two-line recipe β normalize first, then validate the normalized value:
const rut = clean(raw, { throwOnError: false }) // '0012345674' β '12345674' | null if (rut !== null && validate(rut)) db.save(rut) // canonical, Modulo 11 verified
This is the blessed path for zero-padded fixed-width exports and other messy sources:
clean()collapses the unbounded zero-padded family into one canonical string, andvalidate()then proves the verifier. Never storeclean()'s output without thevalidate()step βcleandoes not check the Modulo 11 digit.Deliberate non-goal: no
allowLeadingZeros-style relaxation flag was (or will be) added tovalidate()β every flag that relaxes the canonical contract erodes exactly the guarantee it exists to give.validatestays a binary, flag-free (exceptstrict) identity gate; messy input gets normalized before the gate, never waved through it. -
Dedup of dirty datasets: pass
{ requireValid: false }toequalsto keep the 4.x behavior (same typo in two rows β still the same entity). -
getInvalidRutError()callers: catchInvalidRutErroror checkerr.code === 'INVALID_RUT'instead. -
Size the impact on a real dataset with the differential harness:
npm run test:differentialwritestests/differential-report.md, which now carries a dedicated 4.1.0 β 5.0.0 section: the onlyvalidateregressions are the zero-padded shapes, and the onlyequalsdivergences are wrong-DV pairs.
Added
EqualsOptionsexported type ({ requireValid?: boolean }) for the newequalsoption described below.
Changed (Breaking)
-
validate(),isValidRut()andisRutLike()reject leading-zero padding. Any input whose first (trimmed) character is0now returnsfalse:validate('012.345.678-5'),validate('0012345678')andisRutLike('00012345678')are allfalse(weretruein4.x). The guard lives in the sharedparseRutLikeparser, so the three predicates move together. It is a runtime check on the parsed input, not merely the removal of the0*prefix from the shape patterns: a single zero on a 7-digit body (01234567-4) is absorbed by\d{7,8}and would otherwise be stripped during normalization, so the patterns alone could not close the gap. -
equals()now checks validity by default (requireValid: true).equalsanswers "are these the same RUT?" β and two strings that are not RUTs cannot be the same RUT. By default both arguments must normalize to the same value and that value must pass Modulo 11:Call 4.1.0 5.0.0 Why equals('12345678-9', '12345678-9')truefalsewrong verifier β not a RUT equals('12.345.678-9', '123456789')truefalsesame wrong-verifier value across shapes equals('012345678-5', '12.345.678-5')truetruevalid after normalization (see below) equals('12.345.678-5', '123456785')truetruevalid, same RUT equals(a, b, { requireValid: false })n/a 4.1.0 behavior pure normalization comparison Coherence rule (intended asymmetry): validity is checked against the normalized value, not against
validate()'s canonical shape contract βequals('012345678-5', '12.345.678-5')staystrueeven thoughvalidate('012345678-5')isfalse.equalsis a normalization operation by definition;requireValidadds exactly one thing (the verifier check), because requiring canonical shape would makeequalsuseless for the very thing it exists for: comparing different shapes of the same RUT. This is not a bug.{ requireValid: false }preserves the 4.x comparison byte-for-byte β the legitimate use case is deduplicating dirty datasets, where the same typo in two rows is still the same entity. The differential harness proves the parity on a 50k-pair corpus. -
Node.js >= 20 required (
engines). Node 14β18 are past end-of-life, and 20 is the first floor that guaranteesglobalThis.crypto(a default global since Node 19) β it also matches the versions CI actually tests. TheMath.randomfallback ingenerate()is retained purely as a safety net for exotic embedded runtimes and its JSDoc now says so (generated RUTs are test fixtures, not secrets).
Removed
getInvalidRutError()(deprecated since 4.1.0). It was a v3-era shim that always returned the constant stringInvalid RUT input. Migration: catchInvalidRutError(err instanceof InvalidRutError) or branch onerr.code === 'INVALID_RUT'; the constant message isnew InvalidRutError().messageif you truly need the text.
Unchanged
- The lenient normalizers stay permissive.
clean(),format(),decompose(),getBody(),getVerifier(),mask()andcalculateVerifier()continue to strip leading zeros β they are normalization / recovery tools, not validation. The split is deliberate:validate*answers "is this written as a canonical RUT?";clean/formatanswer "recover a RUT from messy input"; the two compose into the ingestion recipe above.clean()'s docs now spell out that it strips every non-[0-9kK]character wherever it sits βclean('RUT: 12.345.678-5')β'123456785'β by design, for its paste-normalizer role. throwOnErrordefaults are untouched. The seven safe helpers (clean,format,decompose,getBody,getVerifier,calculateVerifier,mask) still throwInvalidRutErrorby default and still accept{ throwOnError: false }. Flipping that default was evaluated and rejected: TypeScript consumers would get self-discovering compile errors, but plain JavaScript consumers would get a silent behavior change (throwβnull) that propagates noiselessly downstream β the worst class of breaking change. These defaults will not change until a hypothetical v6.- Modulo 11, strict mode, the 64-char security cap, and the generic
Invalid RUT inputerror are all untouched.
Internal
- The internal parser was restructured into a single typed core. One typed splitter is now shared by a lenient core (used by
clean/decompose/getBody/getVerifier/mask/format) and a canonical core (used byvalidate/isValidRut/isRutLike).decompose's unreachable defensive re-narrowing branch β a long-standing surviving Stryker mutant β is gone structurally, not via ignore-comment. Same regex budget on thevalidatehot path; no perf regression. parseRutLikegains the leading-zero guard; the now-redundant0*prefix was dropped from thecompact/compactWithHyphen/dottedshape patterns to document the intent at the grammar level.- The differential harness now carries a frozen
v4.1.0baseline (verbatimvalidate+equalsfrom the v4.1.0 tag) alongside the existingv3.4.0one β real upgraders of this major come from 4.x. The regeneratedtests/differential-report.mdshows: 4.1.0 β 5.0.0validateregressions are exactly the zero-padded shapes (50 001 of 1 000 000 inputs; zero new acceptances),equalsdiverges only on wrong-verifier pairs, andequals(a, b, { requireValid: false })matches 4.1.0 on every one of the 50 004 comparison pairs. Same reproducible seed; therandom-digitsstratum still never emits leading-zero strings, so the catch-all only ever reports a real surprise. - New property-based laws (fast-check) pin the 5.0.0 API coherence: the ingestion recipe accepts every zero-padded rendering of a valid RUT (and never a wrong verifier, however padded); recipe acceptance coincides with default-
equalsreflexivity;equals(clean(a), a)for every valida;equalssymmetry in both modes. - Test suite updated accordingly (523 tests across 16 suites).