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
The table of contents is too big for display.
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; 34 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/` (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.
- 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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ here.**

### Added

- **Contract emitters: JSON Schema and OpenAPI generators (FR-022).** `jsonSchemaFile()`
(`contract-schema`) writes JSON Schema 2020-12 for each object's read, create and update shapes, and
a strict structured-output subset with `profile: "strict"`. `openApiFile()` (`openapi`) writes one
OpenAPI 3.1 document for the generated REST surface: the routes, the filter, sort and paging
parameters, the error envelope, and those shapes as components. The output follows the REST contract
and the `wire` setting, and a project on any port runs them through the Node `meta` CLI. They are
reference helpers: opt-in, listed by `meta gen --list`, ejectable and gated by `verify --codegen`
(ADR-0034 Amendment 10). Protobuf is not built. See `docs/features/contract-emitters.md`.
- **A recipe for checking an API change against the last release.** Emit the document at the last tag
and at `HEAD`, then run oasdiff. See `docs/recipes/check-api-against-last-release.md`.
- **A `pydantic` extra on the Python package.** `pip install "metaobjects[pydantic]"` installs
`pydantic>=2.8`, the minimum the generated Python models need, so an adopter can get the enforced
floor with the package. The base install is unchanged.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,12 @@ roadmap item. The fifth has been dogfooded on maintainer-owned projects only, an
sixth ships two libraries at their own stability labels:

1. **Codegen** *(generators you write and own, in every port)* — on the core, you build
the generators your application needs: OpenAPI, JSON Schema, Zod, DTOs, a client,
docs — anything the model describes. A generator is a name plus a function from the
the generators your application needs: Zod, DTOs, a client, docs — anything the
model describes. A generator is a name plus a function from the
model to files, and `verify` gates it with nothing to register; `meta generator new
<name>` scaffolds a working one on TypeScript, and
[Write your own generator](docs/recipes/write-your-own-generator.md) has every port's
20-line shape plus JSON Schema and OpenAPI examples to copy. The reference generators
20-line shape plus worked examples to copy. The reference generators
(Drizzle/Zod + Fastify for TS, Spring REST + DTO + repository for Java, `data class` +
Exposed for Kotlin, EF Core entity class + ASP.NET routes for C#, Pydantic + FastAPI for
Python) are starting points: run one as shipped when it already emits what you want, or
Expand Down
11 changes: 7 additions & 4 deletions agent-context/skills/metaobjects-codegen/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: metaobjects-codegen
description: Use when you need ANY output derived from the MetaObjects model — write your own generator (OpenAPI, JSON Schema, Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration.
description: Use when you need ANY output derived from the MetaObjects model — write your own generator (Zod, DTOs, a client, docs, anything) or eject a reference one — and when configuring or running code generation: editing the gen config (metaobjects.config.ts) to choose generators, targets or the SQL dialect (e.g. postgres to sqlite), the gen command, and hand-edit-preserving regeneration.
---

# MetaObjects code generation
Expand Down Expand Up @@ -77,7 +77,7 @@ in order and every change that needs action.
## Need an output? Write the generator — this is the primary path

**When.** You need an output the model fully describes and no reference generator emits —
or none emits it the way this application needs it. OpenAPI, JSON Schema, Zod schemas,
or none emits it the way this application needs it. Zod schemas,
DTOs or a client for a framework nothing here ships, a service layer in the house style,
a data dictionary, a search-index mapping, test fixtures. That is a generator, and you
write it now, in this change — once it passes the cost test in `references/owning-generators.md`
Expand Down Expand Up @@ -146,9 +146,12 @@ without typechecking, so a wrong accessor runs silently.
Make it deterministic: the same model must give the same bytes, or verify reports drift.

Every port's minimal generator (each run through gen, verify and a convicted model
change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples and not a
product surface — are in the guide "Write your own generator":
change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, examples of writing a
generator and not a product surface — are in the guide "Write your own generator":
<https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/write-your-own-generator.md>.
For the documents themselves, the stock `contract-schema` and `openapi` generators emit them, from
1.2.1 (run through the Node `meta` CLI on any port):
<https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/contract-emitters.md>.

**Use a stock generator as shipped** when one already emits what you need:
`meta gen --list --probe` is the catalog, and each row says how to wire it with nothing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ tested repository generator),
For any output the model describes and no reference emits, start here:

```bash
meta generator new openapi --scope model # entity (default) | package | model
meta generator new data-dictionary --scope model # entity (default) | package | model
meta gen # runs it — it already emits JSON per unit
meta verify --codegen # gates it; nothing to register
npx tsc -p tsconfig.codegen.json # typecheck it; meta gen loads it untyped
Expand Down Expand Up @@ -225,6 +225,14 @@ report hook until you resync it (`meta eject`). On SQLite a decimal field of a r
mount. A report with no view source, or an abstract one, generates nothing. The route and
contract: `references/reporting.md` in the `metaobjects-authoring` skill.

