Skip to content

Validate the write path against a scanner, and match .NET's doubles - #6

Merged
bbfrederick merged 2 commits into
mainfrom
exar1-scanner-validated
Aug 28, 2026
Merged

Validate the write path against a scanner, and match .NET's doubles#6
bbfrederick merged 2 commits into
mainfrom
exar1-scanner-validated

Conversation

@bbfrederick

Copy link
Copy Markdown
Owner

Everything this package wrote had been checked against what the console
writes, never against what a loader accepts. Those are different claims,
and the second is the one the whole .exar1 effort rests on.

The test

Five archives, built from the shipped examples with one mapped parameter
changed per scan
-- 41 scans in all, covering every entry in MAPPINGS
including the whole Special card. Loaded on an XA60 scanner, then saved back.

5 archives loaded              5/5
41 test scans retained        41/41
41 values retained            41/41 exactly as written

Nothing dropped, clamped or reset. That exercises SHA-1 content addressing,
our DEFLATE, sparse-array insert and delete, bit packing into alFree[0],
enum encoding, ms-to-us scaling, the per-slice arrays, the derived basis and
the container rewiring. The scanner's own PDF prints the values back, which
closes the loop independently of the archive reader.

What the returns taught

The writes were already canonical. Not one ASCCONV field differs between
what we sent and what came back, across 1778 keys per protocol. Nothing was
normalised.

Import is not the same as editing. No churn field moved either.
tCheckUUID, the GUID leading sWipMemBlock.tFree and the date hidden inside
sSpecPara.lFinalMatrixSize* are regenerated when a protocol is edited on the
console
, and left alone when one is merely loaded and exported again. The
churn list came from an edited pair, so it describes the wider case.

The last divergence, now closed. Fifteen protocols differed in content hash
while no ASCCONV value did. The cause, across a 116 KB payload, was one double:

ours    2.893620014190674     (Python repr: shortest round-trip)
theirs  2.8936200141906738    (.NET "R": 15 significant figures, else 17)

That had been documented but not fixable, because no amount of reading says
which of two legal spellings a foreign serializer chose -- it needs a sample of
that serializer's output for a value you picked. A scanner re-saving our own
edited protocols is exactly that sample. envelope.dotnet_double now
reproduces 596 of 596 content blobs across every console-authored archive
byte for byte, each hashing back to its stored address; repr missed one.

json.dumps offers no hook for float formatting, so floats travel through the
encoder as marked strings and are substituted after. The marker has to be
printable: control characters are escaped whatever ensure_ascii says.

Two other fixes

apply() resolved an ambiguous step name to an arbitrary scan. These
archives repeat one sequence with an option varied per copy, so a dozen scans
share a name and the caller believes it has named one. It now refuses and says
how many matched. Found while building the load test, which drove the API the
way a caller does rather than the way the tests did.

sequence_stamp. sWipMemBlock.tFree carries the CMRR build behind the
GUID that churns -- R017, its branch, commit and build time -- and only the
GUID is churn. Treating the whole field as churn, as CLAUDE.md did, would
discard the only record of which binary wrote a protocol. That matters because
the Special card can change between builds and nothing would announce it, so a
mapping is only verified for the build it came from. Note what the stamp does
not say: it reads R017 whether the binary was 017pre15 or a later 017, so the
commit and build time are what identify one exactly.

Corpus

The scanner's returns ship as examples/XA60/*_loadtest.{exar1,pdf} beside
their sources, and test_exar_patch.py asserts no scan went missing and that
every ASCCONV field differing from the source is one a mapping writes. The
archives that were sent are under loadtest/ with a manifest saying what each
scan changes.

52 snapshots, 984 scans. The pinned unaccounted set gains the same five MGH
sequences under the names the load-test generator gave them.

Verification

1733 passed, 2 skipped
black / isort / codespell     clean

Still open

No archive has been assembled -- new scans, new instances, new changesets.
Every file here edits an existing export in place, which is what isolates "does
the loader accept our values" from "does the loader accept a tree we built".
The first is now answered; the second is what a PDF-to-archive generator needs.

🤖 Generated with Claude Code

bbfrederick and others added 2 commits August 28, 2026 11:24
Everything this package wrote had been checked against what the console
writes, never against what a loader accepts -- a different claim, and the
one that mattered. Five archives were built with one mapped parameter
changed per scan, 41 in all, and loaded on an XA60 system. Every scan
loaded and every value survived: SHA-1 addressing, our DEFLATE, sparse
array insert and delete, bit packing, enum encoding, unit scaling, the
per-slice arrays and the container rewiring are now tested rather than
assumed. The scanner's own PDF prints the values back.

What the returns then taught was worth as much as the pass.

Not one ASCCONV field differed between what we sent and what came back,
across 1778 keys per protocol -- so the writes were already canonical and
nothing was normalised. The churn fields did not move either, which
refines the model: tCheckUUID, the GUID leading sWipMemBlock.tFree and the
date inside sSpecPara.lFinalMatrixSize* are regenerated when a protocol is
edited on the console, not when one is loaded and exported again.

The single remaining divergence was one double in a 116 KB payload:
2.893620014190674 from Python against .NET's 2.8936200141906738, the same
value spelled two ways. That could be documented but not fixed without
ground truth, since no amount of reading says which legal spelling a
foreign serializer chose; a scanner re-saving our own edited protocols
supplied it. .NET writes fifteen significant figures where they round-trip
and seventeen where they do not, and envelope.dotnet_double now does the
same: all 596 content blobs across every console-authored archive
re-encode to their stored bytes and hash back to their stored address,
where repr missed one. json.dumps has no hook for float formatting, so
floats travel through the encoder as marked strings and are substituted
after -- the marker has to be printable, because control characters are
escaped whatever ensure_ascii says.

Also fixes apply() resolving an ambiguous step name to an arbitrary scan.
These archives repeat one sequence with an option varied per copy, so a
dozen scans share a name, and the caller believed it had named one. It now
refuses and says how many matched. Found while building the load test,
which drove the API the way a caller does rather than the way the tests
did.

Adds sequence_stamp: sWipMemBlock.tFree carries the CMRR build behind the
GUID that churns -- R017, its branch, commit and build time -- and only
the GUID is churn. Treating the whole field as churn, as this file did,
would discard the only record of which binary wrote a protocol. That
matters because the Special card can change between builds and nothing
would announce it, so a mapping is only verified for the build it came
from.

The scanner's returns ship as examples/XA60/*_loadtest.{exar1,pdf} beside
their sources. Corpus is 52 snapshots and 984 scans; the pinned unaccounted
set gains the same five MGH sequences under the names the load-test
generator gave them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bbfrederick
bbfrederick merged commit 151c962 into main Aug 28, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant