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
10 changes: 10 additions & 0 deletions .github/workflows/publish-csharp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ jobs:
steps:
- uses: actions/checkout@v4

# A final version is not published while the eject pending list has entries
# (ADR-0034 Amendment 8). A release candidate passes, which is why the `version`
# input is read first.
- name: Eject release gate
env:
VERSION_INPUT: ${{ github.event.inputs.version }}
run: |
COMMITTED="$(sed -n 's:.*<Version>\(.*\)</Version>.*:\1:p' server/csharp/Directory.Build.props | head -n 1)"
node scripts/eject-release-gate.mjs "${VERSION_INPUT:-$COMMITTED}"

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/publish-java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ jobs:
steps:
- uses: actions/checkout@v4

# A final version is not published while the eject pending list has entries
# (ADR-0034 Amendment 8). A release candidate passes.
- name: Eject release gate
run: node scripts/eject-release-gate.mjs "$(sed -n 's:.*<version>\(.*\)</version>.*:\1:p' server/java/pom.xml | head -n 1)"

- name: Set up JDK 21 + Maven settings (Central server + GPG import)
uses: actions/setup-java@v4
with:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ jobs:
steps:
- uses: actions/checkout@v4

# A final version is not published while the eject pending list has entries
# (ADR-0034 Amendment 8). A release candidate passes.
- name: Eject release gate
run: node scripts/eject-release-gate.mjs "$(node -p "require('./server/typescript/packages/cli/package.json').version")"

- name: Set up Bun
uses: oven-sh/setup-bun@v2

Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/publish-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ jobs:
steps:
- uses: actions/checkout@v4

# A final version is not published while the eject pending list has entries
# (ADR-0034 Amendment 8). A release candidate passes.
- name: Eject release gate
run: node scripts/eject-release-gate.mjs "$(sed -n 's/^version *= *"\(.*\)".*/\1/p' server/python/pyproject.toml | head -n 1)"

- name: Set up uv
uses: astral-sh/setup-uv@v5

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ MetaObjects is a **cross-language metadata standard** for declaring typed entity

The metamodel is the **durable spine**; generated code is the **disposable artifact**. Substrate is local-first: typed metadata lives in your repo, generated code is idiomatic per-language output with **no proprietary runtime**: the generated entity and model code imports nothing from MetaObjects at runtime, and the REST, prompt, client and runtime tiers import ordinary Apache-2.0 packages you can vendor, fork or replace. If `@metaobjectsdev/*` disappears tomorrow, you keep working code.

**Core vs helpers (ADR-0034 Amendment 3, 2026-09-22).** Only the core is a product promise: the metamodel, loader, canonical format and registry; runtime metadata access (the `ObjectManager` and its drivers; the HTTP adapters that mount it on a web framework are helpers, 2026-09-24 ruling); schema migrations (`meta migrate`); the drift gates (`meta verify`); prompt render and the reply parser. Every generator that writes application code into an adopter's repo — routes, controllers, ORM wiring, DTOs, forms, grids, hooks, filter allowlists — is a **reference helper**: it must compile and pass its reference fixtures, and the adopter copies it with `meta eject` and owns the copy. Mechanical test: what the tool guarantees is core; what it writes into your repo is a helper. Every port can eject: `meta eject` (TypeScript), `metaobjects eject` (Python), `mvn metaobjects:eject` (Java/Kotlin), `dotnet meta eject` (C#). Not every generator ships a reference copy: `meta gen --list` marks the ones that do not `package-only` (in TypeScript: `template` alone; ADR-0034 Amendment 3 correction, 2026-09-26, narrowed by Amendments 5 and 7). When you find a generator defect, fix the reference; do not describe it as a broken core guarantee.
**Core vs helpers (ADR-0034 Amendment 3, 2026-09-22).** Only the core is a product promise: the metamodel, loader, canonical format and registry; runtime metadata access (the `ObjectManager` and its drivers; the HTTP adapters that mount it on a web framework are helpers, 2026-09-24 ruling); schema migrations (`meta migrate`); the drift gates (`meta verify`); prompt render and the reply parser. Every generator that writes application code into an adopter's repo — routes, controllers, ORM wiring, DTOs, forms, grids, hooks, filter allowlists — is a **reference helper**: it must compile and pass its reference fixtures, and the adopter copies it with `meta eject` and owns the copy. Mechanical test: what the tool guarantees is core; what it writes into your repo is a helper. Every port can eject: `meta eject` (TypeScript), `metaobjects eject` (Python), `mvn metaobjects:eject` (Java/Kotlin), `dotnet meta eject` (C#). Not every generator ships a reference copy: `meta gen --list` marks the ones that do not `package-only` (in TypeScript: `template`, which is exempt for good, and `requirements` and `agent-docs`, which are pending; ADR-0034 Amendment 3 correction, 2026-09-26, narrowed by Amendments 5 and 7, and held to the shared manifest by Amendment 8). When you find a generator defect, fix the reference; do not describe it as a broken core guarantee.