**Contract emitters (`jsonSchemaFile()`, `openApiFile()`), from 1.2.1.** Reference helpers that
write the REST contract as standard documents. `contract-schema` writes JSON Schema 2020-12 for each
object's read, create and update shapes (`profile: "strict"` for a structured-output subset), and
`openapi` writes one OpenAPI 3.1 document. Opt-in: import them from
`@metaobjectsdev/codegen-ts/generators`, or `meta eject contract-schema openapi` to own them, and
`verify --codegen` gates the output. A project that publishes the contract should fix `wire.decimals`
and `wire.nullMembers`. Details: `docs/features/contract-emitters.md`.

**Cube export (`cubeModel()`).** A reference helper that writes the reporting vocabulary
(`dimension.*`, `measure.*`, `segment.filter`, a served `object.report`) as Cube data-model files,
`model/cubes/<Cube>.yml`: a cube for each table-backed entity that declares any of it, a join for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,23 @@ building block. Every generator is a reference helper an adopter selects, ejects
on a hand edit, and `verify --db` checks the database Prisma pushed (Postgres, SQLite).
Views, expression/partial indexes and SQLite CHECKs are refused, not dropped
(`docs/recipes/prisma.md`);
- ship an OpenAPI, AsyncAPI or JSON Schema generator;
- ship an AsyncAPI generator;
- reach a consumer outside the five ports (Go, Rust, Ruby, PHP, Swift…) directly.

**Each of those is a generator the project writes**, and that is the primary codegen path, not
a workaround: a generator is a function from the loaded model to files, a few dozen lines, and
`verify --codegen` gates its output like any other. `meta generator new <name>` scaffolds one in
TypeScript; `docs/recipes/write-your-own-generator.md` shows every port and ships JSON Schema and
OpenAPI 3.1 example generators to copy. An OpenAPI or JSON Schema generator also reaches the
languages outside the five ports through their own generators. Price it in the wedge (a day or
two for a first generator), and do not report it as impossible.
TypeScript; `docs/recipes/write-your-own-generator.md` shows every port. Price it in the wedge (a
day or two for a first generator), and do not report it as impossible.

**OpenAPI and JSON Schema are stock generators, unreleased, targeting 1.2.1.** `openapi` writes one
OpenAPI 3.1 document for the generated REST surface, and `contract-schema` writes JSON Schema 2020-12
for each object's read, create and update shapes, plus a strict structured-output subset. A project
on any port runs them through the Node `meta` CLI. They reach the languages outside the five ports
through those languages' own OpenAPI tooling, and oasdiff over two emitted documents checks a change
against the last release (`docs/features/contract-emitters.md`). Until 1.2.1 ships, an assessment of
a released version counts these as generators the project writes, from the examples in
`docs/recipes/write-your-own-generator.md`.

**Contract-only cannot** bind a shape to a queue, topic or transport as declared vocabulary
(`api.*`, `operation.*`, `binding.*` are not shipped); a generator can still emit per-transport
Expand Down
2 changes: 1 addition & 1 deletion agent-context/templates/always-on.md.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ from.
- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current.
- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `<Entity>.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one.
- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `<generators>` in the Maven plugin's `<configuration>` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators <names>` (TypeScript, C#, Python; a second Maven `<execution>` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject <name>...` copies one into your repo when you need to change what it emits.
- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new <name>` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes.
- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new <name>` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes.
- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@<store>` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`.
- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill.
- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours.
Expand Down
Loading
Loading