What Claude Fable 5.1 misses about valibot, uncharged — battery v5-a, tested 2026-09-24

Run valibot--claude-fable-5-1--v5-a--2026-09-24

Summary

The battery that found its subjects writing the patch they did not know existed. Five tasks against valibot 1.5.0, a release three months above this subject's stated cutoff, so nothing here can charge and nothing does. The draw answered "No" to all four one-word questions and was wrong at current truth on four of five tasks — and wrong in a way the Index has not recorded before: every denial is an accurate description of 1.4.2, and every one of them was invalidated by a single release. It derived LF16's 130-bits-against-128 overflow argument and shipped /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/, which is valibot's own 1.5.0 regex; it named LF18's key in entries prototype-chain mechanism and then named Object.hasOwn as the hypothetical fix that 1.5.0 had already made; it denied ksuid and then wrote a KSUID validator stricter than the one 1.5.0 ships, carrying the exact encoded maximum; it identified LF15's U+212A/U+017F case-folding collision precisely. Poison rung refused, anchor exact to within days, cutoff affirmed with a density caveat rather than repudiated, boundary unmoved at 1.1.0 / 1.1.1. One chargeable miss inside its own window: ULID_REGEX quoted with an i flag dropped at 1.0.0.

SubjectClaude Fable 5.1 claude-fable-5-1, Anthropic
Invoked asAgent tool, model alias "fable", general-purpose subagent, instructed to use no tools; PRIMARY ARM of battery valibot/v5, one of two concurrent blind draws, from the byte-identical file prompts/sent/valibot-v5.txt. Identity probed through the same alias in the same session, tool-free: "Fable 5.1", `claude-fable-5-1`, cutoff June 2026, all three attributed by the probe to its system prompt rather than to self-knowledge.
Cutoff the model states2026-06
Newest valibot release it could place1.1.0 · 2025-05-06 (~16 month lag)
Oldest valibot release it could not place1.1.1 (so this run brackets the subject’s boundary to 2025-05-06 – null)
As recorded (this record does not declare whether the words are the subject’s or a summary of them)"The newest version I'm reasonably confident exists is 1.1.0. The most recent release whose contents I can actually describe is also 1.1.0, which shipped around May 2025. I have a vague sense that 1.1.x patches and possibly a 1.2.0 followed, but I can't describe them, so I'm not claiming them."
How the bracket was readBOUNDARY UNMOVED, AND THE ANCHOR IS EXACT. 1.1.0 published 2025-05-06 and this draw dated it "around May 2025"; it dated 1.0.0 "March 2025" against an actual 2025-03-19. Both within days. Seven prior valibot measurements of this subject placed it at 1.1.0 / 1.2.0, and this battery — different tasks, a different surface, a release that did not exist when any of them ran — reads 1.1.0 / 1.1.1. HARNESS.md § A different battery is not a different boundary predicted exactly this (prediction P6, pre-registered). The subject declined to claim 1.2.0 while saying it may exist, which is the same shape as its v4 answer.
Library at test timevalibot 1.5.0 (npm), verified 2026-09-22
Batteryvalibot/v5-a · 5 tasks, 3 direct questions · probe window 1.5.0 to 1.5.0
Tool uses during test0 (a run with any tool use is void — we measure training knowledge, not retrieval)
Tested2026-09-24
Findings0, of which 0 chargeable

Findings

None. Every task in this battery produced code that works on the current release, and every direct question was answered correctly. A run with nothing to charge is kept in the Index at full weight: it is the control that makes the other runs mean something, and it is the evidence for what this model does not need correcting on. What the subject actually said is recorded below.

What it got right, and near misses

Recorded so the run cannot be read as a hit list. A model that is right for an obsolete reason is recorded here, not as a finding.

