> ## Documentation Index
> Fetch the complete documentation index at: https://docs.astilba.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Round-trip fidelity

> Why astilba defines lossless on resolved t() output, not file bytes, and how the harness proves it.

The defining technical commitment of astilba is **provable round-trip fidelity**: for a
given bundle, import → export → render is lossless. This page explains what "lossless"
actually means here, and how the harness proves it.

## Lossless is on render output, not bytes

A round-trip takes a resource bundle `B`, parses it to the canonical model `C`, and exports
`C` back to a bundle `B'`. The naive test would be byte equality: `B === B'`. astilba does
**not** use that test.

Instead, equivalence is measured on **resolved `t()` output**: for every probe vector, the
string i18next renders from `B'` must be byte-identical to the string it renders from `B`.

This is deliberate. An adapter may renormalise freely — flattening or re-nesting keys,
reordering keys, re-deriving plural suffixes for the target language — because none of those
changes affect what the end user sees. Byte equality would forbid correct, useful
normalisation; render equivalence permits it while still proving nothing was lost.

<Callout type="info">
  Concretely, a key stored nested as `account.friend.name` is free to round-trip as a flat
  key, or vice versa, as long as `t("account.friend.name")` resolves to the same string.
  Key ordering is similarly free.
</Callout>

## What a probe vector is

The harness compares `B` and `B'` not key-by-key but **vector-by-vector**. A vector is one
resolved `t()` call — a combination of:

* a **key** (with its namespace),
* a **context** value (or none),
* a **plural classification** (none / cardinal / ordinal) and, for plurals, a
  **category**,
* the **args** the adapter's render oracle needs (for i18next: `{ count, ordinal, context }`,
  plus sample bindings for any other interpolation variables).

For a pluralised key, the harness generates one vector per CLDR category **that has a
value**, each at a representative count for that category. So `item_one` / `item_other` in
English produces two vectors; the same model in Arabic (six categories) can produce up to
six.

## The harness: driver + contracts

The fidelity check is split into a format-neutral **driver** (in `@astilba/core`) and a
per-syntax **oracle + adapter** (supplied by a syntax adapter like
`@astilba/adapter-i18next-v4`).

```text theme={null}
input ──► adapter.parse() ──► canonical model ──► adapter.export() ──► exported files
                                                              │
                            ┌─────────────────────────────────┘
                            ▼
        for each vector:  oracle.render(sourceFiles, v)  ===?  oracle.render(exportedFiles, v)
```

The driver, `runRoundTrip`, knows only the canonical model. It cannot render — that is
syntax-specific — so it asks the oracle to:

1. **enumerate vectors** for the model, and
2. **render** one vector against a set of files.

The driver then asserts every vector renders identically from `B` and from `B'`.

<Note>
  The render oracle is a **test-support** capability. In production, the end user's own
  i18next does the rendering — never the oracle. The oracle exists so the harness can
  compare bundles deterministically, without depending on the host's ICU.
</Note>

## One important detail: the source must resolve

The model was built *from the source* `B`, so `B` is guaranteed to resolve every vector the
model produced. If it ever does not, that is a bug in the **harness**, not the export — and
the mismatch is flagged with the note `"source did not resolve a vector the model produced"`
rather than reading as a (false) match where `undefined === undefined`.

## The driver and contracts live in core

These are part of the published `@astilba/core` surface, so any syntax adapter — today the
i18next one, tomorrow an ICU one — can plug in:

| Export                                           | Role                                                           |
| ------------------------------------------------ | -------------------------------------------------------------- |
| `runRoundTrip(adapter, oracle, input)`           | The generic driver.                                            |
| `formatMismatches(report)`                       | Render a non-lossless report as a readable failure message.    |
| `FormatAdapter<Files, Input>`                    | Contract: `parse(input)` → model, `export(model)` → files.     |
| `RenderOracle<Files>`                            | Contract: `vectors(model)`, `render(files, vector, language)`. |
| `Vector`, `RoundTripReport`, `RoundTripMismatch` | The data shapes the driver flows.                              |

See the [core API reference](/reference/core-api) for the exact signatures.

## How i18next binds to it

The i18next-v4 adapter supplies the `FormatAdapter` (its `parse` / `exportModel`) and the
`RenderOracle` (its `render` function plus vector enumeration), and exposes the ergonomic
pair `roundTrip(input)` / `assertLossless(input)` on top. Because that adapter is not yet
published as its own package, those entry points currently live in-repo — see
[the adapter reference](/reference/adapter-i18next).

## Related

* [The canonical model](/concepts/canonical-model) — what `parse` produces and `export`
  consumes.
* [CLDR plural rules](/concepts/plural-rules) — how vectors pick a representative count.
