feat: schema-check a Vale rule before Vale ever sees it - #175
Conversation
|
@claude /review |
|
Claude finished @thecodedrift's task in 7m 32s —— View job Review mode: incremental — read 0 prior review thread(s) before reviewing.
SummaryThis is a well-built unit — the corpus/contract-test discipline (86 rows, measured against the real vendored binary rather than the docs) is exactly the right way to hold a hand-authored transcription honest, and the [New] Correctness:
|
— AI Coding Agent |
`verify` validated `level` and the presence of the rule's `.vale.ini`, so `extends: nonsense` and `scope: fenced` both verified clean. It now also checks `extends` against the twelve check types, every `scope` operand against the vocabulary the binary honors, and every field against the table for the check the rule extends. Ordering is the point for two of the three. Vale reads one assembled config per run, so an unknown `extends` or a foreign field reaching the binary takes down every other Vale rule's findings — not just the offending rule's. The layer is a pure read of the rule's own files, and `test` runs it first and stops, so neither can get that far. `scope` is modelled as a grammar rather than an enum, because the binary accepts a bare operand, a list, `~` negation and `&` chaining, and a flat enum would reject all but the first. It is deliberately stricter than Vale in one place: `~fenced` fires on everything, having silently lost the exclusion it was written for, which is exactly the class of failure this closes. The schema is hand-authored — Vale publishes no JSON Schema and its machine-readable field knowledge is behind a paid hosted MCP — so what makes it true is `test/vale-corpus.ts`: 82 minimal rules, each with a document it must flag, run through both the vendored binary and the schema, asserting the two agree in both directions. A verdict cannot be read off an exit code, since an unrecognized scope parses clean and matches nothing, so the corpus records three outcomes and carries two guards against a row that passes while asserting nothing. Both guards earned their place: `figure.caption` first measured as ignored, because Vale drops everything inside a `<figure>` element. Fixes #171 Refs #170 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cEN93Acyp4zBwP3oDnyy1
`validateValeRule` was a hand-rolled walker pushing template strings into
an error array, so the CLI had two validation styles for two engines —
one producing `ZodIssue`s, one producing strings — against a spec that
asks both to fail the same way.
The per-check field tables are now a `z.discriminatedUnion` on `extends`:
ten `z.strictObject`s, so `E201` is a property of the schema's shape
rather than a hand-written key walk, and two `z.looseObject`s for
`consistency` and `spelling`, which the binary was measured accepting any
key at all on. A new `schemas/layer.ts` turns a zod parse into the
`{ valid, errors }` shape `verify` reports, and the ast-grep path now
goes through it too — the two differ only in how an issue becomes a line,
which is a parameter.
Two things zod cannot express on its own, and both stay explicit:
`scope` is a grammar over the operands rather than an enum, so it is a
`.check()` with a small parser; and the deliberate rejection of a
negation over an unknown operand is a business rule, so it says so in
its own message rather than borrowing the generic one.
`test/vale-corpus.ts` is untouched and all 82 rows still pass, which is
the property that makes this safe: the corpus asserts
`schemaAccepts === binaryAccepts` against the vendored binary, so a zod
schema that came out stricter or laxer than the walker would have named
the row rather than passing quietly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cEN93Acyp4zBwP3oDnyy1
Re-probed strict-versus-permissive across all twelve check types with a
key no check has, rather than a real field borrowed from another one.
The split is unchanged — `consistency` and `spelling` are the only two
that validate nothing — but `sequence` reads as permissive under a
careless probe and is not.
Probe a tokenless `sequence` rule and Vale reports no invalid keys, so a
probe grepping its output for `has invalid keys` scores it permissive.
Give the rule its `tokens` and it rejects an unknown key like every other
strict check. What happens without them is worse than E201:
panic: interface conversion: interface {} is nil, not []interface {}
The process dies, and a stack trace contains no `has invalid keys`
string, so the grep sees a clean run. A measurement taken by grepping for
an error string cannot tell "no error" from "no output" — every corpus
verdict is read from the exit status instead.
Three shapes end the process rather than reporting anything: a `sequence`
with no `tokens`, a `sequence` whose `tokens` is not a list, and a
`metric` with a `formula` and no `condition`. Every key in them is legal;
it is the shape that is fatal, which is why they are checked after the
field tables rather than inside them. A panic is a wider blast radius
than E201 — E201 names a file and a line, a panic names no rule at all —
so `verify` is the last place they can be caught.
Four corpus rows added: `field/sequence+unknown`, which is the coverage
whose absence let the question stand, and one per fatal shape.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cEN93Acyp4zBwP3oDnyy1
The fatal-shape guard fired only on `rule.condition === undefined`, so
`condition:` written with no value verified clean. YAML parses that to
null, not undefined.
Probed against the vendored Vale 3.18.0 binary: a metric check with a
formula panics when its condition is absent, null, or a blank string,
all three with `interface conversion: interface {} is float64, not
bool`. Other types (number, bool, list, map) fail the decode cleanly
with an E201 and are left to the ordinary path. The guard is now
structural, like the neighbouring sequence arm, and two corpus rows
record the measurement. Both rows fail against the old guard.
Also wires VALE_PERMISSIVE_CHECKS into a test, so "consistency and
spelling are exempt on purpose" is machine-checked rather than only
described in a comment, and corrects the panic string the doc comments
attributed to all three shapes.
607043f to
40c9d9c
Compare
`pnpm generate:vale-schema` runs the pinned Vale against rules it writes itself and emits `src/generated/vale-vocabulary.ts`, alongside a divergence report. It replaces a transcription whose measurements were real and whose measuring script was discarded — leaving the answer, not the question. Every verdict comes from the process exit status and the structured JSON output, never from matching stdout against an error phrase. A Go panic contains no `has invalid keys` string, so a phrase-grep scores a crash as a clean run, which is how a tokenless `sequence` rule once looked like a check that validates nothing. A run's outcome is a closed set — clean, diagnostic, panic, unrecognized — and the last is fatal at every call site. Two of the four vocabularies self-enumerate: an unknown `extends` or `level` makes the binary name its own accepted set. If either line stops matching, generation fails rather than emitting a short enum, because a truncated enum is stricter than the binary and strictness is the direction that blocks working rules. The other two are honest about their limit. `E201` names the key you got wrong and never the ones you could have used, and an unknown scope raises nothing at all, so field tables and scope operands are verified rather than discovered from a candidate list whose provenance is recorded. A scope verdict is three-valued, and a `scope: raw` reach probe must fire on every fixture so an operand cannot be dropped because its fixture was unreachable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cEN93Acyp4zBwP3oDnyy1
The enums and field tables move out of `vale-rule.ts` and into the generated artifact; what stays is the part worth reading — the zod construction, the two-stage `.pipe()` that reproduces the binary's own order, the case-folding transform, the scope grammar, and the error messages that explain blast radius to an author. Generating those would mean maintaining a code emitter to reproduce prose. Three things stay hand-written on purpose, and each says why in place: - The fatal shapes. Every key in a tokenless `sequence` is a legal field of its check; it is the shape that panics, which a field table cannot express. - The scope grammar. `~` and `&` are operators around the operands, and rejecting `~fenced` is a business rule rather than a transcription. - The union members, spelled out so each `z.literal` survives into `ValeRule`. An import-time guard requires every derived check type to appear exactly once and to be classified strict or permissive. The artifact's version is asserted against `VALE_VERSION` as a conditional type rather than an `if`. Both are string literal types, so the compiler settles it and a mismatch is a build failure naming the line, instead of a throw in front of a user on whichever command loads the schema first. All 86 existing corpus rows pass unmodified — that is what proves generation reproduced the transcription. Two rows are added for ground the generation newly measured: `comment.line` and `comment.block` in the TypeScript tier, which is the only operand family whose membership depends on which parser the extension routes to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cEN93Acyp4zBwP3oDnyy1
The `cli-rule-validation` requirement "The Vale rule schema is pinned to the vendored binary" was written for a hand-authored transcription. It becomes a derivation, and gains the three constraints that make one trustworthy: take every verdict from the exit status and the structured output rather than from an error phrase; fail rather than emit a partial enumeration, because a short enum is stricter than the binary; and report a divergence from Vale's documentation rather than resolving it silently. Delivery shape is a single PR — the generator, its artifact, and the schema's switch to importing it are only correct together, and there is no intermediate state that reaches production safely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cEN93Acyp4zBwP3oDnyy1
…c reader Three review findings on the derived Vale schema. The import-time guard in src/schemas/vale-rule.ts validated a freestanding list of check names that nothing else used: the discriminatedUnion array was a separate hand-written list, so the guard proved the name list agreed with the derived vocabulary and said nothing about the union. A maintainer could answer the guard's own error by adding the name, leave the union a member short, and get a schema that is silently too strict for the new check. The union array is now UNION_MEMBERS itself, the guard reads its discriminants off the built members, and a duplicate member is rejected too, so "exactly once" is checked rather than claimed. Verified by removing a member: the import now throws where it previously passed. The generator's ValeDiagnostic/readDiagnostic duplicated ValeConfigError and asValeConfigError from src/rules/vale/map.ts. It now imports them, the same way it already imports findValeBinary and VALE_VERSION. probe()'s isolating config and buildIsolatingConfig share three load-bearing details but differ on StylesPath, so they now cross-reference each other instead of being merged: reuse would mean staging a fake .taskless layout in a temp directory to satisfy a path convention no probe has. pnpm generate:vale-schema reproduces both generated artifacts byte for byte.
"two rows are added for ground the generation newly measured" is missing its verb. Reads "to cover ground" in all three places it appears, including the release note.
Stack (root → tip):
What this is
Unit 2 of the
vale-authoring-polishstack, on top of #174. It adds the Valerule schema, wires it into
verify, and brings the corpus that holds the schemato the vendored binary. It also archives the change, since this is the tip.
Review #174 first — it carries the measurements this unit encodes.
The gap
verifycheckedleveland the presence of the rule's.vale.ini. Measuredagainst the pinned Vale 3.18.0, three defects got through it:
extends: nonsenseok: truetokenson anoccurrencecheckok: trueE201, every rule silentscope: fencedok: trueThe first two are not local failures. Vale reads one assembled config per run,
so either one reaching the binary takes down every other Vale rule's findings.
The third is the quiet one: nothing anywhere reports it, ever.
What
verifynow checkspackages/cli/src/schemas/vale-rule.ts, a zod schema reported through the sameLayerResult-shaped error list thesgpath already uses:extendsagainst the twelve check types, naming the accepted set.scopeas a grammar, not an enum. The binary accepts a bare operand, alist,
~negation, and&chaining; a flat enum would reject all but thefirst, which is worse than the gap it closes. The enum applies to the
operands, the schema accepts the operators around them.
E201.way (
Tokens:works), whileextends/message/levelare read literallyand their values are case-sensitive — matching the split the binary has.
It runs before Vale is invoked, and
teststill runsverifyfirst and stops,so a rule that would take the engine down cannot reach it. There is a test
asserting exactly that, via
ran: false.How it is built
Two stages, in the order the binary itself works.
Stage 1 — the header, a
z.record(...).check(...). Vale readsextends,messageandlevelliterally and gives up if they are wrong, so these arechecked against the raw keys.
scoperides along because it is common to alltwelve checks.
Stage 2 — the check's own fields, a
z.discriminatedUnion("extends", …):ten
z.strictObjects and twoz.looseObjects. This is the real win — Vale'sE201class becomes a property of the schema's shape, so a field belonging toanother check type is rejected by the union rather than by a hand-written key
walk. The two loose members are
consistencyandspelling, which the binarywas measured accepting any key at all on; making them strict would be the
"too strict" failure.
Stage 3 — the shapes that panic the binary, a
.check()after the union.See below; these are not field-table facts, so they do not belong in it.
.pipe()sequences the two, which reproduces the binary: zod runs stage 2 onlyif stage 1 produced no issues, so an
extendsVale does not recognize isreported on its own — with no check type there is no field table to check
against.
Between the stages, a transform lowercases every field name Vale would lowercase
and no others. Vale decodes a check's fields case-insensitively (
Tokens:andignoreCase:work) but reads the three header keys literally, andLEVEL: warningreaches the field decode aslevel— which the header reader alreadyremoved — so it comes back as
E201 has invalid keys: 'level'. The transformreproduces that split.
Two things zod cannot express on its own, and both stay explicit rather than
being contorted into schema shape:
scopeis a grammar, not an enum. A.check()with a small parser walksthe
~and&operators and applies the operand enum inside them.~unknownrejection is a business rule, not a transcription, so itcarries its own message saying so rather than borrowing the generic one.
schemas/layer.ts(18 lines of code) turns a zod parse result into{ valid, errors }, and the ast-grep path now goes through it too. The twoengines differ only in how one issue becomes one line, which is a parameter:
ast-grep's messages come from an upstream JSON Schema and are generic
("expected string"), so the path prefix is what makes them actionable; every
Vale message is already a full sentence naming its own field.
export type ValeRule = z.infer<typeof valeBodySchema>— the union members arespelled out rather than mapped over the field table so each
z.literalsurvivesinto that type.
Strict versus permissive, measured for all twelve
Re-probed with a key no check has (
bananafield) rather than a real fieldborrowed from another check, and read off the exit status rather than by
grepping for
has invalid keys:capitalization,conditional,existence,metric,occurrence,readability,repetition,script,sequence,substitutionconsistency,spellingsequenceis strict, and it is the one that misleads. Probe it asextends: sequence+message+ an unknown key and Vale reports no invalidkeys — so a probe grepping for that phrase scores it permissive. It is not: give
the rule its
tokensand it rejects the unknown key like every other strictcheck. What happens without
tokensis worse than anE201:The process dies, and a Go stack trace contains no
has invalid keysstring, sothe grep sees a clean run. A measurement taken by grepping for an error string
cannot tell "no error" from "no output." Every corpus verdict is read from the
exit status instead.
Three shapes that end the process
That probe turned up three rules where every key is legal and the shape is
fatal:
sequencewith notokenssequencewhosetokensis not a listmetricwith aformulaand noconditionA panic is a wider blast radius than
E201: anE201names a file and a line,a panic names no rule at all, produces no findings for anything in the project,
and gives an author nothing to act on.
verifyis the last place they can becaught, so it rejects all three — in a pass that runs after the field tables,
since a shape can only be fatal if every key in it was legal to begin with.
Cost
vale-rule.tswent 358 → 504 lines (201 → 261 non-comment), plus 49 linesof
layer.tsand 8 fewer inverify.ts. So this is not a line saving: zod'sissue construction is more verbose than pushing a string, and that is where the
~60 lines went. What was bought is that the field tables are enforced by shape
rather than by a walk, the two engines share one adapter, and the rule has an
inferred type.
The measured field tables stayed a plain
CHECK_FIELDStable beside a thinunion, deliberately — that table is the measurement, and the union is only how
it gets enforced.
The one deliberate divergence
scope: ~fencedfires on everything, because there is nofencedtosubtract. The binary "accepts" it in the only sense an exit code can express,
and what the author gets is a rule whose exclusion was silently deleted — which
is precisely the class of failure this change exists to close.
verifyrejectsit.
That is the schema being stricter than the binary, which is normally the worse
direction, so it is not a skip in the differential: it is a
divergencerowthat asserts the disagreement and carries its reason. Exceptions are
countable, not hidden.
The corpus
packages/cli/test/vale-corpus.ts— 82 rows, each a minimal rule plus adocument it must flag, run through both the binary and the schema by
vale-schema-contract.test.ts. Shape:extends: nonsenseandextends: Existencerejected.scopeoperand rows, including all six heading levels, thetable.*andcomment.*families,frontmatter/frontmatter.<key>,text.class.<name>,figure.caption, and the~/&/list operators.metaandmeta.class.title— the operandsthe design's first draft named, which do not exist.
draft would have wrongly rejected, and the unknown-key row for each of the
three permissiveness candidates —
sequence(rejects),consistencyandspelling(accept).A verdict is not an exit code
Vale does not say "that is not a scope." An unrecognized scope parses clean and
matches nothing, which from outside is identical to a valid rule whose pattern
did not fire. So rows record three outcomes —
accepted(fired),ignored(exit 0, nothing),
rejected(the run failed) — and two guards keep a row frompassing while asserting nothing:
scope: rawprobe must fire on every control, so every controlcontains the same token. It proves Vale linted the fixture at all.
ignoredrow carries aproof: the same rule withthe construct replaced by a known-valid one, which must fire on the same
control. Only then is "did not fire" attributable to the construct.
Guard 1 is not theoretical.
figure.captionfirst measuredignored— andso did a
scope: textcontrol over the same document, because Vale dropseverything inside a
<figure>element. Read off the first result alone, theoperand would have been dropped from the schema, and every rule using it would
then have failed
verify: the "too strict" failure, arrived at through acareless fixture.
Proving the suite is not vacuous
Seven mutations, each failing exactly the expected row and nothing else:
figure.captionfrom the enumscope/figure.captionreadability(the docs' eleven)extends/readabilitymetato the enumscope/metatokensonoccurrencefield/occurrence+tokenssequencealooseObjectfield/sequence+*rowsshape/*rowsVersion-bump tripwires
Two, per the spec's requirement that a Vale upgrade fails loudly naming the
field:
vale-vendor-contract.test.tsnow asks the binary to enumerate its owncheck types — Vale prints the whole set when rejecting an unknown
extends— and compares that to
VALE_CHECK_TYPES.Verification
pnpm typecheck,pnpm lint,pnpm testpass — 851 tests across 54 files.test/vale-corpus.tswas byte-identical across the zod rewrite and all 82rows passed; it has since gained four rows (and only rows) for the
sequencefinding above, and all 86 pass. That is the property that made the refactor safe rather than
hopeful: the corpus asserts
schemaAccepts === binaryAcceptsagainst thevendored binary, so a zod schema that came out stricter or laxer than the
hand-rolled walker would have named the row instead of passing quietly.
pnpm openspec validate --strict --allpasses, 24 items.pnpm build, then the real CLI against a deliberately-broken rule:And the foreign-field case, which is the one with the widest blast radius:
Fixed as the recipe says (
extends: existence,level: warning,nonword: true,scope: raw), the same rule verifies clean and then reports areal finding on a real file:
On the size of this diff
~1700 lines, against the ~300-line guideline, and it did not want to be split
further. The breakdown: ~550 lines of schema
and adapter, 33 of wiring, and 1300 of corpus and tests. The corpus is not test scaffolding around the schema — per the
design, it is the artifact the schema is derived from, and the schema without
it is an unverified transcription of a vendored binary. Splitting them would put
a slice on
mainwhose central claim ("these are the values Vale accepts")nothing checks.
The reviewable core is
src/schemas/vale-rule.ts,src/schemas/layer.ts, andthe 20-line change to
src/rules/inspect.ts.test/vale-corpus.tsis a table; read a few rows andthe header comment rather than all 86.
Decisions a reviewer might want to push back on
consistencyandspellingare exempt from field checking. Measured,they accept any key at all —
bananafield: trueloads and is ignored. Beingstrict there would reject rules the binary runs, to catch a typo Vale itself
does not catch. The recipe says so in prose, which is the only place it can be
said.
src/schemas/vale-rule.ts, notsrc/generated/. Thatdirectory is for artifacts a script fetches; this is hand-authored. It sits
beside
schemas/ast-grep-rule.ts.messageisreported without its field errors. That mirrors Vale, which abandons the file
at the header read — measured:
Message:(wrong case) reports only"Missing the required 'message' key" — but it does mean two iterations for a
rule with one problem in each stage.
extendssuppresses the per-check field report. With no knowncheck type there is no field table, so the function reports that one error
rather than inventing complaints from a table it does not have. Visible in the
first output above.
.taskless/rules/vale/still verifies; this repository carries none, so the test would be vacuous. It
verifies the nine worked rules from
create-vale-ruleinstead — thepopulation the requirement is actually about.
Fixes #171
Refs #170
Contains #177
What this is
Unit 3 of the
vale-authoring-polishstack, on top of #175. It replaces thehand-written Vale vocabulary with one derived from the vendored binary by a
checked-in generator, keeps zod as the validation layer, and ships the
divergences the derivation found as a report rather than dropping them.
Review #175 first — this rewrites the schema it introduced.
Why
#175 called its own schema what it was: a transcription. Twelve check types,
three levels, ten field tables and twenty-eight scope operands, read off a
binary by hand because Vale publishes no JSON Schema and its machine-readable
field knowledge sits behind a paid hosted MCP.
Every one of those values was measured. The measuring was done by a script
that was then thrown away. What survives is the answer, not the question —
the next person to raise
VALE_VERSIONgets a test failure naming a field andno way to reproduce the measurement it is failing against.
The measuring is also where the errors live, and one is already on the record:
A Go panic contains no
has invalid keysstring, so a phrase-grep scores acrash as a clean run. A method that can make that mistake is worth writing
down once, correctly.
The method rule
Every verdict comes from the process exit status and the structured JSON
output. Never from grepping stdout for an error phrase. A run's outcome is a
closed set with no escape hatch:
cleandiagnosticCodepanicunrecognizedunrecognizedis fatal at every call site. There is deliberately no branchthat folds an unfamiliar shape into "fine".
What is derived, and what is honestly not
'extends' key must be one of [...]'level' must be one of [...]has invalid keys: '<name>'— names the bad keyThe top two self-enumerate: hand the binary a sentinel and it names its own
accepted set. If either line stops matching, the generator errors rather than
emitting a short enum — a truncated enum is stricter than the binary, which
is the direction that blocks rules that would have worked.
The bottom two are verified, not discovered, and the artifact says so. The
candidate list is seeded from four sources, including names the binary rejects
(
prefixes,suffixes,ignorecaseoncapitalization) so that theirrejection stays a recorded finding rather than an omission, and every check's
fields are offered to every other check so the tables are a measured partition.
Membership is read as evidence about the key, since the probe value is
arbitrary. Two of the positive verdicts are inferences rather than readings —
"Vale objected to the value instead" and "Vale panicked, so it dereferenced
something" — and both are named in the report, because an inference nobody
can audit is a grep with better manners.
scopehas no oracle at all, so its verdict is three-valued and ascope: rawreach probe must fire on every fixture or the run is fatal. That guard is
not theoretical:
figure.captionmeasures silent when its fixture nests thecaption inside a
<figure>, and so doesscope: textover the same document,because Vale drops everything in that element. Without the guard the operand
would be dropped as one Vale ignores, and every rule using it would then fail
verify.The divergence report
src/generated/vale-vocabulary-report.md, emitted by the run:meta,meta.class.<kind>verifyrejects themfrontmatter,frontmatter.<key>consistency,spellingactionon all ten strict checksexpected a map, got 'bool'frontmatteris the standing proof of the limit above: nobody proposing fromthe documentation alone would have found it.
Three claims carried in from earlier measurement did not reproduce, and the
report is where that surfaced:
figure.captiondoes fire, on a bare<figcaption>. What never fires isthe nested-in-
<figure>form; the earlier note conflated the two.comment.blockfires, in.jsand.tsalike.comment.*does not need a.tsfixture specifically —.jsreaches thesame tier. Rows for both now exist, because "fires in a source file" and
"fires in this source file" are different claims.
What stays hand-written
sequenceis a legalfield of its check; it is the shape that panics, which a field table cannot
express.
~and&are operators around the operands, andrejecting
~fencedis a business rule rather than a transcription.z.literalsurvives intoValeRule. An import-time guard requires every derived check type to appearexactly once and to be classified strict or permissive.
if.Both sides are string literal types, so a mismatch is a build failure naming
the line instead of a throw in front of a user.
How it is checked
The corpus is the acceptance test. All 86 existing rows pass unmodified
against the generated schema — that is what proves generation reproduced the
transcription. No row was edited to fit the output. Two rows were added for
ground the generation newly measured (
comment.line/comment.blockin theTypeScript tier), for 88.
The generated tables matched the hand-written ones exactly: the same twelve
check types, the same three levels, the same nine common fields, all ten
per-check tables identical, the same twenty-eight scope operands and two
prefixes. Nothing in the vocabulary changed; only how it is produced.
Deleting both artifacts and re-running produces byte-identical files.
What it looks like
The second only appears once the first is fixed, which is the
.pipe()reproducing the binary's own order: with no valid header there is no field
table to check anything against.
Refs #171