## Six pillars

Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,29 @@ generator catalog and the Atlas evaluation are 1.2 work.

### Added

- **The eject contract: every generator hands over its emit, and a test in every port holds
it to that** (ADR-0034 Amendment 8). `fixtures/generator-registry-conformance/registry.json`
now says, for each generator, `"eject": "full"` or an exemption with its reason, and lists
per port the packaged modules an owned copy may still import (the engine surface). Each
port's eject-contract test asserts three things from it: every generator the port ships is
in its registry; every registered generator ships eject source unless exempt; and each
generator, ejected alone, compiles or loads, references no emit code left in the package,
and emits what the packaged generator emits. `template` is exempt in every port that ships
it, with the deprecated Java Mustache generator and the docs-site builders. Where the
contract does not hold yet, the file's `eject.pending` list names the generator, the port
and the packaged modules its owned copy still calls. A port's test fails on an unlisted gap
and on a listed one that has closed. The release does not ship while the list has entries:
`scripts/release.mjs`, `scripts/finish-release.mjs` and the four publish workflows refuse a
version with no prerelease suffix until the list is empty (`scripts/eject-release-gate.mjs`); a
release candidate is allowed. No generator emits different bytes because of this change.
- **Seven generators that no registry listed are registered**, so `--list`, the eject
commands and the registry-driven tests see them. TypeScript: `requirements` (the ledger
pages `meta docs` writes) and `agent-docs` (the pages `meta docs --agent` writes), both
neutral and both still run by `meta docs`; a `generators` entry naming either string is
refused the way `"docs"` is. Java: `plantuml`, `html-docs`, `ai-docs`, `json-schema` and the
deprecated `mustache`, each by the class name a pom already uses. None of the seven ships
eject source yet; `mustache` will not.

- **The Java `entity` and `extractor` generators can be ejected.**
`mvn metaobjects:eject -Dnames=entity,extractor -Dport=java` copies each generator, with
the rest of its emit, into your `codegen/` module. They were the two Java generators that
Expand Down Expand Up @@ -872,6 +895,13 @@ until you regenerate.

### Fixed

- **The Java PlantUML generator no longer throws on a single-segment package.**
`PlantUMLGenerator` failed with `StringIndexOutOfBoundsException` for any model whose
package has one segment (`acme`, not `acme::shop`), which is most small models. It was in
no registry, so no registry-driven test ran it; registering it (above) put it in two such
suites and both failed. A package with no parent now draws no parent namespace. Output
for a multi-segment package is unchanged.

- **A doc generator skipped by `meta gen` is now reported in every output form.** `meta docs`
is the door for documentation (ADR-0025), so `meta gen` warns about and skips a `docsFile()`
or `apiDocsFile()` listed under `generators`. That has not changed. What changed is who hears
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,10 @@ name. Import by default and eject to customize: an owned copy you never change o
behind the reference. (`entityFile` / `queriesFile` / `routesFile` / `barrel` were removed
from that subpath at 1.0 and restored in 1.1, so on a 1.0.x install an owned copy is the
only path for those four.) The engine primitives come from the package main entry,
`@metaobjectsdev/codegen-ts`. One entry cannot be ejected and is `package-only` in
`meta gen --list`: the `template` primitive, which has no emit logic of its own.
`@metaobjectsdev/codegen-ts`. Three entries cannot be ejected and are `package-only` in
`meta gen --list`: the `template` primitive, which has no emit logic of its own, and
`requirements` and `agent-docs`, the pages `meta docs` writes, which do not ship a
reference template yet.

Three ejectable entries behave differently from the rest:

Expand Down
21 changes: 19 additions & 2 deletions docs/CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ the corpora above do two different jobs. Only the first is a promise to adopters
exporter's output, a TypeScript reference helper),
`requirement-test-identity-conformance/` (which tests the `requirement-tests` reference
generator yields from a ledger and what each is called; the text it writes is not
pinned) and the codegen-compile gate. They check that the reference generators are correct
starting points; an adopter's ejected copy is theirs and is not gated. The hand-rolled
pinned), the codegen-compile gate and the eject contract (below). They check that the
reference generators are correct starting points; an adopter's ejected copy is theirs and is not gated. The hand-rolled
reference-server lane of `api-contract-conformance/` still pins the wire contract that
the runtimes and the client speak, which is core.

Expand All @@ -99,6 +99,23 @@ corpora, or between its *vocabulary* and its *behaviour* — never gets a row of
own, and its boundary ends up discoverable only from the feature doc. The split ones
are recorded here instead.

