Every port's fmt command must format a metadata file identically —
same canonical bytes, same decision about whether a file can be formatted at
all. This corpus is the shared source of truth, run by each port's own test
suite, the same way fixtures/conformance/ gates the loader and
fixtures/render-conformance/ gates the render engine.
fmt formats each file on its own — own-mode, declared-here layer only
(ADR-0039). It never merges a file with its siblings: an extends onto a
base declared in another file is preserved as the raw ref string (never
resolved, never an error here — resolving it is the full project loader's
job, not fmt's), and a file declaring overlay: true ANYWHERE in its tree is
reported as a skip, not guessed at — whether or not a same-file base
exists to merge into. A formatter never changes structure: a plain
declaration and a same-(type, name) overlay: true redeclaration in the
SAME file does have a local base, and the ordinary loader would merge them —
but merging silently drops the overlay marker from the output, which is a
structural change fmt must never make. So this is checked directly against
the raw document before anything else runs, not inferred from whether
resolution happens to fail.
This corpus therefore exercises exactly that single-file contract. Loading a whole PROJECT (resolving sources, running the whole-project safety check before a file is rewritten) is CLI-level orchestration, specific to each port's command surface, and is not what this corpus gates — each port's own integration tests cover that.
<fixture-name>/
├── input.json # required — one metadata file's own raw content
├── expected.json # happy path: the canonical text fmt must produce
└── expected-skip.json # OR: fmt must refuse to format this file standalone
Exactly one of expected.json or expected-skip.json is present:
expected.json— formattinginput.jsonstandalone succeeds. The file's canonical output (2-space indent, canonical key order,@-attrs alphabetized, a scalar@fieldsnormalized to its array form, the FR-016/ADR-0018source.rdbphysical-name rewrite applied, trailing newline) MUST deep-equal — and for JSON-emitting ports, byte-equal — this file.expected-skip.json—{ "reason": "overlay" | "error" }.fmtmust refuse to formatinput.jsonstandalone and classify WHY:"overlay"— the file declares (or contains, anywhere) anoverlay: truenode — REGARDLESS of whether a same-(type, name)base also exists in the same file. A MIXED file (a plain declaration alongside an overlay, whether or not that overlay's target is also in this file) is still"overlay"as a whole — fmt does not partially format a file, and it never merges a same-file base+overlay into one node even though the ordinary loader would (that merge drops the overlay marker, which is the one thing a formatter may never do to a file's structure)."error"— any other reason the file cannot be parsed standalone (an unregistered type/subtype, a missing required structural key, …).
A port's test runner loads input.json, formats it the way its own fmt
formats a single file, and asserts the outcome against whichever expectation
file is present. Error/skip message text is never compared — only the
reason classification, exactly like the loader corpus compares error
codes, never message text.
Kebab-case, no leading numbers, descriptive of the one behavior under test —
already-canonical, reorder-structural-keys, skip-overlay-no-local-base.
Names persist forever once a fixture exists in CI history.
A node merged from two files (an overlay, or two plain files that happen to
declare the same (type, name)) has no single file whose "own content" it
is — serializing it would either invent attribution or silently drop one
file's contribution. Rather than approximate that, fmt only ever rewrites
a file it can prove is a complete, self-contained declaration, and every
other port implements that SAME refusal — which is exactly what the
"overlay"/"error" fixtures above pin down cross-port.
| Fixture | What it proves |
|---|---|
already-canonical |
A file that is already canonical round-trips unchanged |
reorder-structural-keys |
Structural keys reorder to name → package → extends → abstract → overlay → isArray → @-attrs → children |
sort-attrs-alphabetically |
Inline @-attrs sort alphabetically regardless of authored order |
normalize-scalar-fields-to-array |
A scalar @fields: "x" becomes @fields: ["x"] |
source-rdb-physical-name-rewrite |
A legacy @table alongside a non-table @kind rewrites to the kind-matching physical-name attr (FR-016/ADR-0018) |
preserve-unresolved-cross-file-extends |
An extends ref to a base declared in another file is preserved as-is, never an error |
skip-overlay-no-local-base |
A file whose only declaration is overlay: true with no local base is skipped, reason "overlay" |
skip-mixed-plain-and-overlay |
A file mixing a plain declaration with an unresolvable overlay is skipped WHOLE, reason "overlay" |
skip-overlay-redeclares-local-base |
A plain declaration AND a same-(type, name) overlay: true redeclaration in the SAME file (a local base exists) is still skipped, reason "overlay" — never silently merged |
skip-structurally-invalid |
A file that cannot be parsed at all (unregistered subtype) is skipped, reason "error" |
crlf-normalized-to-lf |
A CRLF-line-ended input formats to the LF-only canonical form |
bom-stripped |
A UTF-8-BOM-prefixed input parses (BOM stripped) and the output carries no BOM |
mkdir -p fixtures/fmt-conformance/<fixture-name>- Add
input.json. - Decide happy path vs skip, add
expected.jsonorexpected-skip.json. - Run the TS reference runner —
cd server/typescript && bun test packages/metadata/test/fmt-conformance.test.ts— it auto-discovers the new directory. - Port the fixture through C# / Java / Python the same way every other
corpus is ported: run that port's own
fmt-conformance test and fix the port, never the fixture, unless the fixture itself is wrong.