|
| 1 | +# FR: Actionable JSON-shape loader errors (path + did-you-mean + fixture link) |
| 2 | + |
| 3 | +**Status:** Design proposal — needs brainstorm before implementation |
| 4 | +**Date:** 2026-05-25 |
| 5 | +**Scope:** Cross-language — every port's metadata loader raises JSON-shape validation |
| 6 | +errors using the same `ERR_*` codes from the conformance corpus. This FR proposes a |
| 7 | +cross-port UX improvement. |
| 8 | +**Origin:** A downstream consumer onboarding to 0.6.0 reported that the "first hour with |
| 9 | +MO" was dominated by entity-JSON-shape iteration: typos like `enum` instead of |
| 10 | +`field.enum`, or forgotten `@` prefixes on attributes, produced generic |
| 11 | +"unknown field key X" errors with no line number, no suggestion, and no link to a |
| 12 | +working example. |
| 13 | + |
| 14 | +## Goal |
| 15 | + |
| 16 | +Make loader errors **actionable**: when an authored `metaobjects/*.json` doesn't validate, |
| 17 | +the error should tell the user: |
| 18 | + |
| 19 | +1. **Where in the JSON the problem is** — the JSON path (`metadata.root.children[2].object.entity.children[1]`). |
| 20 | +2. **A "did you mean" suggestion** — Levenshtein-nearest known key (`enum` → `field.enum`). |
| 21 | +3. **A link to a working example** — the most-relevant conformance fixture. |
| 22 | + |
| 23 | +All three are existing knowledge in the loader pipeline; surfacing them on the raised |
| 24 | +error reduces "time to fix" from minutes-of-grepping to seconds-of-reading. |
| 25 | + |
| 26 | +## Why this is cross-language |
| 27 | + |
| 28 | +The conformance corpus defines a fixed set of `ERR_*` codes (`ERR_UNKNOWN_TYPE`, |
| 29 | +`ERR_UNKNOWN_SUBTYPE`, `ERR_BAD_ATTR_VALUE`, `ERR_MISSING_REQUIRED_ATTR`, |
| 30 | +`ERR_RESERVED_ATTR`, etc.) that **every port emits identically**. The error code itself |
| 31 | +is cross-language. The *message text* attached to each error is currently per-port and |
| 32 | +varies in quality. This FR proposes: |
| 33 | + |
| 34 | +1. A cross-language **schema** for the error payload (path, suggestion, fixture link). |
| 35 | +2. Per-port **implementation** of populating that payload — Tier 3 (idiomatic) freedom |
| 36 | + on the exact rendering, Tier 1 (invariant) commitment to the fields. |
| 37 | + |
| 38 | +This needs a brainstorm because the error-payload schema decision will affect Java, |
| 39 | +C#, Python, and TypeScript loaders simultaneously. |
| 40 | + |
| 41 | +## Current state by port (rough) |
| 42 | + |
| 43 | +| Port | Path info? | Did-you-mean? | Fixture link? | |
| 44 | +|---|---|---|---| |
| 45 | +| TypeScript (`@metaobjectsdev/metadata`) | Partial (some errors include node names) | No | No | |
| 46 | +| C# (`MetaObjects.Metadata`) | Partial (similar) | No | No | |
| 47 | +| Java (`server/java/metadata`) | Partial | No | No | |
| 48 | +| Python (`server/python/src/metaobjects`) | Partial | No | No | |
| 49 | + |
| 50 | +None of the four ports raises errors with the full information today, but all four |
| 51 | +have the information available inside the loader pipeline at the point of failure. |
| 52 | + |
| 53 | +## Brainstorm topics (open) |
| 54 | + |
| 55 | +### 1. Error payload schema — cross-language fields |
| 56 | + |
| 57 | +Strawman: |
| 58 | + |
| 59 | +```jsonc |
| 60 | +{ |
| 61 | + "code": "ERR_BAD_ATTR_VALUE", |
| 62 | + "message": "Attribute '@values' on 'field.enum[ColorEnum]' must be a non-empty string array; got string", |
| 63 | + "path": "metadata.root.children[0].object.entity.children[1].field.enum.@values", |
| 64 | + "suggestions": [ |
| 65 | + "Did you mean to use the inline form '[...]' instead of a single string?", |
| 66 | + "See fixtures/conformance/enum-inline/input/meta.enums.json for the canonical shape." |
| 67 | + ], |
| 68 | + "fixture": "enum-inline", |
| 69 | + "node": { "type": "field", "subtype": "enum", "name": "ColorEnum" } |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +Decisions to brainstorm: |
| 74 | +- What's the minimum set of fields every port must populate? (`code`, `message`, `path` |
| 75 | + probably; `suggestions`, `fixture`, `node` optional?) |
| 76 | +- How is the payload surfaced — exception object, error response, log line, all three? |
| 77 | + Per-port idiomatic. |
| 78 | +- How does the conformance harness verify the payload? Today it only checks |
| 79 | + the `code`. Adding payload checks raises corpus expectations. |
| 80 | + |
| 81 | +### 2. "Did you mean" — distance threshold + key sources |
| 82 | + |
| 83 | +Levenshtein distance is the obvious metric. Open questions: |
| 84 | +- Threshold (≤ 2? ≤ 3? proportional to key length?). |
| 85 | +- Key universe per error type: |
| 86 | + - `ERR_UNKNOWN_TYPE` → list of registered type names. |
| 87 | + - `ERR_UNKNOWN_SUBTYPE` → list of registered subtypes for the type. |
| 88 | + - `ERR_RESERVED_ATTR` → the reserved-keyword set. |
| 89 | + - `ERR_BAD_ATTR_VALUE` for missing `@` prefix → look in the registered attr names. |
| 90 | +- Single best suggestion vs. ranked list of up to N? |
| 91 | + |
| 92 | +### 3. Fixture link — how does the loader know which fixture is canonical? |
| 93 | + |
| 94 | +Two approaches: |
| 95 | + |
| 96 | +**(a) Lookup table.** A registry mapping `(error_code, type, subtype)` → fixture name. |
| 97 | +Lives at `fixtures/conformance/ERROR_TO_FIXTURE.json`, regenerated by tooling. Each |
| 98 | +port loads it. |
| 99 | + |
| 100 | +**(b) Heuristic.** "Look for a fixture whose name contains the type + subtype." Fragile; |
| 101 | +not all fixtures follow that convention. |
| 102 | + |
| 103 | +Recommendation hypothesis: **(a)** — explicit, deterministic, regeneratable. |
| 104 | + |
| 105 | +### 4. Cross-port rollout |
| 106 | + |
| 107 | +This is a per-port implementation effort once the schema is fixed. Suggested order: |
| 108 | +1. Schema design + an ADR in `spec/decisions/`. |
| 109 | +2. TypeScript impl first (largest active consumer base). |
| 110 | +3. C# next (active port, full surface). |
| 111 | +4. Python + Java in parallel as those ports' conformance ledgers close. |
| 112 | + |
| 113 | +## Out of scope (until brainstorm) |
| 114 | + |
| 115 | +- Rewriting the loader pipeline. The payload-surfacing change should slot into existing |
| 116 | + error-raising sites with minimal pipeline disturbance. |
| 117 | +- Localization of error message text. Strings stay English. |
| 118 | +- IDE / LSP integration that consumes the payload to render diagnostics inline. That's |
| 119 | + a follow-up consumer — getting the payload right comes first. |
| 120 | + |
| 121 | +## Open questions |
| 122 | + |
| 123 | +1. Does the conformance harness gain payload checks, or do those stay per-port? |
| 124 | +2. Should the error payload field names match TypeScript convention (camelCase) across |
| 125 | + all ports, or each port uses its own (`snake_case` for Python, `PascalCase` for C#)? |
| 126 | + The JSON-on-the-wire form is identical regardless. |
| 127 | +3. Is there a desire for a `--strict-author` mode that raises *every* deviation rather |
| 128 | + than the first one? Today's loader is fail-fast; multi-error reporting is a separate |
| 129 | + shape. |
| 130 | + |
| 131 | +## Related work |
| 132 | + |
| 133 | +- The Documentation Provider (FR-004 family) already produces structured XML-doc and |
| 134 | + COMMENT-ON output from metadata `description` / `notes` attrs. That's downstream |
| 135 | + (output), not upstream (input authoring); the schemas are unrelated, but the cross- |
| 136 | + port discipline is the same. |
| 137 | +- ADR-0006 (AI-first YAML authoring) lowers the discoverability bar somewhat by accepting |
| 138 | + sigil-free attrs, but `meta.json` authoring is the canonical interchange and still |
| 139 | + needs first-class error UX. |
0 commit comments