**The eject contract** ([ADR-0034](../spec/decisions/ADR-0034-codegen-scaffold-and-own.md)
Amendment 8). A gate, not a corpus: it has no fixtures of its own, and reads
`generator-registry-conformance/registry.json`.

- *Gated in all five ports.* Each port's eject-contract test asserts that every generator it
ships is in the registry (census), that every registered generator ships eject source
unless the shared file exempts it (completeness), and that each generator ejected alone
compiles or loads, references nothing in the codegen packages outside the engine surface
and emits what the packaged generator emits (depth). TypeScript:
`cli/test/eject-contract.test.ts`. C#: `EjectContractTests`. Java: `EjectContractTest` in
`maven-plugin`. Kotlin: `EjectContractTest` in `codegen-kotlin`. Python:
`tests/conformance/test_eject_contract.py`.
- *Not yet true everywhere, and the shared file says where.* `eject.pending` lists every
generator that still fails an assertion. A port's test fails when an unlisted gap appears
and when a listed one closes without its entry being removed.
- *Not gated: an adopter's owned copy.* The contract is about what the reference hands over.

**Capability requirements** ([features/requirements.md](features/requirements.md)):

- *Vocabulary — gated in all five ports.* The types, their attributes and their closed
Expand Down
39 changes: 32 additions & 7 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
> - **Maven Central** → push a `java-v<maven-version>` tag (`publish-java.yml` signs and deploys
> from repo secrets), or locally `~/.m2/settings.xml` (server ids `central` +
> `gpg-credentials`) + the local GPG signing key and `mvn -Prelease deploy` from
> `server/java` (autoPublish — no manual staging promotion). One route per version, never both.
> `server/java` (autoPublish, so no manual staging promotion; run the eject release gate
> first, see the Java procedure). One route per version, never both.
> - **NuGet** → keyless OIDC via the `publish-csharp.yml` workflow: `gh workflow run
> publish-csharp.yml` (or push a `csharp-v<version>` tag).
>
Expand Down Expand Up @@ -48,6 +49,26 @@ Decide explicitly for every branch the scan returns: merge it into this cut, or
Note that `no-mistakes/*` branches are usually gate replays of work already on `main` — confirm
rather than assume. Do not skip past the list.

**The eject pending list (1.1.0).** 1.1.0 does not ship while
`fixtures/generator-registry-conformance/registry.json` has an entry in `eject.pending`
(ADR-0034 Amendment 8). The scripted paths enforce it: `scripts/release.mjs` (first check of
its preflight), `scripts/finish-release.mjs` (gate 0, before the tag) and the four publish
workflows (the step after checkout) refuse a version with no prerelease suffix while the list
holds an entry. A release candidate (`1.1.0-rc.3`, or `1.1.0rc3` on PyPI) is allowed.

A publish you type by hand passes through none of those, so run the gate yourself as the
command before it. This document has it before each of the four: the npm promote in step 3,
the isolated one-package `bun publish`, the PyPI fallback `uv publish`, and the local
`mvn -Prelease deploy`. It exits non-zero on a refusal:

```
node scripts/eject-release-gate.mjs <version> # the version that registry is about to receive
```

To clear it for a final release: close the gaps and delete their entries. There is no other
step. Each port's eject-contract test fails on a gap that is not listed, so an entry cannot be
deleted before its gap is closed.

**Why this is step 0 and not a nicety:** `0.21.1` shipped *without* a `timestampMode` fix that was
already written and sitting on an unmerged branch. Because versions are immutable on all four
registries, the only correction was a full coordinated `0.21.2` within the hour — four publishes, an
Expand Down Expand Up @@ -549,6 +570,7 @@ scripts/ci-local.sh --strict-toolchains # must end "LOCAL CI PASSED"
# bump the candidate set to the final <version>
rm bun.lock && bun install
# verify packed deps (rule 2); commit "chore(release): <version>"
node scripts/eject-release-gate.mjs <version> # must exit 0; refuses while eject.pending has entries
( cd <pkg-dir> && bun publish ) # default tag = latest, tier order
git push origin main # NO tag here — see golden rule 7
```
Expand Down Expand Up @@ -587,8 +609,8 @@ CDN-cached and lags right after publish.
## Isolated patch (one package)

If only one package changed (e.g. a `cli` bugfix), bump just that package, `rm bun.lock && bun
install`, verify, and `bun publish` it — the others stay at their current version. Tag scoped
(e.g. `cli-v0.5.1`).
install`, verify, run `node scripts/eject-release-gate.mjs <version>` (it must exit 0), and
`bun publish` it. The others stay at their current version. Tag scoped (e.g. `cli-v0.5.1`).

# Releasing the C# packages to NuGet

