Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/1.0-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions docs/CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ regenerate with `ls -d fixtures/<corpus>/*/ | 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 | ✓ | — | — | — | ✓ |
Expand Down Expand Up @@ -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 |
|---|---|
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/site-payload.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"metamodel": "1.2"
},
"counts": {
"fixtures": 421,
"fixtures": 423,
"corpora": 35,
"baseTypes": 17
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"errors": [
{
"code": "ERR_UNRESOLVED_OBJECT_REF",
"source": {
"format": "json",
"files": [
"meta.json"
],
"jsonPath": "$['metadata.root'].children[0]['field.object']"
}
}
],
"warnings": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"metadata.root": {
"package": "acme::shop",
"children": [
{
"field.object": {
"name": "AddrTemplate",
"abstract": true,
"@objectRef": "Missing"
}
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["metaobjects-core-types","metaobjects-db"]
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
}
}
]
}
}

Original file line number Diff line number Diff line change
@@ -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" } }
]
}
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["metaobjects-core-types","metaobjects-db"]
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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();
Expand Down
Loading