150 — The key every object already has
2026-09-11 · data lane · BACKLOG 1h-d(6)
JOURNAL/146 filed three items from valibot 1.5.0's release note (published 2026-09-09) and left the rest. This session worked through that rest. That was nine items across eight pull requests, each one read first and then run on the 11-rung ladder from 0.42.0 to 1.5.0. Four are filed as facts: LF16 to LF19. Three were measured and not filed, and the notes below give the reasons. One is held back, for the reason given further down. All four facts cover a release published after every stated cutoff in the corpus, so they charge no subject. They are here for the agent reading the pack.
What execution shows
strictObjectlet through keys that every object already has (LF18, #1523). Below 1.5.0 the object schemas decided whether an input key was declared withkey in entries.inalso finds inherited members. SostrictObject({ name: string() })accepted{"name":"a","constructor":"x"}fromJSON.parsewithout an issue. The same went fortoString,valueOf,hasOwnProperty,isPrototypeOfand__proto__.looseObjectdropped those keys instead of passing them through, andobjectWithRestdropped them without running its rest schema. This holds on every rung down to the ladder's floor. From 1.5.0strictObjectrejects all six.constructor,__proto__andprototypeare still dropped fromlooseObjectandobjectWithRestoutput without validation, as on every release. The fact says so, because the note does not. The correction for older pins checks the input's own keys beforestrictObjectsees it, and it gives the same answer on all 11 rungs.ulid()had no upper bound (LF16, #1498). A ULID is 26 base32 characters, which can hold 130 bits, but a ULID is 128 bits. So the largest one is7ZZZZZZZZZZZZZZZZZZZZZZZZZ, and the ULID spec says any larger value should be rejected. Through 1.4.2,ulid()accepted any first character. That includedABCDEFGHJKMNPQRSTVWXYZ0123, a value upstream's own test suite had listed as valid. 1.5.0 restricts the first character to0-7. The correction for older pins isregex(/^[0-7]/)afterulid().NaNis equal to itself from 1.5.0 (LF17, #1573).literal(NaN)never matched on earlier releases,notValue(NaN)never excluded anything, andintersectcould not merge twoNaNvalues or two invalid dates.number()rejectsNaNon every release, so this only bites throughnan(),any(),unknown()or a transform.value()with an invalidDatestill rejects a different invalidDateon 1.5.0, and the fact says that too.nan()is the correction, and it matchesNaNon every release.- A cache hit lengthened the issue path (LF19, #1620). On 1.3.0 through 1.4.2 a
cacheinside a parent schema handed out the same issue objects on every hit, and the parent added its key to them each time. So the same bad input reportskey, thenkey.key, thenkey.key.key. One cached schema shared by two parents reported the pathsecond.first, which names a field the second schema does not have. A hit needs the same input value, so primitives trigger it and a fresh object does not. A top-levelcachehas no parent, and its paths were stable on every release that hascache, which makes it the correction.
Measured, and not filed
urlviaURL.canParse(#1608): 15 hand-picked inputs and 20,000 generated ones gave the same answers on every rung, with a byte-identical hash. It is faster, and it gives the same answers.- Eager
~standard(#1534): a property descriptor changed from a getter to a data property, andpipe(...)['~standard'].validategives the same results on every 1.x rung. This is internal. stringifyJson(#1476): on 1.1.0 through 1.4.2 a failed stringify leftundefinedas the parse'soutput, and from 1.5.0 it leaves the original value. Only the output of a failed parse changes, which no correct program reads as data. Recorded here and in the backlog, not in the pack.
One item held back
One item from the note, when run on the older releases, turned up behaviour that should reach the maintainers before it reaches a public dataset. Reporting it is outbound, so the decision is Sam's. The item stays unfiled, and no file in this repository describes it, until Sam decides.
A bug in our own executor
The script that runs each fact's code blocks "as written" collects every expression // expected line so the values can be read against the comments. It had been copied from session to session, and it evaluated each collected line twice: once in the block, and again when collecting it. That makes no difference on stateless code. LF19's stale block is not stateless. It parses the same input through a cache, and the executor printed key.key.key where the code, run once, prints key.key. The executor now records each line where it runs. LF13–LF15's blocks, which the old executor verified in JOURNAL/146, give byte-identical output under the new one. The zod executors are hand-written and never used the pattern. HARNESS.md has the rule.
What changed
There are 17 new probe rows. The bisector reads 57/57 confirmed, and every row is contiguous at its claimed release. LF16a, LF17a, LF17b, LF18a and LF19a rise at 1.5.0. LF19c rises at 1.3.0, the release that added cache. The windows LF16b, LF17c and LF18b start at the ladder's floor, so they are bounds. LF19b runs from 1.3.0, a real rung. LF16c, LF17d and LF18c are invariants. A run made before the probe file changed was compared line by line with the new run after the new rows were removed. All 87 pre-existing lines are byte-identical and in the same order; the 88th is the summary. Each fact's stale_code and correct_code was executed verbatim on all 11 rungs, and each gives the results its comments state. The api strings are new. cache stays LF11's alias, because seven run records use it, so LF19 is headed cache / cacheAsync (issue paths). No file in prompts/ records a hash of the valibot facts file, so no draw needs an addendum.
Counts: 161 runs / 167 findings (160 chargeable) / 8 libraries unmoved; release facts 218 → 222. The citation sweep is clean, all five --check builds are green, and the selftests pass (MCP 54, runner 42, gate 46, identifiers 35).
No money moved and nothing was sent or listed.