Expand Down Expand Up @@ -750,9 +772,10 @@ Project `metaobjects` → **Settings → Publishing → Add a new GitHub publish
python-v<version>`. **Do not ALSO publish by hand.** The `1.0.3` cut ran a local
`uv publish` and then pushed the tag, so the workflow went red with `400 File already
exists` on a release that had shipped — a red run that reads exactly like a failed publish.
The local route is the fallback only when the workflow cannot run: `UV_PUBLISH_TOKEN=<a PyPI
API token> uv publish dist/*` from `server/python`, and then do not push a `python-v*` tag
for that version.
The local route is the fallback only when the workflow cannot run. Run
`node scripts/eject-release-gate.mjs <version>` from the repository root first (it must exit
0; the workflow runs the same gate), then `UV_PUBLISH_TOKEN=<a PyPI API token> uv publish
dist/*` from `server/python`, and then do not push a `python-v*` tag for that version.
4. **Verify:** `curl -s https://pypi.org/pypi/metaobjects/json | python3 -c "import sys,json;print(json.load(sys.stdin)['info']['version'])"`.

# Releasing the Java/Kotlin modules to Maven Central
Expand Down Expand Up @@ -789,7 +812,9 @@ versioned on its own major line — npm major + 7, so `7.x` while npm was `0.x`
with `Component … already exists`): push a `java-v<maven-version>` tag, and
`publish-java.yml` runs the deploy below in CI with the signing secrets (how `8.0.4`
shipped; it takes no version override, so it deploys the COMMITTED version — do not fire
it for an uncommitted pre-release); or run `mvn -Prelease deploy` from `server/java`.
it for an uncommitted pre-release); or run
`node scripts/eject-release-gate.mjs <maven-version>` from the repository root (it must exit
0; the workflow runs the same gate) and then `mvn -Prelease deploy` from `server/java`.
The `central-publishing-maven-plugin`
(`<publishingServerId>central</publishingServerId>`, `<autoPublish>true</autoPublish>`) uploads
the signed bundle and auto-releases — **no manual staging → release promotion**. Auth + the GPG
Expand Down
24 changes: 16 additions & 8 deletions docs/features/own-your-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,13 @@ you how far it is from the reference.

## The docs generators and `shared-model` (TypeScript)

Every stock TypeScript generator can be ejected except one. `template` is the Mustache
Every stock TypeScript generator can be ejected except three. `template` is the Mustache
primitive: you already supply its template, its walk and its name, and it has no emit
logic of its own, so a copy would own nothing. `meta gen --list` marks it `package-only`.
The C#, Java and Python ports rule the same way for theirs (ADR-0034 Amendment 7).
logic of its own, so a copy would own nothing. That exemption is permanent, and the C#,
Java and Python ports rule the same way for theirs (ADR-0034 Amendment 7). `requirements`
and `agent-docs`, the pages `meta docs` and `meta docs --agent` write, were registered by
the eject census and do not ship a reference template yet (ADR-0034 Amendment 8).
`meta gen --list` marks all three `package-only`.

Three of the ejectable ones differ from the rest in what runs them or in what the copy may
change.
Expand Down Expand Up @@ -274,7 +277,11 @@ directory that holds `metaobjects.config.ts`.
## What `meta eject` hands over, and what stays core (TypeScript)

Owning a generator means owning the code it writes **and the code that code calls**,
when that code is a helper. On TypeScript the generated routes call an HTTP adapter —
when that code is a helper. That is the rule for every generator in every port
(ADR-0034 Amendment 8). It is not yet true of all of them: several owned copies still call
emit code in the package, and `fixtures/generator-registry-conformance/registry.json` lists
each one under `eject.pending` until it is closed. What follows is the part that is
already handed over. On TypeScript the generated routes call an HTTP adapter —
the mount helpers, the filter/sort parser, the error envelopes, pagination — and that
adapter is where most route defects have lived. So ejecting a generator whose output
calls it copies the adapter's source too:
Expand Down Expand Up @@ -997,10 +1004,11 @@ against your copy, ignoring the package line and `^// metaobjects:owned-support`
have no edits of your own (`-Dlist` says `identical`), re-eject with `-Dforce`.

`-Dport` is needed only when a name exists in both ports and your project's dependencies
do not say which. Every Kotlin generator is ejectable, and every Java generator except
`template`. `template` is the Mustache primitive: the template you supply is its emit
logic, so the generator has none of its own to hand over. That holds in every port that
ships it.
do not say which. Every Kotlin generator is ejectable. Every Java generator is too, except
`template`, the deprecated `mustache`, and four that are registered and do not ship eject
source yet (`plantuml`, `html-docs`, `ai-docs`, `json-schema`). `template` is the Mustache
primitive: the template you supply is its emit logic, so the generator has none of its own
to hand over. That holds in every port that ships it.

## C#: `dotnet meta eject`

Expand Down
Loading
Loading