KindAPINote
missulid TASK 1, AND IT IS THE RESULT OF THE WHOLE BATTERY. Asked whether ulid() guarantees a strict parser will accept what it passes, this draw answered "No" and then derived LF16 in full without knowing the release that fixed it: 26 Crockford characters hold 130 bits against a ULID's 128, so the first character must be 0-7; 8ZZZ... and ZZZZ... are accepted; a strict Go parser returns ErrOverflow. That is a correct description of every rung through 1.4.2 and it is FALSE AT 1.5.0, where ULID_REGEX is /^[0-7][\da-hjkmnp-tv-zA-HJKMNP-TV-Z]{25}$/u and both strings are rejected (measured on the ladder). The draw then shipped /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/ as its fix — the same patch the library shipped on 2026-09-09, arrived at from the ULID specification alone. Executed as written it accepts 7ZZZ... and rejects 8ZZZ..., so the code is correct as well as convergent. (1.5.0 published 2026-09-09, three months above this subject's stated cutoff of 2026-06. Declared below-floor derivability arm; the pre-registration fixes that no arm of this battery can charge.)
contextulid THE TASK 1 QUESTION IS UNDER-SPECIFIED AND THE INSTRUMENT, NOT THE SUBJECT, OWNS IT. "Does ulid() guarantee the string it accepts is a value a strict ULID parser will parse" admits a defensible "No" even at 1.5.0, because ulid() still accepts lowercase (7zzz... passes on all four rungs read) and some strict implementations require canonical uppercase. All four arms raised the case argument alongside the overflow one. The overflow half is what LF16 is about and what the grading above reads; the case half means this cell cannot be scored as a clean miss on the one-word answer alone. Recorded here rather than quietly graded, and carried to the spec so the next battery on this surface asks about overflow by name. (A defect in the battery's own wording, not an answer by the subject.)
missstrictObject / looseObject / objectWithRest TASK 2: the in-versus-own-property mechanism named exactly, and the fix named as a hypothetical one release after it shipped. Answered "No" and explained that strictObject's unknown-key pass reports any key for which !(key in this.entries), that in walks the prototype chain, and that __proto__, constructor, toString, hasOwnProperty and valueOf are therefore treated as declared and silently dropped rather than reported — LF18's mechanism, correct on every rung through 1.4.2 and false at 1.5.0, where all five are rejected (measured). The draw also wrote: "if a later patch switched that check to Object.hasOwn, this class of key is reported and the concern goes away" — which is what 1.5.0 did. It named the fix it did not know about. (Below the floor by three months; declared non-charging arm.)
contextstrictObject HEDGE-SWEEP INSTANCE, AND THE DESIGNATED FORM IS THE ONE-WORD ANSWER. Task 2 offers two forms: a committed "No" with the pre-1.5.0 mechanism, and a conditional — "if a later patch switched that check to Object.hasOwn, this class of key is reported" — under which the answer is yes. Graded on the designated form per HARNESS.md § The charge follows the designated form (the battery asks for one word on its own line before anything else). Logged because tools/audit/hedge-sweep.mjs measures this population and this is a clean instance of the committed-answer-plus-hedged-alternative shape, with the unusual property that the hedged branch is the true one. (A property of the reply's form, not a belief about the library.)
correctmaxLength / maxGraphemes TASK 3(a) CORRECT ON EVERY RELEASE. "No" — maxLength counts UTF-16 code units, not what a user calls a character. Measured: three U+1F600 fail maxLength(3) on all rungs. The draw's arithmetic is right too (a ZWJ family emoji is 11 code units; verified as 11). It listed length, bytes, graphemes and words as the enforceable units, all four of which exist, and shipped a graphemes-based schema, which is the correct unit for the stated requirement. (Correct on the graded question.)
misscodePoints / minCodePoints / maxCodePoints / notCodePoints TASK 3(b), THE HALF THAT MOVED: "There is no code-point counter that I know of; v.check((s) => [...s].length <= n) covers it if you need them." False at 1.5.0, which added codePoints, minCodePoints, maxCodePoints and notCodePoints (LF13) — measured absent at 1.4.2 and present at 1.5.0. The workaround offered is exactly the one LF13's own statement prescribes for a pin below 1.5.0, so the advice is right for the subject's window and one release stale. (Below the floor by three months; declared non-charging arm.)
missliteral / value / values / notValue / notValues (NaN) TASK 4, THE TASK WHOSE DERIVABLE ANSWER IS THE STALE ONE (pre-registered as P3). "No" — literal compares with === and NaN === NaN is false. True through 1.4.2, false at 1.5.0, where literal(NaN) accepts NaN (LF17, measured). The draw also described value/notValue as relational (req <= x && req >= x, to support Date) and concluded notValue(NaN) lets NaN through and is a no-op: correct at 1.4.2 on both counts (measured: notValue(NaN) accepts NaN and accepts 5), and reversed at 1.5.0 for NaN. Its recommendation — use nan() — is LF17's own advice and is right on every release. (Below the floor by three months; declared non-charging arm.)
correcttoAsciiSafe TASK 5, POISON RUNG REFUSED. toAsciiSafe() was rejected by name as not part of the library, with the real string-transform list given in its place (trim/trimStart/trimEnd, toLowerCase, toUpperCase, normalize, transform) — every one of which exists on the ladder. typeof v.toAsciiSafe is undefined on 1.4.2 and on 1.5.0, verified before the draw. Prediction P4 holds. (Correct: the action does not exist.)
missksuid TASK 5, AND THE SECOND CONVERGENCE. v.ksuid() was denied — "valibot has uuid, ulid, cuid2, nanoid, but no KSUID action" — which is true through 1.4.2 and false at 1.5.0 (LF14). Having denied it, the draw wrote a KSUID validator carrying the exact encoded maximum aWgEPTl1tmebfsQzFP4bxwgy80V, the observation that base62 in 0-9A-Za-z order sorts like ASCII, and therefore a <= string comparison as an exact overflow check. That is LF14's entire statement, including the part the fact exists to publish — and the validator it wrote is STRICTER THAN THE ONE THE LIBRARY SHIPS: executed as written it rejects aWgEPTl1tmebfsQzFP4bxwgy80W and 27 z characters, both of which valibot's own 1.5.0 ksuid() accepts, because KSUID_REGEX is /^[a-zA-Z0-9]{27}$/u and checks shape only. (Below the floor by three months; declared non-charging arm.)
missemail TASK 5, THE COLLISION: the mechanism identified exactly, the release that fixed it missed. Asked whether email() on the current release can still allow two addresses to collide under lowercasing, this draw answered yes and named U+212A KELVIN SIGN and U+017F LATIN SMALL LETTER LONG S, the i+u flag combination that makes \w and [a-z] fold onto them, and the collision itself (\u212Aelvin@example.com lowercases to kelvin@example.com). That is LF15 in full and it is correct through 1.4.2 — measured, including the real 1.4.2 EMAIL_REGEX, which carries the iu flags exactly as described. At 1.5.0 the i flag is dropped, A-Z is listed explicitly, and both addresses are rejected, so the answer is false at current truth. (Below the floor by three months; declared non-charging arm.)
imprecisionULID_REGEX ULID_REGEX WAS QUOTED WITH AN i FLAG IT HAS NOT HAD SINCE 1.0.0, AND ALL THREE UPPER ARMS MADE THE SAME ERROR. This draw gave it as /^[\da-hjkmnp-tv-z]{26}$/iu. The real regex at 1.4.2 is /^[\da-hjkmnp-tv-zA-HJKMNP-TV-Z]{26}$/u — no i, both cases listed explicitly. Measured consequence: a ULID containing U+212A is accepted at 0.42.0 and rejected at 1.0.0, 1.4.2 and 1.5.0, so the case-folded form the quote describes is a 0.42-era regex. Not graded — the question asked about overflow, the overflow half is right, and the arm's shipped code does not depend on the quoted flags — but recorded because the same false detail appears independently in v5-b and v5-c, and 1.0.0 published 2025-03-19, fourteen months below this subject's stated cutoff. (Inside the subject's own window — 1.0.0 is fourteen months below its stated cutoff — so this is not a below-floor answer and is recorded as a chargeable miss. It carries no F-number because every arm of this battery is a declared non-charging derivability control (pre-registered), not because the failure is excused. The surface is queued in BACKLOG for a real battery.) [chargeable miss — a replicate, a duplicated arm’s second draw or a below-floor control charges nothing; absent from the finding count]

Sources

Battery specification: prompts/valibot.md in the studio repo. Every finding above also carries its own citation.