Skip to content

Commit 3a9999d

Browse files
dmealingclaude
andcommitted
docs(spec): update conformance-tests.md for FR5a envelope assertion
Documents the post-FR5a envelope shape for `expected-errors.json`: { errors: [{ code, source: { format, files, jsonPath } }], warnings: [] } The "errors and warnings as sorted sets" section now describes the two-layer comparison: order-independent code-set + ordered per-error envelope deep-equal on `source.format`, `source.files`, `source.jsonPath`. Pre-FR5a legacy fixtures (`[{code}]` arrays) remain accepted by every port's harness — the envelope assertion is skipped, only the code-set runs. The runner pseudocode is updated to show the new envelope branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e682311 commit 3a9999d

1 file changed

Lines changed: 41 additions & 9 deletions

File tree

spec/conformance-tests.md

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,33 @@ Every fixture is a directory under `metaobjects/fixtures/conformance/`. Each fix
2929
│ ├── meta.foo.json
3030
│ └── meta.bar.json # multiple files for overlay/multi-file scenarios
3131
├── expected.json # happy-path: canonical metamodel
32-
├── expected-errors.json # error case: array of {"code": "ERR_*"} objects
32+
├── expected-errors.json # error case: FR5a envelope { errors: [...], warnings: [] }
3333
└── expected-warnings.json # optional: array of warning message strings
3434
```
3535

3636
**Exactly one** of `expected.json` or `expected-errors.json` must be present:
3737
- `expected.json` → the Loader is expected to succeed (no errors). The canonical serialized output of the loaded root MUST deep-equal this file. The canonical shape is the **fused-key form** documented in [`wire-format.md`](wire-format.md) — every node is `{ "<type>.<subType>": <body> }`.
38-
- `expected-errors.json` → the Loader is expected to emit errors. It is a JSON array of objects, each `{ "code": "ERR_*" }` (a stable `ErrorCode`; message text is NOT compared — it is Tier-2 idiomatic per language). The set of emitted error **codes** MUST equal the set of `code` values in this list (compared as sorted sets — order-independent).
38+
- `expected-errors.json` → the Loader is expected to emit errors. Post-FR5a (ADR-0009), the file is an envelope object:
39+
40+
```jsonc
41+
{
42+
"errors": [
43+
{
44+
"code": "ERR_BAD_ATTR_VALUE",
45+
"source": {
46+
"format": "json",
47+
"files": ["meta.users.json"],
48+
"jsonPath": "$['metadata.root'].children[0]['object.entity']..."
49+
}
50+
}
51+
],
52+
"warnings": []
53+
}
54+
```
55+
56+
Per ADR-0009, every port's harness asserts (in declaration order, per error): `code`, `source.format`, `source.files`, and `source.jsonPath`. Error *message text* is NOT compared — only the stable `ErrorCode`; message wording is Tier-2 (idiomatic per language). Other envelope fields (`suggestions`, `fixture`, `node`, `yamlPosition`) are RECOMMENDED-only and are not asserted by the cross-port harness.
57+
58+
The legacy `[{ "code": "ERR_*" }]` array shape remains accepted by every port's parser for backward compat (the envelope assertion is skipped, only the code-set is compared) — pre-FR5a fixtures continue to work without rewrite. The cross-port migration of the corpus to the envelope shape lands in the FR5a coordinated work.
3959

4060
`expected-warnings.json` is optional. When present, the set of warnings MUST equal this list. When absent on a happy-path fixture, warnings are asserted empty.
4161

@@ -83,12 +103,16 @@ These are the **only** reserved structural keys. Everything else inside a body i
83103
- **Children in declaration order**. The order of children inside `children: [...]` reflects authoring order, not alphabetical order. Overlay merge appends; it does NOT re-sort.
84104
- **`@fields` normalization**. Authoring may write `"@fields": "id"` (scalar); canonical form is always the array form `"@fields": ["id"]`.
85105

86-
### Errors and warnings as sorted sets
106+
### Errors and warnings: code-set + envelope-deep-equal
107+
108+
Errors are compared at two layers (per FR5a / ADR-0009):
109+
110+
1. **Code-set check** — sorted sets of `code` values from `expected-errors.json` (envelope or legacy form) vs the loader-emitted codes. Order-independent (mirrors validation-pass dispatch order quirks).
111+
2. **Envelope assertion (envelope-shape fixtures only)** — for every error in declaration order, the harness asserts `code`, `source.format`, `source.files`, and `source.jsonPath` exactly. JSONPath is the canonical form per [ADR-0009](decisions/ADR-0009-loader-error-envelope-and-source-on-node.md) — byte-identical across ports.
112+
113+
Warnings remain compared as **sorted sets of message strings**. Error *message text* is never compared — only the stable `ErrorCode`; message wording is Tier-2 (idiomatic per language).
87114

88-
Errors are compared as **sorted sets of `code` values** (the `code` field of each
89-
`expected-errors.json` object), and warnings as **sorted sets of message strings**, to avoid
90-
ordering issues across validation passes. Each is sorted before comparison. Error *message text*
91-
is never compared — only the stable `ErrorCode`; message wording is Tier-2 (idiomatic per language).
115+
Pre-FR5a fixtures using the legacy `[{"code": "..."}]` array shape get only the code-set check; the envelope assertion is skipped.
92116

93117
## TS conformance runner
94118

@@ -102,7 +126,15 @@ for each subdirectory of fixtures/conformance/:
102126
load result = Loader().loadFromDirectory(inputDir)
103127
104128
if expected-errors.json exists:
105-
assert sorted(result.errors) == sorted(expected-errors.json)
129+
envelope = parse(expected-errors.json) # envelope or legacy array
130+
assert sorted(codes(result.errors)) == sorted(codes(envelope))
131+
if envelope is FR5a-shaped (not legacy array):
132+
for i, (want, got) in enumerate(zip(envelope.errors, result.errors)):
133+
assert want.code == got.code
134+
assert want.source.format == got.source.format
135+
assert want.source.files == got.source.files
136+
assert want.source.jsonPath == got.source.jsonPath
137+
assert len(envelope.warnings) == len(result.warnings)
106138
stop
107139
108140
assert result.errors is empty
@@ -138,7 +170,7 @@ fixture.
138170
2. Add one or more `meta.*.json` files to `input/`.
139171
3. Decide if it's happy-path, error, or warning:
140172
- Happy-path → add `expected.json` (canonical metamodel) and optionally `expected-warnings.json`.
141-
- Error → add `expected-errors.json` (sorted list of error message strings).
173+
- Error → add `expected-errors.json` (FR5a envelope: `{ "errors": [{ "code": "ERR_*", "source": { "format": "...", "files": [...], "jsonPath": "$..." } }], "warnings": [] }`).
142174
4. Run the TS conformance test — the new directory is auto-discovered:
143175
`cd typescript/packages/metadata && bun test test/conformance.test.ts -t "<fixture-name>"`
144176
5. If the test fails with "actual vs expected" diff, you have a choice:

0 commit comments

Comments
 (0)