What Claude Opus 5 gets wrong about valibot — battery v3-c, tested 2026-09-05
Run valibot--claude-opus-5--v3-c--2026-09-05 · self-test: the subject is the operator
Summary
The battery's only charging arm, and it charges the 1.3.0 half only. Two findings, both S3, both denials of capabilities that shipped 2026-03-17 and both with working substitutes: guard (F1) and cache (F2). Its 1.4.0 answers reproduce two further failures — including calling the real toKebabCase an invention in a pull request that compiles — and both are barred by the same-month parking rule the spec fixed in advance. Anchor correct, poison rung refused, boundary unmoved from v2 at 1.1.0 / 1.2.0, stated cutoff affirmed with a density caveat.
Subject
Claude Opus 5 claude-opus-5, Anthropic
Invoked as
Agent tool, model alias "opus"; prompt sent verbatim from prompts/sent/valibot-v3.txt. An identity probe run in this same session through the same alias answered "Claude Opus 5 (the 1M-context variant)", model id `claude-opus-5[1m]`, cutoff May 2026, all three from its system prompt. CHARGING arm of the Opus 5 pair, and — after both Claude Fable 5.1 arms declined their stated cutoff — the only arm in this battery licensed to charge anything.
Cutoff the model states
2026-05
Newest valibot release it could place
1.1.0 · 2025-05-06 (~12 month lag)
Oldest valibot release it could not place
1.2.0 · 2025-11-24 (so this run brackets the subject’s boundary to 2025-05-06 – 2025-11-24)
As recorded (this record does not declare whether the words are the subject’s or a summary of them)
"Both are 1.1.0 — I don't have a specific later version number I can name with any confidence. I'd assume valibot has shipped further releases since (1.2.x, patch releases, possibly more), but I'd be fabricating if I named one."
Library at test time
valibot 1.4.2 (npm), verified 2026-09-05
Battery
valibot/v3-c · 5 tasks, 3 direct questions · probe window 1.3.0 to 1.4.0
Tool uses during test
0 (a run with any tool use is void — we measure training knowledge, not retrieval)
F1 · Denies that valibot has any action that narrows a pipeline's output type from a TypeScript type predicate, three months after guard shipped to do exactly that
valibot 1.3.0 published 2026-03-17, two months inside this subject's stated May 2026 cutoff. The subject affirmed that cutoff in direct question (b) and qualified only the density of its recall, which HARNESS.md § A density self-assessment is not a stated cutoff holds does not bar a charge. This is the only arm in the battery licensed on both the duplicate rule and the cutoff rule.
What the model believes
"Valibot has no action that infers narrowing from a x is T predicate the way Zod's .refine overload does. v.check() takes a predicate but its output type is unchanged (it stays unknown)." Answered "No" to the verdict question before writing anything.
What it wrote
// The advice, as a developer would receive it:
// "There is no built-in for this. Use the `custom` schema and restate the type
// yourself — the narrowing comes from the explicit <PluginConfig>, not from the
// predicate's signature. `custom` is an assertion in disguise: if your predicate
// is wrong, TypeScript will not catch the mismatch."
import * as v from 'valibot'
const PluginConfigSchema = v.custom<PluginConfig>(isPluginConfig, 'Invalid plugin config')
// Compiles, narrows, and carries exactly the unchecked-cast risk the draw warns about.
// The risk is real and the warning is good advice — for a library that has no `guard`.
What works on valibot 1.4.2
import * as v from 'valibot'
// `guard` has existed since 1.3.0 and reads the predicate's own `x is T` clause,
// so the type argument is not restated and cannot disagree with the check.
// Executed against valibot@1.4.2 under tsc --strict:
const PluginConfig = v.pipe(v.unknown(), v.guard(isPluginConfig))
const cfg = v.parse(PluginConfig, raw)
// ^? PluginConfig — no cast, no restated generic
// For contrast, both verified the same way:
// v.pipe(v.unknown(), v.check(isPluginConfig)) -> parses to `unknown` (TS18046)
// v.custom<PluginConfig>(isPluginConfig) -> parses to PluginConfig, unchecked
Impact
A developer is told to restate the type by hand in a custom<T>() generic and is correctly warned that TypeScript cannot check the generic against the predicate — a warning that only applies because the built-in that removes the restatement was not offered. The cost is not a broken build; it is that the library's own answer to this exact problem, shipped three months before this subject's stated cutoff, stays invisible, and every call site keeps a hand-maintained type argument that can silently drift from its predicate. Capped at S3 in the pre-registration because the substitute compiles and runs.
Scope note
Charged on the absence claim only. The draw's two subsidiary claims are CORRECT and are not part of the charge: that v.check() does not narrow (verified — TS18046 under tsc --strict), and that v.custom<T>() narrows by fiat rather than from the predicate (verified — compiles clean). Its closing suggestion to describe the shape with v.object({ name: v.string(), run: v.function() }) is also sound, and v.function() is the current spelling, which it hedged unnecessarily.
Same window and same licence as F1: 1.3.0 (2026-03-17) precedes the affirmed May 2026 cutoff by two months.
What the model believes
"Valibot has no memoization or result cache. Schemas are plain objects with a ~run method; every parse/safeParse executes the full pipeline. (v.lazy() exists but it defers schema construction for recursive types — it is not a result cache.) There's also nothing at the parse level like a cache config option." Answered "No" to the verdict question first.
What it wrote
// "There's also nothing at the parse level like a cache config option."
// The developer is sent to build one:
function memoParse<S extends v.GenericSchema>(schema: S) {
const cache = new Map<unknown, v.InferOutput<S>>()
return (input: v.InferInput<S>): v.InferOutput<S> => {
if (cache.has(input)) return cache.get(input)!
const out = v.parse(schema, input)
cache.set(input, out)
return out
}
}
// This works. It is also a reimplementation of a shipped method, and the four
// caveats the draft then lists (identity keying, uncached failures, unbounded
// growth, shared transform output) are the caveats the shipped method documents.
What works on valibot 1.4.2
import * as v from 'valibot'
// Shipped since 1.3.0. Executed against valibot@1.4.2:
// const C = v.cache(v.pipe(v.string(), v.transform(s => { calls++; return s.toUpperCase() })))
// v.parse(C, 'a'); v.parse(C, 'a'); v.parse(C, 'b') -> calls === 2, not 3
const Cached = v.cache(ExpensiveSchema)
// It takes the config the draft says does not exist:
const Configured = v.cache(ExpensiveSchema, { /* CacheConfig */ })
// `cacheAsync` is the async counterpart.
Impact
The developer hand-rolls a memo table the library ships, and inherits the maintenance of its edge cases. The concrete cost is small — the hand-roll is correct — which is why this is S3 and was capped there before the draws were read. It is included because the denial is specific enough to be checked: the draft rules out both the wrapper and a parse-level config option, and both exist.
Scope note
Disclosed in the pre-registration and repeated here: cache is annotated @beta in the shipped dist/index.d.cts, which is why the severity was capped at S3 in advance rather than argued down afterwards. The draft's remarks about v.lazy() are correct and are not charged. Its own caveat list is, verbatim, close to the hint the shipped declaration carries — "Primitive inputs are cached by value. Object and function inputs are cached by reference identity" — which is worth noting: it reasoned its way to the API's documented behaviour while denying the API.
Task 2, the case-conversion probe, offer direction. "No", and the denial enumerates the surface: "There is no case-convention converter — no toKebabCase, toCamelCase, toSnakeCase, toTitleCase." Three of those four shipped at 1.4.0 (2026-05-05); the fourth has never existed. The slugify + v.transform schema it shipped instead is correct and better designed than the task required. (1.4.0 published 2026-05-05, the same month as this subject's stated cutoff. The Index parks same-month releases rather than guessing at a day, and the spec's admissibility table said so before the draws. Counted in the method page's undercount total.)[chargeable miss — the arm licensed to charge states a cutoff below the release under test;
absent from the finding count]
miss
toKebabCase
Task 3, the recognition direction. "It does not compile. Both actions are invented.v.toKebabCase and v.toTitleCase do not exist in valibot — not in the current release and not in any release I know of." One of the two is real and has been since 1.4.0. This is the S2 shape — a rejected-correct pull request — and it is the most valuable barred miss in the battery. (Same-month parking, as above. The half of the answer that concerns toTitleCase is correct and is recorded separately as a clean poison-rung refusal.)[chargeable miss — the arm licensed to charge states a cutoff below the release under test;
absent from the finding count]
correct
parseJson / stringifyJson
Task 5, the attribution anchor. parseJson/stringifyJson at 1.1.0, "roughly spring 2025 ... around April–May 2025" — correct to the minor. The arm is read for attribution, which is the precondition for its boundary answer counting.
correct
toTitleCase
The poison rung. Refused toTitleCase, which has never shipped in any valibot release. P4 holds on this arm.
context
—
The boundary, and it has not moved. This subject placed its valibot boundary at 1.1.0 / 1.2.0 in valibot/v2 eight days earlier and places it at 1.1.0 / 1.2.0 here, under a different battery on a different surface. HARNESS.md § A different battery is not a different boundary predicts exactly this and it holds. The draw also declined to name a version above 1.1.0 at all — "rather than 'I know 1.2.0 exists but can't describe it', my situation is that my knowledge simply stops" — which is a cleaner abstention than the v2 draw gave.