Skip to content

Reject invalid storage_mode and non-string root config values (#187, #188) - #191

Merged
Liam0205 merged 18 commits into
masterfrom
fix/187-188-storage-mode-failfast-byte-exact
Aug 4, 2026
Merged

Reject invalid storage_mode and non-string root config values (#187, #188)#191
Liam0205 merged 18 commits into
masterfrom
fix/187-188-storage-mode-failfast-byte-exact

Conversation

@Liam0205

@Liam0205 Liam0205 commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Closes #187. Closes #188.

#187 — reject invalid config values instead of silently accepting them

Before this, a typo in storage_mode produced a working engine whose memory and performance profile
was the opposite of what you asked for. #179 had aligned the direction of the silent fallback; the
silence itself remained.

Measuring first changed the shape of the fix. The type-handling divergence was not
storage_mode-specific — it affected every root string field, and the three runtimes disagreed three
different ways:

field pine-go pine-java pine-cpp
storage_mode reject coerce reject (throwing as_string())
log_prefix reject coerce silently ignore (is_string() guard)
_PINEAPPLE_VERSION reject coerce silently ignore
_PINEAPPLE_CREATE_TIME reject coerce silently ignore

So pine-cpp was inconsistent with itself, and pine-java invented values through asText()123
became "123", a container became "". All four fields now reject a wrong-typed value in all three
runtimes; null and an absent key both yield the default, matching Go.

Writing the test for that surfaced a fourth divergence nobody had noticed: _PINEAPPLE_CREATE_TIME
did not exist in pine-java at all
, so a config pine-go refused outright was silently accepted. A
4-field × 6-type matrix found it; reading code had not.

The value whitelist is deliberately in config validation rather than the frame factory, so #179's
dispatch rule ("only the exact literal column selects column storage") is untouched — validation
makes the invalid value unreachable instead of complicating dispatch.

#188 — fixtures chosen by shape, not by what already broke

All eight existing byte-exact fixtures existed because something had already failed. Four new ones
cover shapes enumerated up front, and the first one immediately found a real pre-existing
divergence
: pine-cpp emitted {"common":{},"items":[]} on a validation error where Go and Java emit
null, because it set has_result unconditionally after execution while Go returns
nil, &ValidationError{} before any operator runs.

Section 14's header now records what that channel covers and what it structurally cannot: anything
with trace or from /stats can never be byte-stable, because duration_ms and the counters are real
measurements. That was attempted during #183 and abandoned — adding such a fixture yields a check that
fails on correct code.

The expensive discovery, and why this took twelve review rounds

Adding a validation check makes previously-inert parsing differences observable. Nothing inspected
config value types before, so it did not matter that the three runtimes resolve duplicate keys
differently, match key case differently, or reach fields in different orders. The check turned each of
those into a visible divergence. Six instances were found:

Fixed — pine-cpp validated too late (a co-occurring operator error won), then the fix for that
validated too early (a wrong-typed field's error lost to it). Both now gated by error fixtures,
verified red by reverting.

Accepted, recorded in doc-gaps.md with decision inputs — duplicate-key resolution (last-wins in
Go/Java, first-wins in pine-cpp's FlatMap), Go's case-insensitive struct-tag fallback, which field Go
names when several are wrong-typed (it follows JSON document order, so no fixed check order can match
it), debug's three-way split (pine-java's asBoolean() genuinely turns debug on for 1), and
nested type-error-versus-value-error precedence, which needs a two-pass config parse — your call was to
record rather than restructure config loading inside this range.

The generalizable lesson, now recorded: the affected surface of adding a check includes everything
whose relative order that check newly exposes. Asking which fields are read does not reveal it.

Verification

356 Java tests, 254 C++ cases, the Go suite, lint, codegen-check, cross-validate 57 PASS across 21
sections, error parity 35/35, byte-exact 12/12 both pairs, differential-fuzz 1000/1000. Every new gate
verified red against its own mutation. make fmt-check not run — no clang-format locally and no CI job
for it, an existing tracked gap.

Review history worth knowing before merge

Twelve independent blind review rounds in isolated fixed-commit snapshots, plus two aborted attempts
(an external /tmp cleaner deleted one snapshot before the reviewer read a single file — it reported
zero coverage rather than a clean verdict, and snapshots moved off /tmp after that).

Rounds 7-11 found no code defect. Every finding was a sentence I wrote describing one accepted
limitation, and each correction introduced the next: a stale count, a wrong per-runtime attribution, an
arithmetic that contradicted the number on its own line, an overcorrection in the opposite direction,
then an insertion that structurally broke the table it was explaining. Four attempts to state one
boundary in prose, four failures — all because I kept describing a mechanism as a category.

The fix was to stop writing sentences about it. The rule now lives in
StorageModeValidationTest.nestedTypeErrorPrecedenceDependsOnWhetherTheReadThrows, which asserts all
three accessor classes (asText()/asBoolean() coerce, readStringList throws, .fields() yields an
empty iterator), and the prose copy was deleted rather than maintained in parallel. Round 12 then
caught that the test's own comment overstated its coverage — skip and the container reader were
unasserted — which is closed and mutation-verified.

Three separate gate-strength claims of mine turned out weaker than advertised: ordering tests pinning
only position 1, precedence fixtures using root-level fields only, and wrapping_exact pinning a
prefix rather than byte-identity (05-error-parity.sh uses grep -qF, so a suffix-only divergence
passes). All three are now described as what they actually test.

Three terminal evidence audits, all three FAIL, none on the implementation. Every failure was a
derived restatement in my closure records — a misattributed commit, self-initiated commits credited as
review fixes, a false "no code defect" claim in the paragraph justifying stopping, and once a claim to
have fixed something where my .replace() had silently matched nothing. One real finding did fall out
of the chain at the point the auditor predicted was weakest, and is fixed. Derived counts, ordinals and
the commit list are now removed from those records in favour of the git commands that derive them.

Found in passing, filed separately

differential-fuzz surfaced a genuine Go-vs-Java number-spelling divergence at 2^62 from
transform_by_lua under a valid storage_mode — Go emits 4611686018427388000, Java the exact
4611686018427387904. Outside this range, reproduces at the base, filed as #190 with the case
preserved because the /tmp copy will be reaped. It also asks that the fuzz runner print the failing
seed, which it currently does not.

Liam0205 added 18 commits July 29, 2026 19:14
, #188)

## #187: fail-fast, and the divergence was wider than the issue said

The issue framed this as a storage_mode value problem. Measuring first showed the
type half is not storage_mode-specific at all: pine-go rejects a non-string for
EVERY root string field, because they are all declared `string` and encoding/json
fails the whole unmarshal. The other two disagreed, each differently —

  field                    go      java           cpp
  storage_mode             reject  coerce/accept  reject  (throwing as_string)
  log_prefix               reject  coerce/accept  ignore  (is_string guard)
  _PINEAPPLE_VERSION       reject  coerce/accept  ignore
  _PINEAPPLE_CREATE_TIME   reject  coerce/accept  ignore

so pine-cpp was inconsistent with itself, and pine-java invented values through
asText() (123 became "123", a container became ""). All four fields now reject a
present-but-wrong-typed value in all three runtimes. JSON null is accepted and
leaves the default, matching Go, where decoding null into a string is a no-op.

Writing the test for that found a fourth divergence nobody had noticed:
_PINEAPPLE_CREATE_TIME did not exist in pine-java at all, so a config pine-go
refused outright was silently accepted. Now parsed — metadata only, never read for
behaviour, but present so the type rule is uniform.

On top of that, storage_mode now accepts only "row", "column", or absent/empty;
anything else is rejected with a byte-identical message in all three runtimes.
Rejecting at config load rather than in the frame factory is deliberate: the
dispatch rule stays exactly "only the exact literal column selects column storage"
(#179), and validation makes the invalid value unreachable instead of complicating
dispatch. Go's whitelist constants are duplicated in internal/config rather than
imported from internal/dataframe, which imports config — that would be a cycle.

## #188: fixtures chosen by shape, not by what already broke

All eight existing byte-exact fixtures existed because something had already
failed (#180 added the number-format one, #183 the three escaping ones), so the
global "byte-exact" claim rested on much narrower coverage. Four new fixtures cover
shapes enumerated up front: validation-error envelope, deep nesting (maps in lists
in maps), null values at every position, and a filtered 12-item projection.

The error-envelope one immediately found a real pre-existing divergence: pine-cpp
emitted {"common":{},"items":[]} where Go and Java emit nulls. Go returns
`nil, &ValidationError{}` before any operator runs, so there is no projected
result; pine-cpp set has_result unconditionally after execution. Fixed by clearing
it on the validation path only — an ExecutionError mid-pipeline still carries the
fields written up to the failure, which fixture 02 pins.

Section 14's header now records what this channel covers and what it structurally
CANNOT: anything with trace or /stats can never be byte-stable, because
duration_ms and the counters are real measurements. That was attempted during #183
and abandoned; adding such a fixture would produce a check that fails on correct
code.

Section 21 is rewritten, because #187 inverts the contract it was written to
assert. It previously pinned "an invalid value is accepted silently"; it now pins
rejection parity, with the old expectation recorded rather than deleted since
pre-#187 history still describes it. Its captures needed `|| rc=$?` guards: the
section deliberately runs cases that must exit non-zero, and _env.sh sets -e, so
an unguarded $(...) aborted the whole script at the first rejected case.

Two error fixtures added for the rejection path (error parity now 32/32).

354 Java tests, 253 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, differential-fuzz 1000/1000. Every new gate verified red
against its own mutation.
Reflection: memory/reflections/config-validation-and-byte-exact-coverage-187-188.md

Two user-facing guides were stating the OPPOSITE of the new behaviour — both said
an invalid storage_mode is silently accepted by all three runtimes and falls back
to row. That was true before #187 and is now wrong, which makes it the most
misleading kind of stale doc: a reader would conclude a typo is harmless. Rewritten
to state that every other value is rejected, in the DSL at compile time and in all
three runtimes at config load with a byte-identical message, with the old behaviour
kept as a short labelled history.

dag-engine.md's "six interpretation points" table described exactly the baseline
this change removed, so it is replaced by three layers — type, value, dispatch —
and a behaviour table covering every JSON input form. It now says explicitly that
the type rule is not storage_mode-specific: pine-go rejects a wrong type for every
root string field because encoding/json enforces it per struct field, and the other
two were changed to match. Also records why the value whitelist sits in config
validation rather than the frame factory (it keeps #179's dispatch rule unchanged
and makes the invalid value unreachable), and why Go's whitelist constants are
duplicated in internal/config rather than imported from internal/dataframe, which
imports config.

New reference/root-config-string-fields.md carries the type contract on its own,
because it is not about storage_mode: the next person adding a root string field
would not find it filed under one. It lists what to change in all four places, and
records that _PINEAPPLE_CREATE_TIME did not exist in pine-java at all — a whole
missing field is invisible when reading code, which is why the type matrix test
found it and inspection had not.

Four disciplines added. In ci-quality-baseline.md: enumerate fixtures by response
shape rather than waiting for an incident (all eight byte-exact fixtures had been
incident-driven, and the first shape-enumerated one found a real divergence instead
of confirming parity), plus what that channel structurally cannot cover — anything
with trace or from /stats can never be byte-stable; an exhaustive test matrix finds
gaps that reading cannot; and under set -e every capture in a section that
deliberately asserts failure needs `|| rc=$?`, or the script aborts on the first
expected failure and prints no pass or fail at all. In
investigation-to-fix-testing.md: a single runtime is not necessarily
self-consistent — pine-cpp had three different treatments of four sibling fields,
which is the second instance of the shape #183 recorded.

The "measure the affected surface before starting" lesson is promoted to
must/conventions.md rather than recorded a fifth time. Four consecutive tasks hit
it, and this task's evidence is the sharpest: I wrote the issue myself and still
scoped it too narrowly, because writing an issue asks which field shows the
symptom while the affected surface is which fields the mechanism touches.

doc-gaps closes both open entries and opens one: reviewer scratch copies from
close-local-code-review have no owner for deletion, cost 1-1.7GB each, and hit the
disk quota at 17GB accumulated across the last three audits.
Blind review found no code defect. Every finding is in what I wrote about the
code, and the first one matters because it would mislead someone writing a test.

I documented the wrong-TYPE rejection message as byte-identical across the three
runtimes. It is not: pine-go never emits that text at all. Its rejection comes
from encoding/json failing the whole unmarshal, surfacing as "JSON parse error:
json: cannot unmarshal number into Go struct field ...", while only pine-java and
pine-cpp emit "config field X must be a string". I had conflated the two layers —
the VALUE-layer whitelist message genuinely is byte-identical three ways, and I
carried that property across to the type layer without measuring it. Corrected in
the reference and in the index entry that restated it.

Both READMEs still described section 21 as asserting that invalid values are
accepted silently, which is the contract this range inverted, and
bench-cross-runtime.sh's comment still said all three fall back to row storage —
accurate between #179 and #187, falsified by this range. That is the second time
in three tasks that changing a cross-runtime behaviour left a correct comment
elsewhere describing the old one.

Three narrower corrections: my claim that all eight byte-exact fixtures were
incident-driven is wrong (four came from the channel's founding commit b3be250;
only the increments were incident-driven, which leaves the argument intact but the
count wrong); dag-engine.md claimed three-way agreement on "every JSON input
form"; and the reference excluded `debug` in a way that reads as "already aligned"
rather than "still divergent".

Most valuable finding of the round, and it is a divergence THIS RANGE INTRODUCED:
duplicate JSON keys resolve last-wins in pine-go and pine-java but first-wins in
pine-cpp's FlatMap, so {"log_prefix":"ok","log_prefix":123} is now rejected by two
runtimes and accepted by the third. Before this range nothing inspected the type,
so which duplicate won made no difference. Measured on all three. It sits outside
the frozen scope — root string field TYPE handling — so it is recorded as an open
doc-gap alongside `debug`, which still has the pre-#187 pattern, each with its own
decision inputs rather than fixed quietly here.

354 Java tests, 253 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, differential-fuzz 1000/1000.
…vergence

Second review: 0 blocking again, and two findings are worth more than the rest.

The two new error fixtures asserted in prose that the rejection message is
byte-identical across the three runtimes while checking only message_contains. The
reviewer reworded pine-java's message and section 05 stayed green at 32/32 — the
claim had no gate at all. Section 05 has supported wrapping_exact all along and
another fixture already used it. Both fixtures now use it, and rewording Java's
message turns them red. This is the repository's own rule applied to itself: a
byte-exactness claim needs a channel that does no normalization, and prose is not
that channel.

A SECOND divergence introduced by this range: pine-go's encoding/json falls back to
case-INSENSITIVE struct-tag matching, so {"STORAGE_MODE":"colunm"} binds to
StorageMode and is rejected, while pine-java's root.has() and pine-cpp's
parent.find() match exactly and never see the key, so both accept. Identical
mechanism to the duplicate-key gap found last round: before this range nothing
inspected the type, so where Go bound the key had no consequence. Recorded with the
other two exceptions rather than fixed, since it is outside the frozen scope.

I had also mischaracterized `debug`. I wrote that Go rejects a wrong type while the
other two silently ignore it. Measured: pine-java's asBoolean() coerces, and
debug:1 or debug:"true" actually TURN DEBUG ON, while "yes" and [1] coerce to
false. Three behaviours, not two — which materially changes the effort estimate on
whether to align it.

Section 14's header still carried the "all eight fixtures were incident-driven"
over-claim that last round corrected in ci-quality-baseline.md, and that guide's
closing line points readers at this header before they add a fixture. Fixed in both
places now.

Three minors: both user guides asserted the byte-identical message without the
value-versus-type layer qualifier that the reference already draws; Go formats the
rejected value with %q where the other two concatenate raw, so the value-layer
message differs for values containing a quote or backslash (unreachable by any
all-ASCII fixture, same class as #183's escape-vs-raw); and the Apple DSL's
compile-time gate covers storage_mode only, so "the DSL cannot emit a config the
runtimes reject" is false for the other three root string fields.

354 Java tests, 253 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, differential-fuzz 1000/1000.
Third review: 0 blocking. Three important findings, and the first led me into a
mistake worth recording.

A THIRD divergence this range introduced: with two root fields wrong-typed at
once, each runtime named a different field, because each iterated in its own
order. Before this range pine-cpp threw only for storage_mode and pine-java
coerced everything, so no ordering was observable — the range created the
dependency. This repo already treats first-error priority under simultaneous
violations as an external contract.

My first fix was wrong, and measuring caught it. I reordered both runtimes to
pine-go's RootConfig struct declaration order and wrote comments asserting that
was the rule. Then the same three bad fields in two different JSON key orders made
pine-go name log_prefix once and storage_mode the other time: encoding/json
reports whichever wrong-typed field appears first IN THE DOCUMENT, so no fixed
check order can reproduce it. The reviewer's diagnosis was right; its suggested
remedy, and my implementation of it, rested on a wrong model of Go.

What survives is narrower and true: pine-java and pine-cpp share one order and are
each pinned to it by a unit test, verified red by physically moving the C++ check
block. pine-go is documented as input-order-dependent and explicitly outside the
contract. I also deleted the error fixture I had written for it — section 05
enforces message_contains on all three engines and pine-go cannot match, which is
precisely why this property belongs in unit tests. That deletion is the useful
part: a fixture that can never pass is worse than no fixture.

Nothing gated the type-layer message at all. The two value-layer fixtures use
wrapping_exact, but grep found no fixture containing "must be a string", so
pine-java could have been reworded with every gate green. A new fixture pins it for
java and cpp, with Go excluded by wrapping_exact_engines because its encoding/json
phrasing differs by construction rather than by divergence.

My %q caveat was too broad: it exempted non-ASCII, but only PRINTABLE non-ASCII is
unaffected. U+00A0, U+00AD, U+200B, U+2028 and U+2029 all diverge, and those are
the more reachable class precisely because they are invisible in an editor — a
value pasted from a web page carries them.

Three minors: bench-cross-runtime.sh's correction had been prepended without
deleting the false present-tense sentence, leaving a splice that both asserted and
denied the same thing; doc-gaps' heading said two exceptions where its body said
three, with the new bullet indented under the wrong argument; and index.md never
received the entry at all, on the route startup.md points readers down.

355 Java tests, 254 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, differential-fuzz 1000/1000.
Fourth review: 0 blocking, and it found the fourth instance of the class the
previous three kept surfacing — this range added validation whose outcome depends
on parser details that were previously inert.

pine-cpp kept the storage_mode whitelist in validate_config, which runs at the END
of load_config_from_json, after require_obj, parse_operator, parse_metadata and
apply_registry_traits have each had a chance to throw. pine-go and pine-java both
check it first in validate(). So a config violating storage_mode AND something else
reported different errors: pine-cpp named the operator's missing type_name where
the other two named storage_mode. Which error a config reports first is externally
observable, and this repo already treats first-error priority under simultaneous
violations as a contract.

Unlike the duplicate-key, key-case and Go-document-order divergences — which are
properties of three different JSON parsers — this one is entirely ours, so it is
FIXED rather than recorded: the whitelist moved to parse time, immediately after
storage_mode is read. A new error fixture pins it with wrapping_exact across all
three, verified red by deferring the check again.

The ordering tests I added last round only pinned POSITION 1. The reviewer swapped
storage_mode with log_prefix in pine-cpp and every gate stayed green — 254 C++
cases and section 05 — while the two runtimes blamed different fields. Three of
four positions were free under a test whose own source comment claimed the order
was pinned. Both tests now assert every adjacent pair, which locks all four
positions with three assertions, and the swap that used to be invisible now fails.

Three minors: both user guides asserted the value-layer message is byte-identical
with no qualifier, while the reference already records that %q makes it differ for
values containing a quote, backslash, control character or non-printable Unicode;
doc-gaps' new entry had broken list nesting that rendered two of the four
divergences under the wrong parent, with 待决策 covering only two of them and the
scope line still saying two; and the reflection said seven interpretation points
where the whitelist is three rather than one — nine by the same accounting
dag-engine.md uses — and still carried the "all eight fixtures were
incident-driven" claim corrected elsewhere in round 1.

355 Java tests, 254 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, error parity 34/34, differential-fuzz 1000/1000.
Fifth review, and the first blocking finding of this range — introduced by the
previous round's fix for the fourth.

Moving validate_storage_mode to parse time made it outrank operator errors, which
was the point. But it landed BETWEEN the storage_mode and log_prefix reads, so a
config with both an invalid storage_mode value and a wrong-typed log_prefix
reported the value error in pine-cpp while pine-go and pine-java reported
log_prefix's type error. That is java != cpp, the one pairing the new ordering
tests and the source comment both claim is aligned, and it holds regardless of key
order, so it is a fixed disagreement rather than another parser property.

Nothing caught it, and the reason is worth keeping: the adjacent-pair ordering
tests use two wrong-TYPED fields, and the precedence fixture pairs storage_mode
with an OPERATOR error. Neither mixes the type and value layers, which is exactly
where the defect lived. A new fixture does, pinned with wrapping_exact for java and
cpp, verified red by moving the call back.

The call now sits after all four type checks and before parsing, and the comment
states both constraints it has to satisfy at once, because satisfying one is what
broke the other:

  1. after every root-field type check — otherwise the value error outranks a type
     error that Go and Java report first;
  2. before operator parsing — otherwise a co-occurring operator error outranks the
     value error, which is what round 4 fixed.

Two minors: dag-engine.md and pine-cpp-runtime.md still cited the whitelist as
living in validate_config, the one function the new comment says it must not be in;
and the %q caveat still justified itself by saying two user guides assert
byte-equality without qualification, after the same range qualified both.

355 Java tests, 254 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, error parity 35/35, differential-fuzz 1000/1000.
Sixth review, sixth instance of the same class, and this one is not being fixed.

With an invalid storage_mode AND a nested field of the wrong type — for example
$metadata.common_input given as a string — pine-go and pine-java report the nested
type error while pine-cpp reports the value error. Measured across eight nested
fields. The cause is that encoding/json fails on a type error at any depth, while
validate_storage_mode sits after only the four ROOT type checks.

The reviewer had already tested the move that looks correct: putting the call
before apply_registry_traits fixes all eight nested cases and re-breaks what round 4
fixed, so a co-occurring operator error wins again. The two constraints cannot both
be met by relocating one call, because parse_operator interleaves type-shaped and
semantic throws throughout — satisfying both needs pine-cpp's config loading split
into a type pass and a value pass.

I verified that reading independently and put the scope choice to the user, who
chose to record it rather than restructure config loading inside this range. So the
code is unchanged and three places now state the limit precisely: doc-gaps carries
it as a fifth entry including the remedy that fails and why, dag-engine's
"type layer before value layer" claim is qualified to root level, and the source
comment gains a KNOWN LIMIT block so the next reader does not attempt the move.

Also recorded: the three precedence gates added in rounds 3-5 all use root-level
fields, so none of them can fail on this. The reviewer confirmed each does fail on
the mutation it targets, so they pin what they claim — the claim was just narrower
than the invariant the comment asserted. That gap between "what the test pins" and
"what the comment says" is the shape that produced five of the six findings in this
range.

355 Java tests, 254 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, error parity 35/35, differential-fuzz 1000/1000.
Seventh review: 0 blocking, 0 important. The reviewer verified by mutation the
claim round 6's decision rests on — that moving validate_storage_mode before
apply_registry_traits fixes the nested cases and re-breaks operator precedence —
and confirmed both constraints are real, each with exactly one gate, in the right
layer. It measured over thirty error-precedence combinations and found the only
nested-versus-value divergences are the accepted class.

All three minors are counts that went stale when round 6 inserted a fifth doc-gaps
entry: index.md still said four, the reflection's Promotion and Follow-up sections
still said seven interpretation points after the body was corrected to nine, and
doc-gaps' "the last three are newly introduced" stopped pointing at the intended
three once the nested entry landed between them — so the sentence contradicted the
list immediately below it. The second is the same every-copy-of-a-claim discipline
#179 recorded, recurring inside the document that records it.

Also fixed something the reviewer explicitly declined to report: my note said
"three unit tests" assert byte-parity without qualification, and pine-cpp's test
never mentions byte-identity. It judged the over-count as erring toward less risk
and therefore not worth reporting; I fixed it anyway, because it is a number I
wrote and it is wrong.

Left alone, also from the declined pile: bench-cross-runtime.sh's parenthetical
about when the --modes check was added. The reviewer traced it to e3627ec, which
predates #179, making the sentence ambiguous rather than false — and the same
ambiguity predates this range.

355 Java tests, 254 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, error parity 35/35, differential-fuzz 1000/1000.
…radicted itself

Eighth review: 0 blocking, 0 important for the second round running. Both minors
are in text I wrote to describe the accepted nested-ordering limitation, and both
contradict themselves rather than merely being stale.

dag-engine.md said "the other two" place their value check before nested parsing,
in the same sentence that concludes only pine-cpp reports the value error. Those
cannot both be true. Measured: Config.load calls parseRoot — which parses every
nested structure — before validate(), so pine-java's whitelist is strictly after
nested parsing and it reports the nested type error like Go. Only pine-cpp checks
early. doc-gaps.md had this right and named pine-cpp alone; the dag-engine sentence
generalized it wrongly.

The reflection still read "(three dispatch + three parse + ONE whitelist)" on the
very line whose count the previous commit corrected to nine, so the number and its
arithmetic disagreed inside one sentence.

That second one is the every-copy-of-a-claim discipline failing for the fourth time
in this range, now one line below the line that fixed it. The reason is worth
recording: each round I searched for the NUMBER, never for the ARITHMETIC that
produces it, so a parenthetical restating the same claim in different notation
survived every pass. Searching for "七个" cannot find "三 + 三 + 一".

355 Java tests, 254 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, error parity 35/35, differential-fuzz 1000/1000.
Found while sweeping my own restatements before the ninth review, using the lesson
from the eighth: search for the CLAIM, not the number. root-config-string-fields.md
asserted the value-layer message is byte-identical three ways in one paragraph and
qualified that same claim twenty lines later, so a reader stopping at the first
statement would write a test the caveat says cannot hold. Now cross-referenced.
Ninth review: 0 blocking. The important finding is that I claimed a gate stronger
than the mechanism provides, for the third time in this range.

Five fixture descriptions argue that wrapping_exact closes the byte-identical gap
message_contains left open. But section 05 implements wrapping_exact with grep -qF —
containment, not equality. The reviewer appended " (see docs)" to pine-go's message,
producing a genuine byte divergence, and section 05 stayed 35/35 green. Rewording
mid-message IS caught, so the gate has real value; it pins the message prefix. All
five descriptions now say that, with the measurement that shows it.

That is the same shape as two earlier findings here: the ordering tests pinned only
position 1 while their comment claimed the whole order, and the precedence fixtures
used root fields only while the comment claimed the invariant generally. Each time I
asserted a property and reached for a mechanism that tests something weaker.

dag-engine.md's sentence about pine-java being unaffected by the nested ordering
limit holds for leaf fields only. For CONTAINER-typed fields pine-java joins
pine-cpp, because parseRoot cannot detect those type errors at all: .fields() on a
TextNode yields an empty iterator and readStringList silently returns nothing,
neither throwing, so the whitelist fires first. Measured — pipeline_group as a
string gives go=type, java=value, cpp=value. Those container cases already diverged
before #187, so this is a wrong attribution rather than a regression, but it is the
second consecutive round where the sentence fixing an overgeneralization
overgeneralized in a new direction.

Also repaired a sentence in the reference doc where two back-to-back parentheticals
had split a verb from its object across a line break.

Finally, completed the one measurement the reviewer could not: the external cleaner
deleted its snapshot mid-mutation, leaving open whether fixture 10 gates the
has_result fix. Reverting that fix turns section 14 red on
10_validation_error_envelope for Go vs C++ while Go vs Java stays 12/12. It does.

355 Java tests, 254 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, error parity 35/35, differential-fuzz 1000/1000.
Tenth review: 0 blocking, and the first round in which every gate-strength claim
verified without correction — the reviewer probed each with the weakest mutation
rather than the obvious one (swapping only the two _PINEAPPLE_* reads, which no
fixture touches; moving the value check before _PINEAPPLE_CREATE_TIME alone) and
each intended gate still went red.

The one important finding is that round 9's narrowing overcorrected. I had written
that pine-java joins pine-cpp on the nested ordering limit only for container-typed
fields. Measured, eight scalar LEAF fields do too. The real boundary is whether the
read throws: parseOperatorConfig takes type_name, recall, debug, consumes_row_set,
mutates_row_set, additive_writes_row_set, for_branch_control and skip through
asText() or asBoolean(), which coerce silently, so the whitelist fires first;
readStringList throws, so array fields match pine-go. Confirmed both directions —
type_name=123 gives java the value error, sources="x" gives it the type error.

dag-engine.md now carries a three-row table keyed on the read mechanism instead of
a claim about field shape, and doc-gaps and the source comment say the same thing.
That is the third consecutive round correcting a sentence that was itself written to
correct an overgeneralization, so the table is deliberately mechanical: it names the
accessor per row, which is checkable, rather than a category, which is not.

Two aborted attempts preceded this round and neither was a review outcome. The
external /tmp cleaner deleted one snapshot between the reviewer's directory listing
and its first file read; it reported zero coverage rather than a clean verdict,
which is why this run is provisioned outside /tmp. The first retry then died on an
API 401 before doing any work.

355 Java tests, 254 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, error parity 35/35, differential-fuzz 1000/1000.
Four consecutive review rounds failed to state one boundary correctly in prose —
when pine-java agrees with pine-go about a nested type error outranking the
storage_mode value error. Each attempt described it as a CATEGORY and each was
wrong: "all three agree", then "leaf fields agree, containers do not", then
"only containers do not". The boundary is neither, because it is the ACCESSOR:
asText() and asBoolean() coerce and never throw, so the whitelist fires first;
readStringList throws, so array fields match Go.

Rather than write a fifth sentence, the claim is now a test that asserts it over
all seven coerced operator fields and one throwing one, verified red by making
type_name throw. A sentence that becomes false stays quiet; this fails.

The discipline is recorded in investigation-to-fix-testing.md with the four
attempts tabulated: when the same cross-runtime description is corrected by review
twice, stop rewording it and write the assertion. dag-engine.md's table now points
at the test rather than standing alone, and names the accessor per row — checkable —
instead of a field category, which is not.

Note the test asserts an ACCEPTED limitation rather than demanding a fix; the
residual pine-cpp divergence is tracked in doc-gaps.md and needs a two-pass config
parse, which the owner decided against for this range.

356 Java tests, 254 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, error parity 35/35.

differential-fuzz hit one genuine divergence during this round and it is NOT in
this range: go emits 4611686018427388000 where java emits 4611686018427387904 for
the same float64 (2^62), produced by transform_by_lua under a valid storage_mode,
and reproducing at the range base. Filed as issue #190 with the case preserved
under .code-review/preserved/, since the /tmp copy will be reaped. Subsequent runs
were 1000/1000 four times — the failing seed was not printed in the summary, which
#190 also asks to fix.
…laims

Eleventh review: 0 blocking. The new test, the mechanism table and the discipline
note all verified — the test is a genuine gate in both directions, the table is
correct row by row against measurement, and the four-attempt history matches the
commits. The findings are all in what surrounds them.

Two important. My round-10b insertion landed inside the three-layer table, between
the value-layer and dispatch-layer rows, so the dispatch row ended up orphaned
several paragraphs below and the table the prose exists to explain was broken by it.
And that prose said .fields() throws IllegalArgumentException while row three of my
own table, two lines further down, says .fields() yields an empty iterator and does
not throw — the sentence contradicted its own table.

Four minor. Two hardcoded counts in the reflection went stale across four later
commits (3 and 2, against 5 Java tests, 3 C++ cases and 5 error fixtures), and they
sit on the lines either side of the one stating the rule against hardcoding counts.
Two more restatements still claimed all eight byte-exact fixtures were
incident-driven, after round 1 corrected that in two other places.

Fixing these took four attempts and every intermediate step broke something else: a
claim spanning two lines half-corrected, two edits colliding into a sentence with no
object, and a replacement that swallowed the clause introducing the section-14
boundary. Each was caught by re-reading the rendered paragraph and checking marker
parity — the discipline this range recorded after round 5, which I had to actually
run here rather than cite.

Worth noting what the reviewer did: it rejected two claims raised by its own
delegated verification pass, re-deriving each against the code and git history
before deciding. That is the opposite of the failure this range kept repeating.

356 Java tests, 254 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, differential-fuzz 1000/1000.
Round 11's two important findings were both created by round 10b's edit to this same
19-line block: it was inserted inside the three-layer table, orphaning a row, and it
asserted .fields() throws while its own table two lines below said the opposite.
That is three consecutive rounds of findings in one passage.

The mechanism now exists in four places — a mutation-verified test with twelve
assertions, doc-gaps.md, investigation-to-fix-testing.md, and this hand-written
table. Only the test goes red when it is wrong. So the prose copy is deleted and
replaced with a pointer to the test, which is what the previous commit already
established as the durable form.

The discipline generalizes one step further than the note added last commit: not
just "write the assertion instead of rewording" but "then delete the prose copy" —
keeping both means keeping a copy that can silently drift, and this passage drifted
three times in three rounds. Recorded in place.

doc-gaps' cross-reference now points at the test rather than the deleted table.

356 Java tests, 254 C++ cases, lint, codegen-check clean.
Twelfth review: 0 blocking. Two important findings, both consequences of the
previous commit's deletion.

The value-layer table cell now ended mid-sentence: the clause introducing the
mechanism stopped exactly where the deleted paragraph used to continue it. Closed at
the last complete sentence, with the pointer carrying the rest.

More substantively, the pointer claimed the test asserts the rule per-field and that
changing any reader turns it red. Both were overstated — `skip` was missing from the
coerced-field list and the `.fields()` container reader had no assertion at all. The
reviewer proved it by making each throw and getting a fully green suite. Both are now
covered, and the pointer says "three accessor classes" rather than "per-field",
because that is what the test actually does.

Writing the container assertion took three attempts and the third failure is worth
recording. Using `pipeline_group` was wrong because a wrong-typed one also makes
validate() throw "pipeline_group is empty", indistinguishable from the whitelist
message, so it passed for the wrong reason. Switching to `flow_contract` still stayed
green under mutation, because I PREPENDED the key while the base config already
contains it — and Jackson's readTree is last-wins, so the valid object silently
overrode my string and the container path was never exercised.

That is the duplicate-key resolution this range documents as an accepted limitation,
quietly defeating a test about the same family of mechanism. The fix replaces the
existing key instead of prepending, and both traps are written into the test so the
next person does not rediscover them.

The minor finding, a 161-char comment against a 110-column limit, is left: no CI job
checks line length and the missing clang-format job is already a tracked doc-gap.

356 Java tests, 254 C++ cases, Go suite, lint, codegen-check, cross-validate 57
PASS across 21 sections, differential-fuzz 1000/1000.
Terminal audit returned FAIL, and this is the one finding in it that touched code.

Round 12 reported a 161-character comment line in config.cpp against the 110-column
ColumnLimit in .clang-format. I recorded the other findings from that round and
silently let this one go, so it was still live at HEAD — never fixed, never
disproved, never written down as accepted.

It fell out at exactly the point the auditor predicted the evidence chain was
weakest: where the only record of a finding is my own transcription of a report that
no artifact preserves. That prediction landing is worth more than the fix.

Nothing gates it, which is why nothing else caught it: clang-format is not installed
locally and there is no CI job for it, already a tracked doc-gap.

The audit's other three findings were record defects, all corrected in
.code-review/closure-ledger-187-188.md and the scope addendum: a round/commit
attribution that named a commit which never touched the file, two self-initiated
commits credited as round fixes while my own evidence-gap record classified them
otherwise, and a "rounds 7 through 12 found no code defect" claim that this ledger's
own r12 row falsifies — sitting in the paragraph that justified stopping the loop.

254 C++ cases, lint clean.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🔍 PR 审查

项目 结果
结论 APPROVE
审查范围 9b52773afac09710d9c3201f56c49d0d61a4c9ca...a70b98fe53ebcc367143186e5bf6d37598c69bcc

已检查各运行时的 storage_mode 白名单校验、根配置字符串类型校验、验证错误结果形状、错误优先级以及新增跨运行时测试与校验脚本,未发现 BLOCKER、MAJOR、MINOR 或 NIT 问题。Go 单元测试尝试执行,但 runner 需要下载 Go 1.26.2 工具链,当前未能完成。


本次审查由 Codex 主链路 (gpt-5.6-sol) 完成。

@Liam0205
Liam0205 merged commit a9830fc into master Aug 4, 2026
23 checks passed
@Liam0205
Liam0205 deleted the fix/187-188-storage-mode-failfast-byte-exact branch August 4, 2026 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant