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/` (454 fixtures; 38 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/` (455 fixtures; 38 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
41 changes: 25 additions & 16 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ _npm `1.3.0` (full lockstep across all 14 `@metaobjectsdev/*` publish candidates

**`metamodelVersion` moves to `1.3`.** This release adds vocabulary (`field.duration`,
`validator.exactlyOne`, `@constraintName`, `@condition`, `@maxLength` on `field.enum`,
`@sequenceStart`, `@sequenceName`, `@flattenedColumnPrefix`, the `pascal` column rule, and
`@dbSetName`, a C#-only attr) and nothing that loaded before stops loading. The registry gains two
`@sequenceStart`, `@sequenceName`, `@flattenedColumnPrefix`, the `pascal` column rule and
`@collectionName`) and nothing that loaded before stops loading. The registry gains two
fields on a type or attr entry, carried into the registry manifest: `implementedIn` (cross-port
vocabulary only the named ports implement so far, e.g. the `view.*` controls, `["typescript"]`) and
`ports` (vocabulary only the named ports ever act on, e.g. `@dbSetName`, `["csharp"]`, and
`@objectAdapter`, `["java", "kotlin"]`: the JVM runtime both share acts on it). The manifest's row
`ports` (vocabulary only the named ports ever act on, e.g. `@objectAdapter`, `["java", "kotlin"]`:
the JVM runtime both share acts on it). The manifest's row
set grows by the `implementedIn` rows that used to be carved out. A port that reports `1.2` does not
know that vocabulary; upgrade every port together.

Expand Down Expand Up @@ -50,6 +50,8 @@ Each item below is described in full under Added, Changed or Fixed; this list is
- **`@sequenceStart` records a start; in 1.3.0 it does not move an existing counter.** A later
release is planned to move a counter that sits below the declared start forward to it, never
backward.
- **`@dbSetName` is `@collectionName`.** The attr was never published under its first name, so
there is no alias: a model that set `@dbSetName` in a pre-release build renames it.
- **`metamodelVersion` is `1.3`.** Upgrade every port together.

### Added
Expand Down Expand Up @@ -83,22 +85,29 @@ Each item below is described in full under Added, Changed or Fixed; this list is
canonical grows by those rows. `meta types` marks them `[ts; pending elsewhere]` / `[java;
pending elsewhere]` (it used to say `[ts-only]`, "the cross-port contract does not carry it"; its
JSON change is under Changed), and the metamodel docs now cover the view controls.
`@objectAdapter` joins `@dbSetName` as port-scoped (`"ports": ["java", "kotlin"]`). C#, Java,
`@objectAdapter` is port-scoped (`"ports": ["java", "kotlin"]`, below). C#, Java,
Kotlin and Python now register the `metaobjects-ui-web` attrs (`view.textarea @rows` and the
`view.image` attrs), and C# and Python register `@object` / `@objectAdapter`, so a model that
uses any of them loads strictly in every port. Gated by the conformance fixtures
`view-kind1-controls`, `object-kind1-jvm-binding-attrs` and
`error-kind1-view-unregistered-attr`, and by the registry canonical.
- **Port-scoped vocabulary, and `@dbSetName`.** A registry attr may name the ports
that act on it (`"ports": ["csharp"]` on its `spec/metamodel` entry, carried into the registry
- **Port-scoped vocabulary.** A registry attr may name the ports
that act on it (`"ports": ["java", "kotlin"]` on its `spec/metamodel` entry, carried into the registry
manifest). Every port registers and type-checks it, so a model that carries it loads under every
port's strict loader; a port not named does nothing with it. An attr the registry does not carry
still fails, and a wrong value type fails in every port (Java now refuses a list given to a scalar
string attr, as the other ports did). `meta types` marks such an attr `[csharp-only]` and its JSON
carries `ports`. The first one is `@dbSetName`: the EF Core DbSet property name the generated C#
context gives an `object.entity`, `object.projection` or `object.report` (`"@dbSetName":
"Diagnosis"`), in place of the derived plural; the routes, the reverse finders and the api
reference read the property name through it.
string attr, as the other ports did). `meta types` marks such an attr `[java+kotlin]` (one port:
`[csharp-only]`) and its JSON carries `ports`. `@objectAdapter` is the one there is.
- **`@collectionName`, and the `naming.collections` pluralizer.** An `object.entity`,
`object.projection` or `object.report` may name its collection in its own casing
(`"@collectionName": "CaseHistory"`); it need not be a grammatical plural. The generated C#
context uses it as the DbSet property name in place of the derived plural, and the routes, the
reverse finders and the api reference read the property through it. Absent, the project's
pluralizer derives the name: `"naming": { "collections": "standard" }` (the default) or `"none"`
in `.metaobjects/config.json`, or one the application registers (`registerPluralizer` in
TypeScript, `NamingStrategies.RegisterPluralizer` in C#, `NamingStrategies.registerPluralizer` in
Java, `register_pluralizer` in Python). Every port registers the attr and reads the key, and
refuses an unknown pluralizer when it reads the config.

- **An int-backed enum can be an array.** `@intValueMap` together with `isArray: true` (the
map on the field, or inherited from a shared enum) now loads in every port instead of
Expand Down Expand Up @@ -635,8 +644,8 @@ Each item below is described in full under Added, Changed or Fixed; this list is
- **C#: one generated context maps each relation to one object.** Two concrete
objects of one context that resolve to the same table, view, procedure or function in the same
`@schema` are refused with `ERR_DUPLICATE_SQL_NAME`, naming both; before, they silently shared it
once `@dbSetName` split their DbSet names. The same name in two schemas is two relations, and an
object that extends another and inherits its source is one EF hierarchy. A `@dbSetName` that is not
once `@collectionName` split their DbSet names. The same name in two schemas is two relations, and an
object that extends another and inherits its source is one EF hierarchy. A `@collectionName` that is not
a C# identifier, or names `Database`, `Model`, `ChangeTracker` or `ContextId`, is refused by name.
- **TypeScript: the collision refusal checks only the names the run emits.** The pluralized module
names are checked when entity modules, queries or a UI tier are wired, and the REST segments when
Expand Down Expand Up @@ -682,7 +691,7 @@ Each item below is described in full under Added, Changed or Fixed; this list is
Changed.
- **A generated name collides only in the scope that carries it**. The run-wide
refusal of two objects whose short names pluralize alike is gone. C#: DbSet names are unique
within one generated context (the error says "rename one, or set @dbSetName"), route segments
within one generated context (the error says "rename one, or set @collectionName"), route segments
within one routes entry, reverse finders within their class. TypeScript: the pluralized names
within the entity modules' scope (one directory under the flat layout, one per package under
the package layout), REST segments across the app. Python and JVM keep one run-wide scope,
Expand Down Expand Up @@ -858,7 +867,7 @@ Each item below is described in full under Added, Changed or Fixed; this list is
cross-port wire contract and keeps the shared rule (`/api/diagnosises`) until every port
changes together. An adopter that aliased the old DbSet name can drop the alias. **Upgrade
note:** code that names `Diagnosises` or `FindDiagnosisesBy…` stops compiling; use the new name,
or set `"@dbSetName": "Diagnosises"` on the entity to keep the old one.
or set `"@collectionName": "Diagnosises"` on the entity to keep the old one.

- **TypeScript: a derived flattened `field.object` no longer produces flat member keys (#524
follow-up).** A flattened field that carries an `origin.*` owns no column (the Drizzle table and
Expand Down
12 changes: 8 additions & 4 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) | 454 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 455 | ✓ | ✓ | 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 @@ -148,14 +148,17 @@ canonical (`fixtures/registry-conformance/expected-registry.json`).
- `view.image`, and its `@accept`, `@aspectRatio`, `@maxBytes`, `@maxEdge`, `@store`
- *Implemented in Java only* (`implementedIn: ["java"]`): `object.* @object` (ADR-0001), the
native class an object binds to. Java's runtime acts on it; Java's own behaviour is unchanged.
- *Implemented in C# only* (`implementedIn: ["csharp"]`): `object.* @collectionName`, the
collection name the generated DbSet takes. Every port registers it and reads the `naming.collections`
key that names the default pluralizer; C# codegen does not apply that pluralizer yet.
- *Loads strictly in every port.* Each port registers the `metaobjects-ui-web` provider
(`view.textarea @rows`, the `view.image` attrs) and `@object` / `@objectAdapter` on
`object.*`, loads all three fixtures strictly, and emits `implementedIn` in its manifest, which
byte-matches the canonical in all five ports (`KindOneVocabularyTests` in C#;
`registry-conformance.test.ts` in TypeScript; `RegistryManifestConformanceTest` in Java and
Kotlin; `test_registry_conformance.py` in Python).
- *Port-scoped, for comparison* (`ports`, language-specific by nature, never "pending"):
`@dbSetName` (`["csharp"]`) and `@objectAdapter` (`["java", "kotlin"]`).
`@objectAdapter` (`["java", "kotlin"]`).

**Native index vocabulary and `@description` in the ORM models.** `meta migrate` creates
every index (`identity.secondary`, `index.lookup`, a field's `@unique`, with `@where`, `@using`,
Expand Down Expand Up @@ -351,7 +354,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker.

## Fixture-to-doc mapping

### `fixtures/conformance/` — metamodel loader + canonical serializer (454)
### `fixtures/conformance/` — metamodel loader + canonical serializer (455)

| Fixture prefix | Feature doc |
|---|---|
Expand All @@ -376,6 +379,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker.
| `requirement-*`, `error-unknown-attr-requirement` | [features/requirements.md](features/requirements.md) (vocabulary only — the gate has its own corpus, `requirement-check-conformance/`; see "Split coverage" above) |
| `reporting-*`, `error-dimension-*`, `error-measure-*`, `error-ratio-*`, `error-segment-*`, `error-report-*`, `error-relative-date-*` | [features/reporting.md](features/reporting.md) (the `dimension` / `measure` / `segment` / `object.report` vocabulary and its load-time rules; design in the [FR-044 spec](superpowers/specs/2026-10-02-fr-044-core-reporting-design.md)) |
| `provider-extension-*`, `provider-custom-vocabulary-*` | [features/extending-with-providers.md](features/extending-with-providers.md) (a project's own provider: a new subtype plus attributes on an existing type load with it and are refused without it, every port writing the provider in its own language) |
| `object-collection-name` | [features/migrations/physical-naming.md](features/migrations/physical-naming.md) (`@collectionName` and the `collections` pluralizer) |
| `smoke-empty-metadata` | [features/entities.md](features/entities.md) |

### `fixtures/yaml-conformance/` (16)
Expand Down Expand Up @@ -712,7 +716,7 @@ JSON and verify payload feeds it unchanged.

## Orphaned fixtures (tested but not yet documented)

The fixtures in the nine corpora mapped above (metamodel 454 + yaml 16 + verify 45
The fixtures in the nine corpora mapped above (metamodel 455 + yaml 16 + verify 45
+ render 18 + persistence 62 + api-contract 123 + source-resolution 25 + scope 10 +
dependency 47) each map to a feature doc, and so do the two requirement corpora, `shared-model-conformance`, the
`cube-model` corpus and `explorer-conformance`, whose case lists live in their own READMEs. None are orphaned today. The remaining
Expand Down
6 changes: 5 additions & 1 deletion docs/features/migrations/physical-naming.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ every runtime `ObjectManager` and `verify` read it, so they always produce the s
|---|---|---|---|
| `columns` | `snake_case`, `literal`, `kebab-case`, `pascal`, or a name your application registered | a field with no `@column` | `snake_case` |
| `tables` | `snake_case`, `literal`, or a name your application registered | a source with no `@table`/`@view`/`@proc`/`@function`: its `name`, else the object's name made plural | `snake_case` |
| `collections` | `standard`, `none`, or a name your application registered | an object with no `@collectionName`: the collection name the generated C# DbSet takes | `standard` |

An unknown key fails when the file is read, and so does a name no registered strategy answers.
Index and constraint names are unchanged.
Expand Down Expand Up @@ -61,7 +62,10 @@ and is `pascal` otherwise:

Table rules register the same way (`namingStrategies.tables`, `RegisterTables`, `registerTables`,
`register_table_naming_rule`) and name a source with no physical name; the entity-name fallback
is still made plural after the rule. A built-in name cannot be replaced. Register the same
is still made plural after the rule. Pluralizers register the same way (`registerPluralizer`,
`RegisterPluralizer`, `registerPluralizer`, `register_pluralizer`): an object name in, in its
declared casing, its collection name out. `standard` is the cross-port plural and `none` keeps the
name as it is. A built-in name cannot be replaced. Register the same
strategy in every port that reads the database, because each port resolves the names itself.
Every port looks a strategy up by name each time it applies one, so registering again under the
same name replaces it for everything that runs afterwards. A test that registers its own calls
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.3"
},
"counts": {
"fixtures": 454,
"fixtures": 455,
"corpora": 38,
"baseTypes": 17
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"metadata.root": {
"package": "acme::clinical",
"children": [
{ "object.entity": { "name": "Diagnosis", "@dbSetName": ["Diagnosis"], "children": [
{ "object.entity": { "name": "Diagnosis", "@objectAdapter": ["com.acme.clinical.DiagnosisAdapter"], "children": [
{ "field.long": { "name": "id" } },
{ "identity.primary": { "name": "pk", "@fields": ["id"] } }
] } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{
"object.entity": {
"name": "Diagnosis",
"@dbSetName": "Diagnosis",
"@collectionName": "Diagnosis",
"children": [
{
"source.rdb": {
Expand Down Expand Up @@ -51,7 +51,7 @@
{
"object.projection": {
"name": "DiagnosisSummary",
"@dbSetName": "DiagnosisSummary",
"@collectionName": "DiagnosisSummary",
"children": [
{
"source.rdb": {
Expand All @@ -70,7 +70,7 @@
{
"object.report": {
"name": "DiagnosisCounts",
"@dbSetName": "DiagnosisCounts",
"@collectionName": "DiagnosisCounts",
"@dimensions": [
"code"
],
Expand Down
Loading
Loading