Skip to content

A ninth assertion that compares bytes, and the two levels it takes to land it - #4

Merged
samuelduchesne merged 3 commits into
mainfrom
006-lossless-round-trip
Sep 6, 2026
Merged

A ninth assertion that compares bytes, and the two levels it takes to land it#4
samuelduchesne merged 3 commits into
mainfrom
006-lossless-round-trip

Conversation

@samuelduchesne

Copy link
Copy Markdown
Contributor

Adds preserved-text, the corpus's first byte-level assertion, the thirteen cases that carry it, and the governance entries for the preserving writer in both languages.

The corpus goes from 56 cases and 148 assertions to 69 and 211.

⚠️ Do not squash this PR

It must merge as three commits, because each one is a level and the levels are cut from them:

Commit Tag to cut from main after merge
e6d543f A ninth assertion, which compares bytes conformance-2026.9
d1615ce Register the names the preserving writer adds governance-2026.13
086d563 The preserving writer exists in both languages conformance-2026.10 and governance-2026.14

A squash leaves conformance-2026.9 and governance-2026.13 with no commit to point at. Two conformance levels are not indecision: FR-025 requires the assertion to land while the second language still fails it, so the entries exist, and the change that makes it pass must remove them, which changes the level. The corpus has done this once before, for the introspection capability.

No tags are pushed with this branch. governance/README.md is explicit that "a tag is cut from main after the change has merged, never from a branch", and "publish before you pin" — so the four tags are cut here first, and only then may idfkit and @idfkit/core point at them.

What the assertion is, and is not

A library's preserving write of a case's source text equals that source text, byte for byte.

It is not round-trip under another name. Round-trip re-reads a library's own output and compares documents, saying nothing about the bytes in between, and it writes with preservation explicitly off for that reason. This is that echo, asserted deliberately.

runners/compare.md forbids textual comparison, and the exception added to rule 1 is bounded by a direction rather than by an assertion number: a library's output against that library's own input, never against the other library's output. An exception keyed to "assertion 9" would invite the next byte comparison by precedent.

The cases

Nine carry byte hazards the curated set cannot hold, because it was swept from what one engine emits: both line-ending conventions and both in one file, no trailing newline, a comment carrying both delimiters, a value across two lines, an unterminated final statement, a duplicate name, comments-only and empty. All donated from the second language's syntax fixtures.

Four declare operations, a typed list of the four changes the touched record has to get right, narrowing the assertion to the property FR-023 states. preserve-edit-no-op is the case that caught the first language reformatting an object after writing a field the value it already held.

Divergence entries

46 typescript entries land at conformance-2026.9 naming idfkit-js#12 and are removed at conformance-2026.10, by the change that ends the absence. Both runners are green at the level they are run against.

Two python entries are added and neither is a finding this assertion exposed: idfkit#185 and idfkit#187 were open already and already carried entries on other assertions of the same cases. The five findings this feature did expose in the first language were fixed there rather than accepted here.

Coverage recorded, not implied

Three cases declare preserved-text and are skipped in both languages, because both readers refuse a versionless file. That gap is written into the README's coverage-gaps section rather than left to be inferred from a skip, along with the fact that this assertion proves reproduction of the text a read was given and not of a file's bytes.

…carry it

`preserved-text` asserts that a library's preserving write of a text equals that
text, byte for byte. It is the corpus's first byte-level assertion and its last:
rule 1 of runners/compare.md forbids textual comparison, and the exception added
here is bounded by a DIRECTION rather than by an assertion number, so a second
one cannot be added by precedent. This assertion compares a library's output to
that same library's own input and never to the other library's output, which is
the direction the rule's reason has no purchase on: it is about two libraries
rendering one value as two texts, and this assertion's whole content is that
nothing was re-rendered.

It is not `round-trip` under another name. Round-trip re-reads a library's own
output and compares documents, saying nothing about the bytes in between, and it
writes with preservation explicitly OFF for that reason. This is that echo,
asserted deliberately.

Nine cases carry byte hazards the curated set does not hold, because that set
was swept from what one engine emits: two line-ending conventions and both in one
file, a file with no trailing newline, a comment carrying both delimiters, a
value across two lines, an unterminated final statement, a duplicate name, a file
of comments and an empty one. All nine are donated from the second language's
syntax fixtures.

Four more declare `operations`, a typed list of the four changes the touched
record has to get right, and narrow the assertion to the property FR-023 states:
every byte outside the extents of the objects those operations touched equals the
input. `preserve-edit-no-op` is the case that caught the first language writing a
field the value it already held and reformatting the object for it.

The corpus goes from 56 cases and 148 assertions to 69 and 211. The 37 oracle
cases gain the assertion over their input and over their committed
`expected.epJSON` read as an object-notation input: those files carry a third
tool's formatting rather than either library's, so the object-notation half needs
no new fixture.

The second language has no preserving write yet, which is why 46 entries land in
known-divergence.toml naming idfkit-js#12. The assertion is cut while it fails,
which is what makes the corpus the specification of the behaviour rather than a
check applied afterwards, and the change that lands the writer removes them.

Two first-language entries are added and neither is a finding this assertion
exposed: idfkit#185 and idfkit#187 were open already and already carried entries
on other assertions of the same cases. The four findings this assertion did
expose were fixed in idfkit rather than accepted here.
`check-naming-register.mjs` reads the surface each package's exports map points
at, so the change that adds `rawText` to a built .d.ts is the change the gate
fires on. The register has to be published at a tag first, and this is that tag.

`the original source text` becomes aligned on `IdfDocument.rawText`. It was
divergent only because the second language had nothing to put in the column, and
it is the minimum a consumer needs to answer whether a write will preserve
without reaching into the layer.

`the concrete syntax tree` becomes `the retained source structure` and stays
divergent, with a reason that is true after this feature rather than before it.
The old reason said TypeScript had no counterpart "because it has no lossless
round-trip yet", which stops being true in the change that lands the writer; a
divergence reason describing an absence that has ended reads as a gap nobody
closed. The two languages really do retain different material for the same
purpose, a flat list of text runs against the syntax layer plus a statement
index, and the old concept name named Python's implementation of the idea rather
than the idea, so the TypeScript column could only ever be empty.

The anchoring and the touched record get no entry. Neither is public in either
language, and the register governs public names.
46 typescript entries against idfkit-js#12 went stale the moment the writer
landed, and a case passing while its entry remains is a failure rather than a
pass. They are removed here, which is what conformance-2026.10 is:
conformance-2026.9 with those entries gone.

The parity ledger moves `lossless-round-trip` to complete on both sides, and
`absence_kind` and `issue` go with the absence they recorded. `names` gains the
two register concepts this feature made true.

Complete rather than partial, on both sides. Both formats are in scope, so
neither language carries a gap. The object notation's all-or-nothing terms
differ from the text format's per-object terms in both languages equally, which
is a property of the format rather than a difference between the libraries, and
the entry's comment block states it so a reader who knows one format does not
assume the other's terms.
@samuelduchesne
samuelduchesne requested review from a team as code owners September 6, 2026 16:00
@samuelduchesne
samuelduchesne merged commit 21de220 into main Sep 6, 2026
1 check passed
@samuelduchesne
samuelduchesne deleted the 006-lossless-round-trip branch September 6, 2026 16:08
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