From 1b854188447b55648efd87b129d7ded20e0fe179 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Mon, 2 Nov 2026 05:32:48 -0500 Subject: [PATCH 1/2] fix(metadata): root-level node resolves a bare @objectRef in its own package (#522); one bump rule for a new attribute (#523) --- AGENTS.md | 2 +- CHANGELOG.md | 8 +++ docs/1.0-readiness.md | 2 +- docs/CONFORMANCE.md | 6 +-- docs/RELEASING.md | 2 +- .../expected-errors.json | 15 ++++++ .../input/meta.json | 14 +++++ .../providers.json | 1 + .../expected.json | 51 +++++++++++++++++++ .../input/meta.json | 32 ++++++++++++ .../providers.json | 1 + .../validation/RegisteredValidation.java | 10 +++- 12 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 fixtures/conformance/error-root-level-field-objectref-unresolved/expected-errors.json create mode 100644 fixtures/conformance/error-root-level-field-objectref-unresolved/input/meta.json create mode 100644 fixtures/conformance/error-root-level-field-objectref-unresolved/providers.json create mode 100644 fixtures/conformance/root-level-field-object-bare-ref/expected.json create mode 100644 fixtures/conformance/root-level-field-object-bare-ref/input/meta.json create mode 100644 fixtures/conformance/root-level-field-object-bare-ref/providers.json diff --git a/AGENTS.md b/AGENTS.md index 1ce9781050..31c4c7fa80 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,7 +71,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken. - **Kotlin** — `codegen-kotlin` (KotlinPoet on JVM): entity + Exposed table + Spring controller + payload + relations + filter allowlist + validator + stored-proc + output-parser generators. `integration-tests-kotlin` runs the persistence-conformance corpus through Exposed against Testcontainers Postgres. **Cross-port conformance corpora** (every port runs the shared corpus): -- Metamodel: `fixtures/conformance/` (421 fixtures; 35 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green. +- Metamodel: `fixtures/conformance/` (423 fixtures; 35 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green. - Render: `fixtures/render-conformance/`. TS / C# / Java / Kotlin / Python byte-identical. - Persistence: `fixtures/persistence-conformance/`. **Query** scenarios run on every port (TS / C# / Java / Kotlin / Python), each provisioning its test DB by executing the committed, TS-produced `canonical/schema.postgres.sql` (Postgres only — Derby dropped for the cross-port query corpus, ADR-0015; the FR-044 `report-*` scenarios also run on SQLite, MySQL and D1 where a port has the engine, see [docs/CONFORMANCE.md](docs/CONFORMANCE.md)). The **migration** scenarios are exercised by **TS only** (TS owns schema migrations). **The corpus now gates WRITES, not just reads (SP-H):** an `op: roundtrip` scenario type INSERTs through each port's runtime/ORM write codec (NOT raw SQL), reads the row back, and asserts the wire-normalized value. The `AllTypes` entity (`roundtrip-all-types.yaml`) carries one field of **every** persistable `field.*` subtype — string/int/long/double/float/decimal/boolean/date/time/timestamp(+tz)/currency/enum/uuid/object — plus an **array-of-VO** `field.object @isArray @storage:jsonb` column (`labels`, written as 2-element / empty-`[]` / single-element arrays across the three rows) — so every subtype write+read (incl. the array-of-value-object jsonb codec) round-trips through every port against Testcontainers PG. (`field.byte`/`field.short`/`field.class` were cut as non-functional registration-only stubs — the matrix tracks only genuinely-supported subtypes; see `fixtures/registry-conformance/README.md` → "Per-subtype write-round-trip matrix".) - API-contract: `fixtures/api-contract-conformance/`. TS / C# / Java / Kotlin / Python all green — each port runs **two lanes**: a hand-rolled reference server AND its **generated** API artifact booted over HTTP (the deployed controller/routes; TS+C# full-stack vs Testcontainers PG, Java/Kotlin/Python generated controller + in-memory repo behind the consumer seam). The generated fan-out found 10 real deployment bugs golden snapshots missed. Three sub-corpora run the **generated lane only, on all five ports** — `write-through/`, `projection/` (F22: a view-only `object.projection` serves GET list + GET by id and answers every write verb with `405 {"error": "method_not_allowed"}`) and `report/` (FR-044: a view-backed `object.report` serves GET list, answers `POST` with that same `405`, and mounts no `/{id}` route). That is deliberate, not a gap: what is under test is whether a port's GENERATOR emits those routes, and a hand-rolled reference server would answer every scenario by construction. The `m2m/` sub-corpus also gates **TPH x M:N together** (base-declared, subtype-declared, abstract-mid-declared, a non-subtype source onto a subtype TARGET, and the cross-subtype source id answering `200 []`) — the two corpora were originally built disjoint (`tph/` had no relationships, `m2m/` no discriminators), which is precisely how that defect class survived. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3631b5903c..b7b19f608f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,14 @@ here.** It now generates `tags: list[str] = []`, from the same four spellings TypeScript accepts, or from a native YAML list such as `default: [a, b]`. A default Python cannot spell as a list is left to the database. +- **Java: a bare `@objectRef` on a field declared directly under `metadata.root` now resolves.** + A root-level `field.object` (the reusable template other fields `extends`) with + `@objectRef: "Address"` failed to load in Java even when `Address` sat in the same package, + while TypeScript loaded it. A root-level node now resolves a bare reference in its own + package, as ADR-0042 specifies, and a real miss still fails with `ERR_UNRESOLVED_OBJECT_REF`. + The error no longer names the loader as the owner of a root-level node. Two new conformance + fixtures (`root-level-field-object-bare-ref`, `error-root-level-field-objectref-unresolved`) + hold every port to it; C#, Python and TypeScript already passed. ## [1.2.0] — 2026-10-30 diff --git a/docs/1.0-readiness.md b/docs/1.0-readiness.md index ced552c54c..b54be142ed 100644 --- a/docs/1.0-readiness.md +++ b/docs/1.0-readiness.md @@ -117,7 +117,7 @@ Status legend: ✅ done · 🔶 in progress · ⬜ not started · ❓ **[RATIFY] (attribute ⇒ PATCH, top-level type ⇒ MINOR, subtype ⇒ PATCH when inert), and Amendment 2 separates the package and metamodel axes. Trigger is *new public surface, not code size*: package **MINOR** only for newly-dependable - surface (codegen output, CLI flag, a newly-supported vocab member); package **PATCH** + surface (codegen-engine surface, CLI flag, a new top-level type); package **PATCH** for bugfix/refactor; the **Metamodel spec version** bumps only on a shared vocabulary/wire change. The fast `7.3→7.7` minor run was breaking *velocity*; decouple + this rule ends the churn. diff --git a/docs/CONFORMANCE.md b/docs/CONFORMANCE.md index 73b8182683..d7e9157567 100644 --- a/docs/CONFORMANCE.md +++ b/docs/CONFORMANCE.md @@ -26,7 +26,7 @@ regenerate with `ls -d fixtures//*/ | wc -l` for directory-shaped corpor | Corpus | Fixtures | TS | Java | Kotlin | C# | Python | |---|---|---|---|---|---|---| -| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 421 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ | +| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 423 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ | | [`fixtures/yaml-conformance/`](../fixtures/yaml-conformance/) | 16 | 16 / 16 | 15 / 16 (1 ledgered: `yaml-quoted-leading-zero` — Java pipeline strips quotes off `"007"`) | inherits via Java | 15 / 16 (1 ledgered: `error-yaml-coerced-hex-in-string` — YamlDotNet doesn't coerce `0xFF`) | 16 / 16 | | [`fixtures/verify-conformance/`](../fixtures/verify-conformance/) | 45 | ✓ | ✓ | inherits via Java | ✓ | ✓ | | [`fixtures/verify-strict-conformance/`](../fixtures/verify-strict-conformance/) | 1 | ✓ | — | — | — | ✓ | @@ -285,7 +285,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker. ## Fixture-to-doc mapping -### `fixtures/conformance/` — metamodel loader + canonical serializer (421) +### `fixtures/conformance/` — metamodel loader + canonical serializer (423) | Fixture prefix | Feature doc | |---|---| @@ -611,7 +611,7 @@ files into a real Cube. ## Orphaned fixtures (tested but not yet documented) -The fixtures in the nine corpora mapped above (metamodel 421 + yaml 16 + verify 45 +The fixtures in the nine corpora mapped above (metamodel 423 + yaml 16 + verify 45 + render 18 + persistence 59 + api-contract 119 + source-resolution 25 + scope 10 + dependency 37) each map to a feature doc, and so do the two requirement corpora, `shared-model-conformance` and the `cube-model` corpus, whose case lists live in their own READMEs. None are orphaned today. The remaining diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 4a8f6df227..5006db9ed3 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -266,7 +266,7 @@ what they render is generator output and does not.) | Codegen **ENGINE** public surface **additive** (new `render*` primitive, new `GenContext` field, optional param) | new `render*` primitive | PATCH (MINOR if it headlines a feature release) | MINOR | | Codegen **ENGINE** public surface **breaking**, or changed **CLI semantics** | `relativeModuleSpecifier` +required param | **MINOR** | MAJOR | | A **runtime library**'s public surface (`runtime-ts`, `render`, OMDB, ObjectManager, web client) — additive / breaking | a new mount option / a removed export | PATCH / **MINOR** | MINOR / MAJOR | -| New **attribute** on an existing type/subtype | `@intValueMap`, `@lenient`, `@maxTokens` | **PATCH** | MINOR | +| New **attribute** on an existing type/subtype | `@intValueMap`, `@lenient`, `@maxTokens` | **PATCH** | **PATCH** | | New **subtype** of an existing type | `field.uri`, `index.lookup`, `attr.intMap` | **PATCH** when inert (see the vocabulary rule below); **MINOR** when it changes existing metadata's meaning/output, narrows something previously permitted, or headlines a feature | MINOR | | New top-level metadata **type** | `requirement.*`, `index.*`, `api.*` | **MINOR** | MINOR | | **Breaking** metamodel-vocabulary change (retire an attr/subtype/type; narrow what is permitted) | FR-037 `@readOnly`, FR-038 `@verifiedBy`, ADR-0052 `@promptStyle` re-homing | **MINOR** (pre-1.0 MINOR *is* the breaking slot) | **`metamodelVersion` MAJOR**, package MINOR (ADR-0035 Am. 2) | diff --git a/fixtures/conformance/error-root-level-field-objectref-unresolved/expected-errors.json b/fixtures/conformance/error-root-level-field-objectref-unresolved/expected-errors.json new file mode 100644 index 0000000000..3c510ca24d --- /dev/null +++ b/fixtures/conformance/error-root-level-field-objectref-unresolved/expected-errors.json @@ -0,0 +1,15 @@ +{ + "errors": [ + { + "code": "ERR_UNRESOLVED_OBJECT_REF", + "source": { + "format": "json", + "files": [ + "meta.json" + ], + "jsonPath": "$['metadata.root'].children[0]['field.object']" + } + } + ], + "warnings": [] +} diff --git a/fixtures/conformance/error-root-level-field-objectref-unresolved/input/meta.json b/fixtures/conformance/error-root-level-field-objectref-unresolved/input/meta.json new file mode 100644 index 0000000000..e0afebb868 --- /dev/null +++ b/fixtures/conformance/error-root-level-field-objectref-unresolved/input/meta.json @@ -0,0 +1,14 @@ +{ + "metadata.root": { + "package": "acme::shop", + "children": [ + { + "field.object": { + "name": "AddrTemplate", + "abstract": true, + "@objectRef": "Missing" + } + } + ] + } +} diff --git a/fixtures/conformance/error-root-level-field-objectref-unresolved/providers.json b/fixtures/conformance/error-root-level-field-objectref-unresolved/providers.json new file mode 100644 index 0000000000..f9bb435a37 --- /dev/null +++ b/fixtures/conformance/error-root-level-field-objectref-unresolved/providers.json @@ -0,0 +1 @@ +["metaobjects-core-types","metaobjects-db"] diff --git a/fixtures/conformance/root-level-field-object-bare-ref/expected.json b/fixtures/conformance/root-level-field-object-bare-ref/expected.json new file mode 100644 index 0000000000..b8f056a7c3 --- /dev/null +++ b/fixtures/conformance/root-level-field-object-bare-ref/expected.json @@ -0,0 +1,51 @@ +{ + "metadata.root": { + "package": "acme::shop", + "children": [ + { + "object.value": { + "name": "Address", + "children": [ + { + "field.string": { + "name": "street" + } + }, + { + "field.string": { + "name": "city" + } + } + ] + } + }, + { + "field.object": { + "name": "AddrTemplate", + "package": "acme::shop", + "abstract": true, + "@objectRef": "acme::shop::Address" + } + }, + { + "object.value": { + "name": "Contact", + "children": [ + { + "field.string": { + "name": "name" + } + }, + { + "field.object": { + "name": "billing", + "extends": "acme::shop::AddrTemplate" + } + } + ] + } + } + ] + } +} + diff --git a/fixtures/conformance/root-level-field-object-bare-ref/input/meta.json b/fixtures/conformance/root-level-field-object-bare-ref/input/meta.json new file mode 100644 index 0000000000..39fbb18e57 --- /dev/null +++ b/fixtures/conformance/root-level-field-object-bare-ref/input/meta.json @@ -0,0 +1,32 @@ +{ + "metadata.root": { + "package": "acme::shop", + "children": [ + { + "object.value": { + "name": "Address", + "children": [ + { "field.string": { "name": "street" } }, + { "field.string": { "name": "city" } } + ] + } + }, + { + "field.object": { + "name": "AddrTemplate", + "abstract": true, + "@objectRef": "Address" + } + }, + { + "object.value": { + "name": "Contact", + "children": [ + { "field.string": { "name": "name" } }, + { "field.object": { "name": "billing", "extends": "AddrTemplate" } } + ] + } + } + ] + } +} diff --git a/fixtures/conformance/root-level-field-object-bare-ref/providers.json b/fixtures/conformance/root-level-field-object-bare-ref/providers.json new file mode 100644 index 0000000000..f9bb435a37 --- /dev/null +++ b/fixtures/conformance/root-level-field-object-bare-ref/providers.json @@ -0,0 +1 @@ +["metaobjects-core-types","metaobjects-db"] diff --git a/server/java/metadata/src/main/java/com/metaobjects/loader/validation/RegisteredValidation.java b/server/java/metadata/src/main/java/com/metaobjects/loader/validation/RegisteredValidation.java index 99cf268c8c..c9656f581c 100644 --- a/server/java/metadata/src/main/java/com/metaobjects/loader/validation/RegisteredValidation.java +++ b/server/java/metadata/src/main/java/com/metaobjects/loader/validation/RegisteredValidation.java @@ -40,7 +40,10 @@ private static void walk(MetaData node, MetaDataRegistry registry, ValidationCon // BARE refs against it. Nested nodes carry no package of their own, so they inherit the // enclosing object's. Mirrors TS validation-registry.ts. String pkg = referrerPkg; - if (node instanceof MetaObject) { + // A root-level node of any type (e.g. an abstract field.object template that other + // fields extend) is a top-level declaration too: it carries its own package and its + // bare refs resolve in it, not against the empty context. + if (node instanceof MetaObject || node.getParent() instanceof MetaRoot) { String p = node.getPackage(); pkg = (p != null && !p.isEmpty()) ? p : referrerPkg; } @@ -64,7 +67,10 @@ private static void walk(MetaData node, MetaDataRegistry registry, ValidationCon MetaObject target = ctx.symbols().resolveObject(entityRef, pkg); // Qualify the node name with its owning entity (e.g. "Order.items") so the // error is locatable from the message alone, not just the source envelope. - String qname = (node.getParent() != null && node.getParent().getShortName() != null + // A root-level node has no owning entity: the parent is the loader/root, whose + // name must not be printed as an owner. + String qname = (node.getParent() != null && !(node.getParent() instanceof MetaRoot) + && node.getParent().getShortName() != null && !node.getParent().getShortName().isEmpty()) ? node.getParent().getShortName() + "." + node.getShortName() : node.getShortName(); From f569c7a14630a9f7d022bd36bad8b4f2e3eda58e Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Mon, 2 Nov 2026 05:44:03 -0500 Subject: [PATCH 2/2] chore(site): refresh payload fixture count --- examples/showcase/site-payload.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/showcase/site-payload.json b/examples/showcase/site-payload.json index 82e51fcbdd..6e9ec3f63d 100644 --- a/examples/showcase/site-payload.json +++ b/examples/showcase/site-payload.json @@ -7,7 +7,7 @@ "metamodel": "1.2" }, "counts": { - "fixtures": 421, + "fixtures": 423, "corpora": 35, "baseTypes": 17 },