A ANLAAgent-Native Lossless Archive

Conformance — ANLA-MVP v0.1

Everything needed to check an independent implementation of the specification, and everything the two reference implementations check about each other.


What is here

PathWhat it is
fixtures.jsonThe language-neutral test cases: trees, plans, and a fixed (uuid, created_ns) per case
vectors/*.anlaFrozen archives, byte-exact, regenerable from the fixtures
vectors/SHA256SUMSTheir hashes
vectors/browser-interop-v0.1.anlaThe archive shipped with the original v0.1 browser release, kept as a read-compatibility vector — not regenerated
run_node.mjsDriver for the JavaScript implementation: pack, verify, extract, selftest, fuzz
../tools/fuzz_differential.pyDifferential fuzzer: mutates the vectors and compares both implementations' verdicts
make_vectors.pyRegenerates the frozen vectors, or checks them with --check

The Python side of the suite lives in 0 and drives run_node.mjs through a subprocess, so one pytest run exercises both implementations.


Running it

In a browser, with nothing installed: <https://anla.evemisslab.com/demo/> runs 76 of these assertions live, including the byte-for-byte comparison against the hashes committed here. It fetches nothing — the fixtures and vectors are compiled into the page — so a red row there is a real defect on your platform.

Locally:

python -m pytest python/tests -q

201 tests. The cross-implementation tests skip themselves if node is not on PATH; everything else runs with nothing but a Python interpreter.

Individual pieces, if you want them separately:

node conformance/run_node.mjs selftest
python conformance/make_vectors.py --check
node conformance/run_node.mjs verify conformance/vectors/basic-store.anla

T-AUX-1 is worth singling out, because it is easy to write as a test that cannot fail. Comparing a manifest with a copy of itself passes no matter what the code under test did. The assertion here rewrites the archive — anla strip does the same thing from the command line — and then compares the rewritten archive's extraction against the original's.


Why the fixtures are shaped this way

fixtures.json is deliberately not generated by either implementation. Both read it and pack the same trees with the same plan and the same fixed variable inputs, and the suite then asserts the outputs are equal byte for byte. A fixture emitted by one of the implementations under test would only prove that implementation is self-consistent.

Two details are worth knowing before writing a third implementation:

Paths are sometimes given as code points. The interesting Unicode cases — a precomposed/decomposed pair, an emoji with a variation selector — are exactly the ones an editor, a filesystem or a copy-paste can silently normalize. A byte-exactness test whose fixture can be rewritten in transit tests nothing, so those paths are carried as path_codepoints arrays.

Not every case is byte-exact across implementations. Cases flagged byte_exact_across_implementations: false use DEFLATE, and two DEFLATE encoders legitimately produce different valid output for the same input. For those, the promise is mutual readability and per-implementation reproducibility, not equal bytes. This is stated in SPEC.md §10 rather than quietly worked around.


The assertions

IDAssertion
T-HDR-1Bad magic, bad version, bad header CRC each rejected
T-FTR-1Bad footer magic, bad footer CRC, UUID mismatch each rejected
T-FTR-2Footer pointing at a non-MANF record rejected
T-MAN-1Manifest hash mismatch rejected
T-MAN-2Manifest identity mismatch rejected
T-REC-1Record header CRC mismatch rejected
T-REC-2Unknown record type rejected
T-CHK-1Chunk payload hash mismatch rejected
T-CHK-2Raw chunk hash mismatch rejected
T-CHK-3Chunk descriptor / record disagreement rejected
T-CHK-4Unknown codec rejected
T-COV-1Chunk coverage not equal to the file size rejected
T-PTH-1.., absolute, drive-letter, UNC, NUL, empty-component paths rejected
T-PTH-2Duplicate path rejected
T-EMP-1Empty file round-trips
T-EMP-2Empty archive round-trips
T-DUP-1Identical content in two files stores one chunk
T-BIG-1File larger than chunk_size splits and restores
T-UNI-1Non-ASCII paths round-trip byte-exact
T-EXT-1Paths the target filesystem folds together fail as a fidelity error, naming both
T-AUX-1Rewriting an archive with the intelligence plane emptied changes no extracted byte
T-AUX-2Stripping twice gives the same bytes as stripping once
T-REP-1Same input and same (uuid, created_ns) gives byte-identical output
T-GLB-1* does not cross /, ** does, a directory survives its excluded contents
T-XIM-1Python writer read by JavaScript
T-XIM-2JavaScript writer read by Python
T-XIM-3Both writers agree byte for byte on every reproducible case
T-XIM-4Both reject the same corruption with the same error code
T-ORG-1The original v0.1 release archive still verifies, in both implementations
T-BMB-1A chunk declaring an absurd raw_size is refused before allocation
T-BMB-2A DEFLATE payload expanding past its declared size is stopped mid-decode, in both implementations
T-CDC-1The gear table matches its derivation, and its pinned digest
T-CDC-2Cut ranges tile the input exactly, within min/max, averaging near avg
T-CDC-3An insertion at the front leaves nearly every chunk shared; fixed-size shares none
T-CDC-4Both implementations cut identically — the content-defined cases are byte-exact
T-CDC-5A reader with no knowledge of the profile still reads a content-defined archive
T-SEQ-1A record sequence of zero is rejected
T-SEQ-2An absurd record sequence is rejected, by both implementations, with the same code
T-SEQ-3The manifest record's sequence must equal len(chunks) + 1
T-SEQ-4Two chunk records may not share a sequence number
T-FUZZ-1A bounded differential fuzz run finds no divergence, on fixed seeds
T-FRZ-1Every frozen vector still matches what the current writer produces

Implementing ANLA-MVP v0.1 elsewhere

The order that makes this least painful:

  1. Read. Parse the header, the footer, then the manifest. Verify the manifest hash against the footer before parsing it. Every frozen vector should open.
  2. Verify. Check each chunk descriptor against its record, each stored payload hash, each decoded chunk against its content id, then each file's coverage and hash. Now T-CHK, T-COV and T-ORG-1 should pass.
  3. Refuse. Work through the T-HDR, T-FTR, T-REC, T-PTH and T-BMB rows. This is where a decoder either becomes trustworthy or does not.
  4. Write. Emit store-only archives first and compare against basic-store.anla byte for byte. If the bytes differ, the difference is almost always canonical JSON key ordering, integer formatting, or object ordering — see SPEC.md §6 and §8.1.
  5. Compress. Add deflate, remembering it is the zlib wrapper (RFC 1950), not raw DEFLATE.

Differential fuzzing

python tools/fuzz_differential.py -n 20000 --seed 1 --keep

The suite proves the two implementations agree on the inputs someone thought of, which is a weaker statement than it looks. The fuzzer mutates the frozen vectors — bit flips, truncation, hostile length and offset fields with the covering CRC repaired, and manifest-level defects re-sealed so every hash is correct — and asks one question of each mutant: do both implementations reach the same verdict?

Not "is the verdict right": that needs an oracle. Agreement needs none, and disagreement is always a defect in an implementation or in this specification for having left the case open.

Two grades:

  • Divergence — one accepts, the other refuses. Always a bug. Fails the run.
  • Code mismatch — both refuse, with different codes. SPEC.md fixes the verification order, so for a mutant with one defect the codes should match. This is reported, kept as a file, and does not fail the run, because it may be a specification gap and the triage belongs to a person.

It has produced two findings so far, and both were invisible to the hand-written suite:

  1. A 64-bit field above Number.MAX_SAFE_INTEGER was a resource-limit error in JavaScript and a malformed-manifest error in Python. Both refused; one was misclassifying. SPEC.md §11 now says which.
  2. Record sequence was specified and unchecked. The spec said "1-based, strictly increasing" and neither implementation looked at the field. Python accepted sequence = 2^63; JavaScript refused it only because it could not represent the number. The rule is now stated as arithmetic a reader can actually evaluate (SPEC.md §4.3) and both enforce it.

The second one is the argument for the whole exercise: no amount of writing more tests by hand would have found it, because the tests and the implementations shared the same blind spot.


If a vector disagrees with your reading of the specification, the specification is what is wrong — open an issue. A frozen vector and a normative document that disagree is a defect in the document, because the vectors are what third parties will actually test against.