diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3a4d4e40b..122f58d7b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,8 +35,41 @@ loaded before stops loading. This vocabulary ships in 1.3.0 with `metamodelVersi
one over measures as a `number` member (one reading a dimension is refused). Loader rule M11,
report rule R13 and a conformance fixture per refusal, in every loader port.
+- **`meta init --skills-dir
` and `meta agent-docs --skills-dir `: install the skills
+ for any coding assistant.** The `metaobjects-*` skills went only to `.claude/skills/`, which
+ Claude Code reads. `--skills-dir .agents/skills` (or any project-relative directory) puts them
+ where another assistant looks; the choice is recorded in `.metaobjects/.agent-context.json`,
+ so a later `--refresh-docs` keeps it, and moving the directory prunes the untouched copies at
+ the old one. The always-on file names the directory, so an assistant that does not load
+ skills opens `//SKILL.md` by path. A directory outside the project is refused.
+- **[What differs by language](docs/features/language-differences.md).** One page lists what
+ the same model does differently per port: that schema migrations and database checks need
+ the Node `meta` tool in every language, the list default and cap, the validation error body,
+ filter extras, field names in code, generated data access and hand-edit handling. Every port
+ page now says up front that migrations and `verify --db` run in Node, and the README's
+ getting-started path says it for Java, Kotlin, C# and Python teams.
+
### Changed
+- **The always-on agent file works with any coding assistant, carries the REST conventions,
+ and is written once.** `.metaobjects/CLAUDE.md`, an identical copy of `.metaobjects/AGENTS.md`
+ that nothing imported, is no longer written; a refresh removes an untouched one and names a
+ hand-edited one. The text no longer tells the reader to use a root `CLAUDE.md` only, and
+ `--wire-root` on a project with neither root file now creates `AGENTS.md` as well as
+ `CLAUDE.md`, each with a plain pointer line beside the `@` import. A first init on a project
+ that has only a root `CLAUDE.md` creates `AGENTS.md` too (assistants other than Claude Code
+ read no `@` import out of `CLAUDE.md`); a refresh creates no root file and instead prints one
+ advisory line naming the missing `AGENTS.md`. A new "Calling the
+ generated REST API" block gives the filter form (`?filter[status][eq]=active`, never
+ `?status=active`) with its nine operators, sorting and paging (including each port's default
+ when `limit` is omitted), and the error bodies. To offset it, the Java and Kotlin generator
+ class lists, the TypeScript catalog pipeline and the prompt and report rules moved behind the
+ skills they already pointed to. Measured on the agent-context conformance stacks (C#,
+ Java + React, Java + Kotlin + React + TanStack, Python, TypeScript + React + TanStack), the
+ file an assistant loads each session is 5,580 / 6,045 / 6,705 / 5,585 / 6,572 bytes, from
+ 4,978 / 5,767 / 6,731 / 4,983 / 6,207: the REST block adds more than the moved detail takes
+ out. The always-on bytes written to disk fall by 44 to 50% because the copy is gone. The C#,
+ Java and Python staleness notices no longer name `.claude/skills`.
- **Breaking, TypeScript: a value object is never renamed; one claim registry refuses real
collisions** (amending ADR-0044 and ADR-0056). `acme::alpha::Note` is declared as `Note` in its
own module; the `AcmeAlphaNote` derived name, `assignEmittedNames` and its
diff --git a/README.md b/README.md
index 72ea831217..c253871fe9 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,11 @@ gated by `meta verify --codegen`, which is what keeps output from drifting. See
[write your own generator](docs/recipes/write-your-own-generator.md).
Java, Kotlin, C# and Python start the same way from their own package: see
-[`docs/ports/`](docs/ports/).
+[`docs/ports/`](docs/ports/). **Their schema migrations and database checks still run in the
+Node `meta` tool** (`npx meta migrate`, `npx meta verify --db`), so those teams need Node where
+migrations are written or applied. Where the ports behave differently (list defaults, error
+bodies, field names, data access) is listed in
+[What differs by language](docs/features/language-differences.md).
The metamodel is the **durable spine**; generated code is the **disposable
artifact**. Substrate is local-first: typed metadata lives in your repo as YAML or
@@ -341,6 +345,11 @@ metaobjects/
| C# | `dotnet tool install --global MetaObjects.Cli` → [`docs/ports/csharp.md`](docs/ports/csharp.md) |
| Python | `pip install metaobjects` → [`docs/ports/python.md`](docs/ports/python.md) |
+Every language also needs the Node `meta` CLI for the schema: `npm i -D @metaobjectsdev/cli`,
+then `npx meta init --config-only` in a Maven-, .NET- or pip-rooted project, and
+`npx meta migrate` / `npx meta verify --db` from there. The per-language differences are in
+[What differs by language](docs/features/language-differences.md).
+
## Cross-language conformance
Every port runs against the same fixture corpora at [`fixtures/`](fixtures/).
diff --git a/agent-context/README.md b/agent-context/README.md
index 81175b33bd..f2e32b7625 100644
--- a/agent-context/README.md
+++ b/agent-context/README.md
@@ -2,12 +2,14 @@
This tree is the single source the assembler (`@metaobjectsdev/sdk`,
`src/agent-context/`) turns into the files scaffolded into a consumer project:
-the slim always-on Markdown (`.metaobjects/AGENTS.md` + `CLAUDE.md`) and the six
-`metaobjects-*` Claude skills (each a universal `SKILL.md` plus the
-`references/.md` fragments matching the project's resolved stack).
+the slim always-on Markdown (`.metaobjects/AGENTS.md`, imported by the project's root
+`AGENTS.md` / `CLAUDE.md`) and the six `metaobjects-*` skills (each a universal `SKILL.md` plus
+the `references/.md` fragments matching the project's resolved stack). The skills
+install under `.claude/skills/` unless `--skills-dir` names another directory (such as
+`.agents/skills/`); the always-on text names the directory through `{{skillsDir}}`.
- `servers/.meta.json` — per-server install, codegen command and the always-on's verify step (`verifyStep`).
-- `templates/always-on.md.mustache` — the slim always-on body: a working loop first (`{{stackLine}}`, `{{stackLoops}}`, `{{verifySteps}}`), then the rules. The full principles it shortens live in `metaobjects-codegen/references/principles.md`.
+- `templates/always-on.md.mustache` — the slim always-on body: a working loop first (`{{stackLine}}`, `{{stackLoops}}`, `{{verifySteps}}`), the REST conventions agents trip on, then the rules and the skill pointers (`{{skillsDir}}`). The full principles it shortens live in `metaobjects-codegen/references/principles.md`.
- `templates/loops/.md`, `templates/loops/client.md` — the generate steps of that loop, one per declared server (and one for a web client), concatenated by the assembler.
- `skills//SKILL.md` — universal skill body.
- `skills//references/.md` — language/framework fragment; installed iff `` is in the stack (`Stack.tokens`). The requirements pages are universal topic files: they install in every stack, because whether a project declares `requirement.*` nodes changes after `meta init` runs.
@@ -18,7 +20,7 @@ The **scaffolded** set is exactly the six skills in the SDK's `SKILL_NAMES`
(`src/agent-context/types.ts`) — the assembler emits only those. `skills/`
additionally holds **`metaobjects-fit-assessment`**, a *pre-adoption* tool that is
deliberately **not** in `SKILL_NAMES` and so is never scaffolded (a not-yet-adopted
-target has no `.claude/skills/metaobjects-*` to receive it); it carries
+target has no installed `metaobjects-*` skills to receive it); it carries
`scaffold: false` under `metadata:` in its front-matter as the human marker (no code reads it). Its design + retro-test
validation live in `docs/superpowers/specs/2026-07-12-metaobjects-fit-assessment-design.md`.
diff --git a/agent-context/skills/metaobjects-codegen/references/principles.md b/agent-context/skills/metaobjects-codegen/references/principles.md
index 2c2f61d72d..6308156c9d 100644
--- a/agent-context/skills/metaobjects-codegen/references/principles.md
+++ b/agent-context/skills/metaobjects-codegen/references/principles.md
@@ -12,7 +12,7 @@ reasons. Read it when a rule there does not settle the case in front of you.
## Principles
- **Adopting onto existing code? Metadata FOLLOWS the code.** On a migration (existing working code / live DB), author metadata + tune codegen to *reproduce* what the code already is — native types (`field.uuid` when the code uses `UUID`, not `field.string`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), names, nullability — so regen changes as little existing code as possible. The only existing code that should change is the hand-written layer codegen replaces; ask when a modeling choice is ambiguous. (Greenfield: model-first, below.)
-- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift.
+- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `AGENTS.md` or `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift.
- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps.
- The **live database** is a derived artifact too — never hand-apply a schema change to a running DB (ad-hoc `psql`/console `ALTER`/`CREATE`/`DROP`), not even to preview a column or unblock a boot. Drive every schema change from the metadata, never ad-hoc: on the Node/TS-owned migrate stack that is `meta migrate` (metadata → DDL, ADR-0015); on a **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. A hand-applied ad-hoc change drifts the live DB from the metadata + migration history and collides at the next migrate/boot ("column already exists") — a state no migration can reproduce. Run `meta verify --db ` after any DB-touching work to catch that drift early — the URL is required, and the bare form exits 2.
- **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.
diff --git a/agent-context/templates/always-on.md.mustache b/agent-context/templates/always-on.md.mustache
index cc1aaaf1bf..f942c0eace 100644
--- a/agent-context/templates/always-on.md.mustache
+++ b/agent-context/templates/always-on.md.mustache
@@ -3,7 +3,7 @@
> {{stackLine}}
>
> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your
-> **root `CLAUDE.md`**, not here — this file is regenerated.
+> root `AGENTS.md` or `CLAUDE.md`, not here: this file is regenerated.
The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of
truth. Code, schema and migrations are generated from it and are disposable.
@@ -11,7 +11,7 @@ truth. Code, schema and migrations are generated from it and are disposable.
## Start here: the whole loop, in order
{{stackLoops}}
-### Schema (every stack: the Node `meta` CLI owns it)
+### Schema (every stack: the Node `meta` CLI owns it, so Java, Kotlin, C# and Python projects need Node too)
Never hand-write DDL, and never `ALTER` a live database by hand.
@@ -21,16 +21,15 @@ Never hand-write DDL, and never `ALTER` a live database by hand.
- At boot or deploy: `npx meta migrate apply-pending --db `.
- A later change: edit the metadata, regenerate, then
`npx meta migrate --dialect --slug ` (offline, from the committed snapshot).
- For a renamed field add `--rename-column .=` (a renamed object:
- `--rename-table =`), which keeps the data. A destructive change is refused until you
- allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum,
- `--allow drop-column` only when the data should go.
+ A renamed field needs `--rename-column .=` (an object:
+ `--rename-table =`) to keep its data. A destructive change is refused until you
+ allow its kind by name: `--allow drop-check` for a changed enum, `--allow drop-column` only
+ when the data should go.
### Requirements, when the metadata declares `requirement.*` nodes
-They list what the software must do. A commit that changes behaviour updates them in the same
-commit: `@status: live` once a claim is built (never before), a new node for new behaviour.
-`meta verify` checks that links resolve, not that a status is true.
+A commit that changes behaviour updates them in the same commit: `@status: live` once a claim
+is built (never before), a new node for new behaviour.
### Before you call it done
@@ -39,6 +38,21 @@ commit: `@status: live` once a claim is built (never before), a new node for new
Commit the metadata, the generated code, `.metaobjects/migrations/` and
`.metaobjects/.gen-state/.hashes.json`.
+## Calling the generated REST API
+
+When the project serves the generated routes (any port):
+
+- Filter as `?filter[][]=`: `?filter[status][eq]=active`, never
+ `?status=active` (TypeScript refuses that with 400; the other ports ignore it). Operators: `eq`, `ne`, `isNull` (`true`/`false`), `in` (comma list, at most 100), `like`
+ (case-sensitive, `%` and `_`), `gt`, `gte`, `lt`, `lte`. Filters AND; only `@filterable`
+ fields filter.
+- `?sort=:asc|desc`, `?limit=N&offset=N`; `withCount=1` returns `{"rows", "total"}`.
+ Always send `limit`: without it TypeScript and C# return every row, the other ports 50.
+- `PUT` is the same partial update as `PATCH`.
+- Errors are `{"error": ""}`: 400 for validation (only TypeScript adds `issues`) and for
+ a bad filter or sort (`invalid_filter_field`, `_op`, `_value`, `invalid_sort`, each naming
+ `field`), 404 `not_found`, 409 for a unique or foreign-key conflict.
+
## Rules
- What the metadata describes is generated, never hand-written (tables, types, validators,
@@ -47,12 +61,9 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and
broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on.
- Never spell a table or column name as a string; use the generated table or names constants.
- Wire a generator only for output you will import.
-- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a
- template, the data in a payload, a structured reply read through its generated extractor
- (`metaobjects-prompts`).
-- A grouped total, dashboard number or read-only report view (per client, per month) is an
- `object.report` over `dimension.*` / `measure.*` (`metaobjects-authoring`,
- `references/declaring-reports.md`). A hand-written `@sql` view body is the last resort.
+- An LLM prompt in a project that declares `template.prompt` is a declared template
+ (`metaobjects-prompts`); a grouped total or report view is an `object.report`
+ (`metaobjects-authoring`). Open the skill before writing either by hand.
- Never invent metadata vocabulary. Search it: `npx meta types `, and
`npx meta types . --detail` for one construct's valid attributes.
- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are
@@ -60,14 +71,14 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and
## Going deeper
-The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work:
-unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a
-generator and the full principles (`metaobjects-codegen`), REST routes and the web client
-(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and
-upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when
-its task comes up; it names the one reference page to open.
-
-This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in
-the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a
-file you edited (only `--force` does, so never `--force` one with local edits): it writes
-`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`.
+The loop above needs no skill. For less common work load a `metaobjects-*` skill by name, or open
+`{{skillsDir}}//SKILL.md` if your assistant does not load skills: `metaobjects-authoring`
+(unusual shapes, reports, adopting existing code), `metaobjects-codegen` (owning or writing a
+generator), `metaobjects-runtime-ui` (REST routes, the web client), `metaobjects-prompts` (LLM
+prompts), `metaobjects-verify` (migration edge cases, drift, upgrades), `metaobjects-audit` (an
+adoption review). Each names the one reference page to open.
+
+`meta init --refresh-docs` regenerates this file and the skills, so keep project guidance in
+the root `AGENTS.md` or `CLAUDE.md` (or copy a skill to a new name). A refresh never overwrites a
+file you edited; it writes `.new` beside it, which you merge into `` and delete.
+Never `--force` a refresh over local edits.
diff --git a/agent-context/templates/loops/java.md b/agent-context/templates/loops/java.md
index 638e593fec..44210fe5d2 100644
--- a/agent-context/templates/loops/java.md
+++ b/agent-context/templates/loops/java.md
@@ -2,11 +2,8 @@
1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator
list, one `` each, under `` pointing at the metadata.
- Nothing is generated until it is listed. The Spring REST tier is `SpringDtoGenerator`,
- `SpringValueObjectGenerator`, `SpringRepositoryGenerator`, `SpringFilterAllowlistGenerator` and
- `SpringControllerGenerator` (package `com.metaobjects.generator.spring`); the build refuses a
- list missing a generator another one's output needs, and names it. `JUnitRequirementTestsGenerator`
- writes one test per requirement; `JavaApiDocsGenerator` writes `api/java/AGENT-API.md`. The
- full table is in the `metaobjects-codegen` skill's `references/java.md`.
+ Nothing is generated until it is listed; the build names any generator a listed one needs.
+ Which classes to list (the Spring REST tier, requirement tests, API docs) is in the
+ `metaobjects-codegen` skill's `references/java.md`.
2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=java`
copies a generator to change what it emits.
diff --git a/agent-context/templates/loops/kotlin.md b/agent-context/templates/loops/kotlin.md
index ec62287786..ffc8805caf 100644
--- a/agent-context/templates/loops/kotlin.md
+++ b/agent-context/templates/loops/kotlin.md
@@ -1,13 +1,9 @@
### Kotlin: generate with `mvn metaobjects:generate`
1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator
- list, one `` each. Nothing is generated until it is listed. The Exposed
- and Spring tier is `KotlinEntityGenerator`, `KotlinExposedTableGenerator`,
- `KotlinRelationsGenerator`, `KotlinFilterAllowlistGenerator` and
- `KotlinSpringControllerGenerator` (package `com.metaobjects.generator.kotlin`).
- `KotlinRequirementTestsGenerator` writes one test per requirement; `KotlinApiDocsGenerator`
- writes `api/kotlin/AGENT-API.md`. The full table is in the `metaobjects-codegen` skill's
- `references/kotlin.md`.
+ list, one `` each. Nothing is generated until it is listed. Which
+ classes to list (the Exposed and Spring tier, requirement tests, API docs) is in the
+ `metaobjects-codegen` skill's `references/kotlin.md`.
2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=kotlin`
copies a generator to change what it emits. Several Kotlin generators truncate their output
on every run, so never put your own code in a generated file.
diff --git a/agent-context/templates/loops/typescript.md b/agent-context/templates/loops/typescript.md
index 6007a7ca92..f5133dc911 100644
--- a/agent-context/templates/loops/typescript.md
+++ b/agent-context/templates/loops/typescript.md
@@ -12,15 +12,9 @@
Per entity: `.ts` (Drizzle table, row types, Zod schemas, enum types) and
`.names.ts` (physical names), plus an `index.ts` barrel. Install what they import:
`npm i drizzle-orm zod` and your database driver. Every other generator (CRUD `queriesFile()`
- / `routesFile()`, docs, `requirementTests()` for a test stub per requirement) is one line too:
-
- ```
- npx meta gen --list --probe 2>/dev/null | grep -E "^ - name|wouldEmit| import:" | paste - - -
- ```
-
- `npx meta eject ` copies one into `codegen/generators/` to change it;
+ / `routesFile()`, docs, `requirementTests()`) is one line too; `npx meta gen --list` is the
+ catalog. `npx meta eject ` copies one into `codegen/generators/` to change it;
`npx meta generator new ` writes a new one.
-2. **Generate.** `npx meta gen`. See a file's exports with `grep -n "^export" `.
-3. **Reference pages.** `agentDocsFile()` and `apiDocsFile()` with `targets: { docs: { outDir:
- "docs" } }` write `agent/schema.md`, `agent/ui.md`, `agent/requirements.md` and
- `api/AGENT-API.md`. When they exist, read the one for the tier you touch.
+2. **Generate.** `npx meta gen`. See a file's exports with `grep -n "^export" `. When
+ `agentDocsFile()` / `apiDocsFile()` are wired, read the `agent/*.md` or `api/AGENT-API.md`
+ page for the tier you touch.
diff --git a/docs/README.md b/docs/README.md
index 542ee95aaa..782ad18387 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -36,6 +36,7 @@ docs/
│ ├── extending-with-providers.md # custom metamodel subtypes/attrs via MetaDataTypeProvider
│ ├── yaml-authoring.md
│ ├── cli.md # CLI command matrix across all five ports
+│ ├── language-differences.md # what differs by language: Node for the schema, list defaults, error bodies, names, data access
│ ├── codegen-concepts.md # declarative template-codegen scopes (perEntity/perPackage/perModel)
│ ├── codegen-data-shapes.md # the neutral template data dict shared cross-port
│ ├── downstream-metadata-decisions.md # guidance for adopters extending the metamodel
@@ -67,6 +68,7 @@ this tree is documentation, not the source of truth.
| You want to … | Read |
|---|---|
| Pick a language and ship a small CRUD app | [`ports/.md`](ports/) |
+| Know what behaves differently per language (Node for migrations, list defaults, validation errors, field names, data access) | [`features/language-differences.md`](features/language-differences.md) |
| Understand what `object.entity`, `source.rdb`, `template.prompt` mean | [`features/`](features/) |
| Compare what TS vs Java vs Kotlin vs C# vs Python emit for the same metadata | any [`features/*.md`](features/) — every feature shows all five ports side-by-side |
| Author metadata in YAML instead of JSON | [`features/yaml-authoring.md`](features/yaml-authoring.md) |
diff --git a/docs/features/api-contract.md b/docs/features/api-contract.md
index f74a9c4ad7..c1f78f786a 100644
--- a/docs/features/api-contract.md
+++ b/docs/features/api-contract.md
@@ -282,9 +282,11 @@ each handler they register instead; an `HTTPException` is rethrown to your
explicit `limit` on an entity — `limit=100000` is honoured as asked — except
the TS opaque-view route, which refuses a `limit` above 1000 (see "TS-only
error responses"). A list over a large table should therefore always send a
- `limit`. A default page size is deliberately not introduced in a patch release:
- it would silently truncate the lists of every existing client that relies on
- the unpaged answer.
+ `limit`. A default page size is deliberately not introduced on the mounts that
+ return every row — and no port's default changes in a patch release: either
+ would silently truncate the lists of every existing client that relies on
+ the unpaged answer. Every per-port difference of this kind is collected in
+ [What differs by language](language-differences.md).
- `withCount=1` — opt-in flag that switches the list response from
`[...]` to `{ rows: [...], total: }` (needed for grid
pagination). The grid hook always sends `withCount=1`.
@@ -844,6 +846,8 @@ api-contract-conformance suite today (see
## See also
+- [`docs/features/language-differences.md`](language-differences.md) — what this
+ contract leaves open, port by port (list defaults, error bodies, filter extensions)
- [`docs/features/metadata-api.md`](metadata-api.md) — the `GET /_meta`
model-shape contract this page's row-data contract complements
- [`docs/features/loaders.md`](loaders.md) — how metadata maps to the
diff --git a/docs/features/cli.md b/docs/features/cli.md
index a2c84a40ee..0eece216ba 100644
--- a/docs/features/cli.md
+++ b/docs/features/cli.md
@@ -846,8 +846,9 @@ meta migrate --db "$DB_URL" --dialect postgres \
## Agent-context scaffold is Node-only — by design
-The `.metaobjects/AGENTS.md`/`CLAUDE.md` always-on files and the
-`.claude/skills/metaobjects-*/` reference tree are assembled by **one** tool: the
+The always-on `.metaobjects/AGENTS.md` and the `metaobjects-*` skills (under
+`.claude/skills/` by default, or wherever `--skills-dir` puts them, such as `.agents/skills/`
+for assistants that read that) are assembled by **one** tool: the
Node `meta agent-docs` command. Per [ADR-0033](../../spec/decisions/ADR-0033-single-agent-context-assembler.md)
the per-port native assemblers (Python/Java/C#) and their byte-identity conformance
gates were removed — that content is effectively one static artifact, and every port
diff --git a/docs/features/language-differences.md b/docs/features/language-differences.md
new file mode 100644
index 0000000000..27f74d8a52
--- /dev/null
+++ b/docs/features/language-differences.md
@@ -0,0 +1,79 @@
+# What differs by language
+
+One model, five ports. The metamodel, the loader, the drift gates and the cross-port REST
+contract behave the same everywhere, and the shared conformance corpora check that they do.
+Most of what a port *generates* follows its own language's conventions instead, and in a few
+places the ports also differ in behaviour. This page lists those differences, so you can find
+them here rather than discover them in production.
+
+This is the one place the list is kept. When a release changes a row, it changes it here, and
+the port pages link to this page rather than repeating it. The table describes `main`.
+
+## You need Node for the schema, whatever your language
+
+**Schema migrations and database checks run in the Node `meta` tool, for every port**
+([ADR-0015](../../spec/decisions/ADR-0015-single-shared-migrate-engine.md)). A .NET, Java, Kotlin or Python
+team generates code with its own build tool (`dotnet meta`, `mvn metaobjects:*`,
+`metaobjects`), but `meta migrate`, `meta migrate apply-pending`, `meta verify --db` and
+`meta verify --replay` exist only in the Node CLI. So does `meta deps sync`, and so does
+`meta agent-docs`, which writes the agent instructions. Install it per project with
+`npm i -D @metaobjectsdev/cli` and run it as `npx meta`. Node is needed on the machine that
+writes or applies migrations (a developer machine, CI, the deploy step), not in your running
+service. `npx meta init --config-only` writes just `.metaobjects/config.json`, so a Maven- or
+pip-rooted project can point the Node CLI at its metadata without gaining a TypeScript
+project.
+
+## The table
+
+| | TypeScript | C# | Java | Kotlin | Python |
+|---|---|---|---|---|---|
+| **Generate code** | `npx meta gen` | `dotnet meta gen` | `mvn metaobjects:generate` | `mvn metaobjects:generate` | `metaobjects gen` |
+| **Codegen drift check** | `npx meta verify --codegen` | `dotnet meta verify` | `mvn metaobjects:verify` | `mvn metaobjects:verify` | `metaobjects verify` |
+| **Schema migrations** | `npx meta migrate` | `npx meta migrate` (Node), or `dotnet ef migrations add` over the generated `AppDbContext` ([C# port](../ports/csharp.md)) | `npx meta migrate` (Node); `--migration-format flyway` writes Flyway-named files | `npx meta migrate` (Node); `--migration-format flyway` as for Java | `npx meta migrate` (Node) |
+| **Database drift check** | `npx meta verify --db` | `npx meta verify --db` (Node) | `npx meta verify --db` (Node) | `npx meta verify --db` (Node) | `npx meta verify --db` (Node) |
+| **Generated data access** | Drizzle tables and row types (`entityFile()`), CRUD queries (`queriesFile()`) | EF Core entities and `AppDbContext` | Spring DTOs and a repository interface | Exposed tables and a repository base | None: the generated router calls a repository `Protocol` you implement |
+| **Runtime metadata access** | `ObjectManager` on Kysely (`@metaobjectsdev/runtime-ts`) | None yet (roadmap) | OMDB (JDBC, Spring transactions) | Through Java's OMDB | `ObjectManager` on DB-API 2; not wired to the generated router |
+| **Generated REST routes** | Fastify (`routesFile()`; Hono and Drizzle mounts in `runtime-ts`) | ASP.NET minimal API | Spring `@RestController` | Spring `@RestController` | FastAPI `APIRouter` |
+| **List with no `limit`** | Every matching row | Every matching row | First 50 | First 50 | First 50 |
+| **Largest `limit` on an entity list** | No cap (an opaque raw-SQL view: 1,000) | No cap | No cap | No cap | No cap |
+| **Largest report page** | 10,000 rows, then 400 `report_too_large` | Same | Same | Same | Same |
+| **Validation error body** | 400 `{"error": "validation", "issues": [...]}` | 400 `{"error": "validation"}` | 400 `{"error": "validation"}` | 400 `{"error": "validation"}` | 400 `{"error": "validation"}` |
+| **Filter and sort errors** | 400 with `field`, plus `op`, `expected` and the allowed fields | 400 with `field` | 400 with `field` | 400 with `field` | 400 with `field` |
+| **`?status=active` (a bare field, not `filter[status][eq]`)** | 400 `filter.bare_field`, naming the form that works | Ignored: every row comes back | Ignored | Ignored | Ignored |
+| **Filter extras beyond the nine operators** | `?search=`, `filter[or]` / `filter[and]`, leading-`%` refusal, filter-value format check | None | None | None | None |
+| **Malformed JSON, a malformed id, a bad page bound, a database constraint, a server error** | Pinned bodies: `invalid_json`, `invalid_id`, `pagination.invalid_value`, `constraint_violation`, 500 `internal` | As the framework answers (a report's bad page bound is pinned in every port) | Same as C# | Same as C# | Same as C# |
+| **Field names in code** | The model's names (`createdAt`) | PascalCase properties (`CreatedAt`) | The model's names | The model's names | The model's names, not snake_case (`createdAt`) |
+| **Field names on the wire** | The model's names, in every port | | | | |
+| **Decimals and null members on the wire** | Each port writes its serializer's default; a `wire` setting makes them identical. The per-port table is under [The wire setting](api-contract.md#the-wire-setting) | | | | |
+| **Hand edits to a generated file** | Merged (three-way) on the machine that generated it; refused by name elsewhere | Refused by name, or overwritten with `WithHandEdits(HandEdits.Overwrite)` | Refused by name, or overwritten with `overwrite` | Same as Java | Refused by name, or overwritten with `handEdits: overwrite` |
+| **Own a generator** | `npx meta eject` | `dotnet meta eject` | `mvn metaobjects:eject` | `mvn metaobjects:eject` | `metaobjects eject` |
+| **Requirement authoring lint** | Yes (advisory) | No | No | No | No |
+
+The REST rows are the detail behind the [cross-port REST contract](api-contract.md): what that
+contract pins is identical in every port, and these rows are what it leaves open. When an
+agent or a client talks to a generated API, the safe subset is the one every port shares:
+`filter[][]=` with the nine operators, an explicit `limit`, and a 400 that
+carries `error` (and `field` for a filter or sort error).
+
+## Why these differ
+
+Two rules produce most of the table. First, **each port writes its own language's idiom**:
+EF Core in C#, Spring in Java, Exposed in Kotlin, Pydantic and FastAPI in Python. Identical
+output is something a project opts into (the `wire` setting), not a default the ports are
+bent to fit ([ADR-0019 Amendment 2](../../spec/decisions/ADR-0019-runtime-return-type-contract.md)).
+Second, **generated application code is a reference helper**, not a guarantee
+([ADR-0034 Amendment 3](../../spec/decisions/ADR-0034-codegen-scaffold-and-own.md)): a route,
+a repository or a DTO is a starting point you can eject and change, so a difference in what
+the reference writes is a difference you can remove in your own copy.
+
+The rest (the list default, the validation detail, Python's field names and data access) are
+known gaps rather than design, and the roadmap closes several of them. When one closes, its row
+here changes in the same pull request.
+
+## See also
+
+- [Cross-port REST API contract](api-contract.md): the shared contract and its gates.
+- [CLI architecture](cli.md): which commands each port's own CLI carries.
+- [Migrations and drift](migrations-and-drift.md): the schema workflow every port uses.
+- The port pages: [TypeScript](../ports/typescript.md), [C#](../ports/csharp.md),
+ [Java](../ports/java.md), [Kotlin](../ports/kotlin.md), [Python](../ports/python.md).
diff --git a/docs/llms/llms-full.txt b/docs/llms/llms-full.txt
index 6384dd8536..b750c5743c 100644
--- a/docs/llms/llms-full.txt
+++ b/docs/llms/llms-full.txt
@@ -4,7 +4,7 @@
**Note for AI assistants:** This file is the concatenated reference material for MetaObjects, intended to be loaded as LLM context. For the short index, see [llms.txt](https://metaobjects.dev/llms.txt). For the canonical spec and source, see the [GitHub repo](https://github.com/metaobjectsdev/metaobjects). When this file is out of date relative to the GitHub source, the GitHub source wins.
-**Adopting MetaObjects in a project?** Before anything else: install the port's package, run `meta init` (the Node `meta` CLI — works in any project with Node, including JVM/Python projects with a TS frontend), then load the scaffolded `.metaobjects/AGENTS.md` and the `.claude/skills/metaobjects-*` skills. Those are version-matched to the installed release and far richer than this file. See "For AI assistants adopting MetaObjects" below.
+**Adopting MetaObjects in a project?** Before anything else: install the port's package, run `meta init` (the Node `meta` CLI — works in any project with Node, including JVM/Python projects with a TS frontend), then load the scaffolded `.metaobjects/AGENTS.md` and the `metaobjects-*` skills. Those are version-matched to the installed release and far richer than this file. See "For AI assistants adopting MetaObjects" below.
---
@@ -42,8 +42,8 @@ If you are an AI assistant helping someone adopt MetaObjects, do this **first**
- Java / Kotlin: add `com.metaobjects:metaobjects-metadata`, `com.metaobjects:metaobjects-codegen-spring` (Java) or `com.metaobjects:metaobjects-codegen-kotlin` (Kotlin), and `com.metaobjects:metaobjects-maven-plugin`, all at `8.3.0`.
- Python: `pip install metaobjects`
- C#: install the MetaObjects .NET tool (invoked as `dotnet meta`).
-2. **Run `meta init`** (the Node `meta` CLI). It works in any project that has Node — including a JVM or Python project with a TypeScript frontend. It scaffolds a slim always-on `.metaobjects/AGENTS.md` + `.metaobjects/CLAUDE.md` (auto-wired via an `@.metaobjects/AGENTS.md` import line into the project's root `CLAUDE.md`/`AGENTS.md`, so it loads automatically) plus six `metaobjects-*` Claude Code skills under `.claude/skills/`, carrying only the project's language reference fragments. Re-run `meta init --refresh-docs` to update the agent docs after a CLI upgrade. (The Node `meta` CLI is the single agent-context scaffolder for every backend, by design — ADR-0033; the JVM/Python/C# CLIs deliberately redirect to it. For a non-Node project, scaffold just the agent context with `npx meta agent-docs --server `.)
-3. **Load `.metaobjects/AGENTS.md` and the `.claude/skills/metaobjects-*` skills.** They teach metadata authoring, codegen, runtime/UI, prompt construction, and `verify` — for this project's exact stack and version.
+2. **Run `meta init`** (the Node `meta` CLI). It works in any project that has Node — including a JVM or Python project with a TypeScript frontend. It scaffolds a slim always-on `.metaobjects/AGENTS.md` (auto-wired via a one-line pointer and an `@.metaobjects/AGENTS.md` import line into the project's root `AGENTS.md`/`CLAUDE.md`, so it loads automatically in any coding assistant) plus six `metaobjects-*` skills under `.claude/skills/` (or the directory `--skills-dir` names, such as `.agents/skills`), carrying only the project's language reference fragments. Re-run `meta init --refresh-docs` to update the agent docs after a CLI upgrade. (The Node `meta` CLI is the single agent-context scaffolder for every backend, by design — ADR-0033; the JVM/Python/C# CLIs deliberately redirect to it. For a non-Node project, scaffold just the agent context with `npx meta agent-docs --server `.)
+3. **Load `.metaobjects/AGENTS.md` and the `metaobjects-*` skills.** They teach metadata authoring, codegen, runtime/UI, prompt construction, and `verify` — for this project's exact stack and version.
This file is the short corpus; the deep, version-matched how-to is the scaffolded local context.
@@ -293,7 +293,7 @@ Run the CLI:
```bash
# Scaffold metaobjects/, .metaobjects/, metaobjects.config.ts, AND the agent context
-# (.metaobjects/AGENTS.md + CLAUDE.md + .claude/skills/metaobjects-*). Wires no generators.
+# (.metaobjects/AGENTS.md + the metaobjects-* skills). Wires no generators.
$ meta init
# The catalog: every generator by layer, with a file count for YOUR model
diff --git a/docs/llms/llms.txt b/docs/llms/llms.txt
index a8bb7fd3a5..8efd170782 100644
--- a/docs/llms/llms.txt
+++ b/docs/llms/llms.txt
@@ -31,14 +31,14 @@ If you are an AI assistant helping someone adopt MetaObjects in their project, d
- **C#**: install the MetaObjects .NET tool (`dotnet tool install` the MetaObjects CLI tool; invoked as `dotnet meta`).
2. **Run `meta init`** (the Node `meta` CLI). It works in any project that has Node — including a JVM or Python project that happens to have a TypeScript frontend. It scaffolds, into the project:
- - a slim always-on `.metaobjects/AGENTS.md` + `.metaobjects/CLAUDE.md` agent context, **auto-wired into the project's root `CLAUDE.md`/`AGENTS.md`** (an `@.metaobjects/AGENTS.md` import line) so it loads automatically; and
- - six `metaobjects-*` Claude Code skills under `.claude/skills/`, carrying only the language reference fragments for the project's stack.
+ - a slim always-on `.metaobjects/AGENTS.md` agent context, **auto-wired into the project's root `AGENTS.md`/`CLAUDE.md`** (a one-line pointer plus an `@.metaobjects/AGENTS.md` import line) so it loads automatically in any coding assistant; and
+ - six `metaobjects-*` skills under `.claude/skills/` (Claude Code's directory; pass `--skills-dir .agents/skills`, or any other directory your assistant reads), carrying only the language reference fragments for the project's stack. The always-on file names the directory, so an assistant that does not load skills opens them by path.
Re-run `meta init --refresh-docs` to update the scaffolded agent docs after a CLI upgrade. (The Node `meta` CLI is the single agent-context scaffolder for every backend, by design — ADR-0033; the JVM/Python/C# CLIs deliberately redirect to it. Run `meta init` regardless of backend language, or `npx meta agent-docs --server ` to scaffold just the agent context.)
**Upgrading a 1.0.x project to 1.1.0:** read before the first `gen`. It is the one page for every port: the steps in order and every change that needs action (the physical-naming default, documentation as a generator, the canonical decimal string, integer-only validator bounds). Several of them fail nothing when skipped.
-3. **After scaffolding, load `.metaobjects/AGENTS.md` and the `.claude/skills/metaobjects-*` skills.** They teach metadata authoring, codegen, runtime/UI, prompt construction, and `verify` — for this project's exact stack and version.
+3. **After scaffolding, load `.metaobjects/AGENTS.md` and the `metaobjects-*` skills.** They teach metadata authoring, codegen, runtime/UI, prompt construction, and `verify` — for this project's exact stack and version.
This `llms.txt` is the short index; the deep, version-matched how-to is the scaffolded local context.
@@ -75,7 +75,7 @@ MetaObjects pillars are capabilities of one metadata model, not separate product
## CLI and project setup
-MetaObjects deliberately does **not** ship one universal binary. Schema operations are language-agnostic and live in **one canonical Node `meta` CLI**; codegen is language-specific and runs in each language's own build tool (ADR-0015).
+MetaObjects deliberately does **not** ship one universal binary. Schema operations are language-agnostic and live in **one canonical Node `meta` CLI**; codegen is language-specific and runs in each language's own build tool (ADR-0015). So a C#, Java, Kotlin or Python team still needs Node wherever migrations are written or applied. What else differs per language (list defaults, error bodies, field names, data access) is listed in [What differs by language](https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/language-differences.md).
- Node CLI binary name: `meta`
- Project config file: `metaobjects.config.ts` (configures `outDir`, `targets`, `dialect`, `dbImport`, `apiPrefix`, and the `generators` array); column and table naming is `naming` in `.metaobjects/config.json`, read by every port
@@ -89,7 +89,7 @@ MetaObjects deliberately does **not** ship one universal binary. Schema operatio
### Commands
-- `meta init` — scaffold `metaobjects/`, `.metaobjects/`, and `metaobjects.config.ts` (`--with-docs` also wires `docsFile()` and `apiDocsFile()`), **plus** the agent context: a slim `.metaobjects/AGENTS.md` + `CLAUDE.md` (auto-wired into the project's root `CLAUDE.md`/`AGENTS.md`) and six `metaobjects-*` Claude Code skills under `.claude/skills/` scoped to the project's stack. `meta init --refresh-docs` updates only the agent docs.
+- `meta init` — scaffold `metaobjects/`, `.metaobjects/`, and `metaobjects.config.ts` (`--with-docs` also wires `docsFile()` and `apiDocsFile()`), **plus** the agent context: a slim `.metaobjects/AGENTS.md` (auto-wired into the project's root `AGENTS.md`/`CLAUDE.md`) and six `metaobjects-*` skills scoped to the project's stack, under `.claude/skills/` or the directory `--skills-dir` names. `meta init --refresh-docs` updates only the agent docs.
- `meta gen [...]` — TS codegen from entities defined under `metaobjects/`. Supports `--dry-run` and `--watch`. Generators (`entityFile()`, `queriesFile()`, `routesFile()`, `formFile()`, `tanstackQuery()`, `tanstackGrid()`, `barrel()`) are reference helpers: `meta init` wires none. Import one from its package to run it as shipped; `meta gen --list` prints the import line. `meta eject ...` copies the ones you need to change (`entity`, `queries`, `routes`, `names`, `barrel`, `form`, `hooks`, `grid`, `grid-hook`, `routes-hono`) into `codegen/generators/` in your own repo (ADR-0034 scaffold-and-own) and prints the local import and entry to add to `metaobjects.config.ts`. The shipped defaults target Fastify/Drizzle/React/TanStack and are retargeted by editing an ejected copy, not by switching tools. `meta gen --strict` turns an unknown attribute into an error, as `meta verify` does. `meta eject --undo` removes ejected copies that never changed. Per-port codegen runs in that port's own build tool: `mvn metaobjects:generate` (Java/Kotlin), `dotnet meta gen` (C#), `metaobjects gen` (Python).
- `meta verify` — drift check. `verify --codegen` (regen-and-diff vs committed output), `verify --templates` (prompt `{{field}}` ↔ payload-VO drift), `verify --db` (live-DB schema drift, Node `meta` only). Per-port codegen verify: `mvn metaobjects:verify -Dmeta.verify.mode=codegen|templates` (Java/Kotlin), `dotnet meta verify` (C#), `metaobjects verify` (Python).
- `meta verify` also checks any `requirement.*` nodes the model declares (capability requirements — what the system is supposed to do, as metadata). `@status` is a loader-enforced closed enum: `planned | live | partial | retired`. A dangling `@implementedBy` is an ERROR on `live`/`partial` and CORRECT on `planned` — an intention has nothing to point at yet. A requirement states what SHOULD be true and is never a journal of what happened. **`retired` satisfies that by stating a prohibition in force.** A capability that was built and then deliberately removed keeps its entry, written as the rule that it must not come back (`@supersededBy` names what replaced it, and is RESOLVED, so it must be a full dotted path; `@implementedBy` is REFUSED on `retired`). Do NOT delete a retired capability's requirement — that entry is the whole point of the feature: given a brief for a capability that had been retired, agents working from the model alone proposed rebuilding it 24 times out of 24. `@verifiedBy` and the `abandoned`/`superseded` statuses were retired in `0.24.0` and now fail the load. Entirely opt-in: no `requirement.*` nodes means no diagnostics. Registered vocabulary in all five ports, and every port's `verify` runs the same gate (`meta verify`, `metaobjects verify`, `mvn metaobjects:verify`, `dotnet meta verify`); the authoring lint is the Node `meta` CLI only.
diff --git a/docs/ports/csharp.md b/docs/ports/csharp.md
index 5816d2c53d..92744dc115 100644
--- a/docs/ports/csharp.md
+++ b/docs/ports/csharp.md
@@ -5,6 +5,8 @@ A first-class full-stack target on .NET 8. Loader + conformance + EF Core codege
Postgres + Npgsql. The baseline is **.NET 8** (`net8.0`); the tool and the packages are not
built for earlier runtimes.
+> **Schema migrations and database checks run in the Node `meta` tool.** `dotnet meta` generates and verifies code, but writing and applying migrations (`npx meta migrate`, `npx meta migrate apply-pending`) and checking a live database against the model (`npx meta verify --db`) need Node: `npm i -D @metaobjectsdev/cli`, then `npx meta init --config-only` to point it at your metadata. Node is needed where migrations are written or applied, not in your running service. What else behaves differently in this port is listed in [What differs by language](../features/language-differences.md).
+
## Regeneration and hand edits
`dotnet meta gen` has no three-way merge (that is TypeScript-only). It decides per file from a
diff --git a/docs/ports/java.md b/docs/ports/java.md
index dd55508be2..2340e01f41 100644
--- a/docs/ports/java.md
+++ b/docs/ports/java.md
@@ -22,6 +22,8 @@ the Java diff-and-converge migration engine and its `meta:migrate` / live-DB-dri
path was also removed — OMDB is pure data-access (CRUD/query/codec/transactions).
Prompt / template drift is still checked via the `metaobjects-render` `Verify` API.
+> **Schema migrations and database checks run in the Node `meta` tool.** The Maven plugin generates and verifies code, but writing and applying migrations (`npx meta migrate`, `npx meta migrate apply-pending`) and checking a live database against the model (`npx meta verify --db`) need Node: `npm i -D @metaobjectsdev/cli`, then `npx meta init --config-only` to point it at your metadata. Node is needed where migrations are written or applied, not in your running service. What else behaves differently in this port is listed in [What differs by language](../features/language-differences.md).
+
## Regeneration and hand edits
There is no three-way merge on the JVM (that is TypeScript-only). `mvn metaobjects:generate`
diff --git a/docs/ports/kotlin.md b/docs/ports/kotlin.md
index f093af6ace..59d4202a88 100644
--- a/docs/ports/kotlin.md
+++ b/docs/ports/kotlin.md
@@ -20,6 +20,8 @@ Two modules:
- **`metaobjects-metadata-ktx`** — thin Kotlin facade over the Java loader + render
engine for idiomatic Kotlin runtime use.
+> **Schema migrations and database checks run in the Node `meta` tool.** The Maven plugin generates and verifies code, but writing and applying migrations (`npx meta migrate`, `npx meta migrate apply-pending`) and checking a live database against the model (`npx meta verify --db`) need Node: `npm i -D @metaobjectsdev/cli`, then `npx meta init --config-only` to point it at your metadata. Node is needed where migrations are written or applied, not in your running service. What else behaves differently in this port is listed in [What differs by language](../features/language-differences.md).
+
## Regeneration and hand edits
There is no three-way merge on the JVM (that is TypeScript-only). Kotlin runs through the same
diff --git a/docs/ports/python.md b/docs/ports/python.md
index eb105b4bdb..480d451dfa 100644
--- a/docs/ports/python.md
+++ b/docs/ports/python.md
@@ -9,6 +9,8 @@ with no ORM of any kind, so your SQLAlchemy or Django models stay hand-written b
Python port has no `migrate` command by design; it consumes the canonical
`schema.postgres.sql` artifact verbatim.
+> **Schema migrations and database checks run in the Node `meta` tool.** `metaobjects` generates and verifies code, but writing and applying migrations (`npx meta migrate`, `npx meta migrate apply-pending`) and checking a live database against the model (`npx meta verify --db`) need Node: `npm i -D @metaobjectsdev/cli`, then `npx meta init --config-only` to point it at your metadata. Node is needed where migrations are written or applied, not in your running service. What else behaves differently in this port is listed in [What differs by language](../features/language-differences.md).
+
## Regeneration and hand edits
`metaobjects gen` has no three-way merge (that is TypeScript-only). It decides per file from a
diff --git a/docs/ports/typescript.md b/docs/ports/typescript.md
index a75500615b..bd10e1f538 100644
--- a/docs/ports/typescript.md
+++ b/docs/ports/typescript.md
@@ -5,6 +5,8 @@ The reference implementation. Published to npm at `1.2.0` as 14
runtimes: the generated code and the CLI run on stock Node (via `tsx` or after a `tsc`
build), and Bun is simply the fastest dev workflow in this repo.
+> The Node `meta` CLI is also the schema tool for the other four ports, so a Java, C# or Python team runs `npx meta migrate` and `npx meta verify --db` too. Where the TypeScript output behaves differently from theirs (list defaults, error bodies, filter extensions) is listed in [What differs by language](../features/language-differences.md).
+
## Regeneration and hand edits
Three-way merge is a TypeScript feature: on the machine that generated a file, `meta gen`
diff --git a/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/principles.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/principles.md
index 2c2f61d72d..6308156c9d 100644
--- a/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/principles.md
+++ b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/principles.md
@@ -12,7 +12,7 @@ reasons. Read it when a rule there does not settle the case in front of you.
## Principles
- **Adopting onto existing code? Metadata FOLLOWS the code.** On a migration (existing working code / live DB), author metadata + tune codegen to *reproduce* what the code already is — native types (`field.uuid` when the code uses `UUID`, not `field.string`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), names, nullability — so regen changes as little existing code as possible. The only existing code that should change is the hand-written layer codegen replaces; ask when a modeling choice is ambiguous. (Greenfield: model-first, below.)
-- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift.
+- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `AGENTS.md` or `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift.
- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps.
- The **live database** is a derived artifact too — never hand-apply a schema change to a running DB (ad-hoc `psql`/console `ALTER`/`CREATE`/`DROP`), not even to preview a column or unblock a boot. Drive every schema change from the metadata, never ad-hoc: on the Node/TS-owned migrate stack that is `meta migrate` (metadata → DDL, ADR-0015); on a **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. A hand-applied ad-hoc change drifts the live DB from the metadata + migration history and collides at the next migrate/boot ("column already exists") — a state no migration can reproduce. Run `meta verify --db ` after any DB-touching work to catch that drift early — the URL is required, and the bare form exits 2.
- **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.
diff --git a/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md
index a3f4014b0b..d9e60606b5 100644
--- a/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md
+++ b/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md
@@ -3,7 +3,7 @@
> Stack: csharp server, no client.
>
> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your
-> **root `CLAUDE.md`**, not here — this file is regenerated.
+> root `AGENTS.md` or `CLAUDE.md`, not here: this file is regenerated.
The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of
truth. Code, schema and migrations are generated from it and are disposable.
@@ -28,7 +28,7 @@ truth. Code, schema and migrations are generated from it and are disposable.
2. **Generate.** `dotnet meta gen`. `dotnet meta eject ` copies a generator into the repo
to change what it emits.
-### Schema (every stack: the Node `meta` CLI owns it)
+### Schema (every stack: the Node `meta` CLI owns it, so Java, Kotlin, C# and Python projects need Node too)
Never hand-write DDL, and never `ALTER` a live database by hand.
@@ -38,16 +38,15 @@ Never hand-write DDL, and never `ALTER` a live database by hand.
- At boot or deploy: `npx meta migrate apply-pending --db `.
- A later change: edit the metadata, regenerate, then
`npx meta migrate --dialect --slug ` (offline, from the committed snapshot).
- For a renamed field add `--rename-column .=` (a renamed object:
- `--rename-table =`), which keeps the data. A destructive change is refused until you
- allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum,
- `--allow drop-column` only when the data should go.
+ A renamed field needs `--rename-column .=` (an object:
+ `--rename-table =`) to keep its data. A destructive change is refused until you
+ allow its kind by name: `--allow drop-check` for a changed enum, `--allow drop-column` only
+ when the data should go.
### Requirements, when the metadata declares `requirement.*` nodes
-They list what the software must do. A commit that changes behaviour updates them in the same
-commit: `@status: live` once a claim is built (never before), a new node for new behaviour.
-`meta verify` checks that links resolve, not that a status is true.
+A commit that changes behaviour updates them in the same commit: `@status: live` once a claim
+is built (never before), a new node for new behaviour.
### Before you call it done
@@ -57,6 +56,21 @@ commit: `@status: live` once a claim is built (never before), a new node for new
Commit the metadata, the generated code, `.metaobjects/migrations/` and
`.metaobjects/.gen-state/.hashes.json`.
+## Calling the generated REST API
+
+When the project serves the generated routes (any port):
+
+- Filter as `?filter[][]=`: `?filter[status][eq]=active`, never
+ `?status=active` (TypeScript refuses that with 400; the other ports ignore it). Operators: `eq`, `ne`, `isNull` (`true`/`false`), `in` (comma list, at most 100), `like`
+ (case-sensitive, `%` and `_`), `gt`, `gte`, `lt`, `lte`. Filters AND; only `@filterable`
+ fields filter.
+- `?sort=:asc|desc`, `?limit=N&offset=N`; `withCount=1` returns `{"rows", "total"}`.
+ Always send `limit`: without it TypeScript and C# return every row, the other ports 50.
+- `PUT` is the same partial update as `PATCH`.
+- Errors are `{"error": ""}`: 400 for validation (only TypeScript adds `issues`) and for
+ a bad filter or sort (`invalid_filter_field`, `_op`, `_value`, `invalid_sort`, each naming
+ `field`), 404 `not_found`, 409 for a unique or foreign-key conflict.
+
## Rules
- What the metadata describes is generated, never hand-written (tables, types, validators,
@@ -65,12 +79,9 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and
broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on.
- Never spell a table or column name as a string; use the generated table or names constants.
- Wire a generator only for output you will import.
-- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a
- template, the data in a payload, a structured reply read through its generated extractor
- (`metaobjects-prompts`).
-- A grouped total, dashboard number or read-only report view (per client, per month) is an
- `object.report` over `dimension.*` / `measure.*` (`metaobjects-authoring`,
- `references/declaring-reports.md`). A hand-written `@sql` view body is the last resort.
+- An LLM prompt in a project that declares `template.prompt` is a declared template
+ (`metaobjects-prompts`); a grouped total or report view is an `object.report`
+ (`metaobjects-authoring`). Open the skill before writing either by hand.
- Never invent metadata vocabulary. Search it: `npx meta types `, and
`npx meta types . --detail` for one construct's valid attributes.
- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are
@@ -78,14 +89,14 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and
## Going deeper
-The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work:
-unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a
-generator and the full principles (`metaobjects-codegen`), REST routes and the web client
-(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and
-upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when
-its task comes up; it names the one reference page to open.
-
-This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in
-the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a
-file you edited (only `--force` does, so never `--force` one with local edits): it writes
-`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`.
+The loop above needs no skill. For less common work load a `metaobjects-*` skill by name, or open
+`.claude/skills//SKILL.md` if your assistant does not load skills: `metaobjects-authoring`
+(unusual shapes, reports, adopting existing code), `metaobjects-codegen` (owning or writing a
+generator), `metaobjects-runtime-ui` (REST routes, the web client), `metaobjects-prompts` (LLM
+prompts), `metaobjects-verify` (migration edge cases, drift, upgrades), `metaobjects-audit` (an
+adoption review). Each names the one reference page to open.
+
+`meta init --refresh-docs` regenerates this file and the skills, so keep project guidance in
+the root `AGENTS.md` or `CLAUDE.md` (or copy a skill to a new name). A refresh never overwrites a
+file you edited; it writes `.new` beside it, which you merge into `` and delete.
+Never `--force` a refresh over local edits.
diff --git a/fixtures/agent-context-conformance/csharp/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/csharp/expected/.metaobjects/CLAUDE.md
deleted file mode 100644
index a3f4014b0b..0000000000
--- a/fixtures/agent-context-conformance/csharp/expected/.metaobjects/CLAUDE.md
+++ /dev/null
@@ -1,91 +0,0 @@
-# Working with MetaObjects in this project
-
-> Stack: csharp server, no client.
->
-> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your
-> **root `CLAUDE.md`**, not here — this file is regenerated.
-
-The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of
-truth. Code, schema and migrations are generated from it and are disposable.
-
-## Start here: the whole loop, in order
-
-### C#: generate with `dotnet meta gen`
-
-1. **Config.** `codegen/Generators.cs` (written by `dotnet meta init`) is the generator list.
- Nothing is generated until it is listed. The EF Core data layer and its routes:
-
- ```csharp
- new MetaObjects.Codegen.Generators.EntityGenerator(),
- new MetaObjects.Codegen.Generators.DbContextGenerator(),
- new MetaObjects.Codegen.Generators.FilterAllowlistGenerator(),
- new MetaObjects.Codegen.Generators.RoutesGenerator(),
- ```
-
- `RequirementTestsGenerator { OutDir = "/Generated" }` writes one test per
- requirement; `ApiDocsGenerator` writes `api/csharp/AGENT-API.md`. `dotnet meta gen --list`
- is the catalog.
-2. **Generate.** `dotnet meta gen`. `dotnet meta eject ` copies a generator into the repo
- to change what it emits.
-
-### Schema (every stack: the Node `meta` CLI owns it)
-
-Never hand-write DDL, and never `ALTER` a live database by hand.
-
-- First migration (needs a reachable, empty database):
- `npx meta migrate --from-db --db --dialect --slug init` writes
- `.metaobjects/migrations/-init/up.sql` and `down.sql`.
-- At boot or deploy: `npx meta migrate apply-pending --db `.
-- A later change: edit the metadata, regenerate, then
- `npx meta migrate --dialect --slug ` (offline, from the committed snapshot).
- For a renamed field add `--rename-column .=` (a renamed object:
- `--rename-table =`), which keeps the data. A destructive change is refused until you
- allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum,
- `--allow drop-column` only when the data should go.
-
-### Requirements, when the metadata declares `requirement.*` nodes
-
-They list what the software must do. A commit that changes behaviour updates them in the same
-commit: `@status: live` once a claim is built (never before), a new node for new behaviour.
-`meta verify` checks that links resolve, not that a status is true.
-
-### Before you call it done
-
-- `dotnet meta verify --codegen` (the generated code and the requirements).
-- `npx meta verify --db ` (the live schema matches the metadata).
-
-Commit the metadata, the generated code, `.metaobjects/migrations/` and
-`.metaobjects/.gen-state/.hashes.json`.
-
-## Rules
-
-- What the metadata describes is generated, never hand-written (tables, types, validators,
- foreign keys, schema, migrations). Hand-write only what it cannot describe, on top.
-- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or
- broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on.
-- Never spell a table or column name as a string; use the generated table or names constants.
-- Wire a generator only for output you will import.
-- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a
- template, the data in a payload, a structured reply read through its generated extractor
- (`metaobjects-prompts`).
-- A grouped total, dashboard number or read-only report view (per client, per month) is an
- `object.report` over `dimension.*` / `measure.*` (`metaobjects-authoring`,
- `references/declaring-reports.md`). A hand-written `@sql` view body is the last resort.
-- Never invent metadata vocabulary. Search it: `npx meta types `, and
- `npx meta types . --detail` for one construct's valid attributes.
-- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are
- unique within a node, and package paths use `::`.
-
-## Going deeper
-
-The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work:
-unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a
-generator and the full principles (`metaobjects-codegen`), REST routes and the web client
-(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and
-upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when
-its task comes up; it names the one reference page to open.
-
-This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in
-the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a
-file you edited (only `--force` does, so never `--force` one with local edits): it writes
-`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`.
diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md
index 2c2f61d72d..6308156c9d 100644
--- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md
+++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md
@@ -12,7 +12,7 @@ reasons. Read it when a rule there does not settle the case in front of you.
## Principles
- **Adopting onto existing code? Metadata FOLLOWS the code.** On a migration (existing working code / live DB), author metadata + tune codegen to *reproduce* what the code already is — native types (`field.uuid` when the code uses `UUID`, not `field.string`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), names, nullability — so regen changes as little existing code as possible. The only existing code that should change is the hand-written layer codegen replaces; ask when a modeling choice is ambiguous. (Greenfield: model-first, below.)
-- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift.
+- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `AGENTS.md` or `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift.
- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps.
- The **live database** is a derived artifact too — never hand-apply a schema change to a running DB (ad-hoc `psql`/console `ALTER`/`CREATE`/`DROP`), not even to preview a column or unblock a boot. Drive every schema change from the metadata, never ad-hoc: on the Node/TS-owned migrate stack that is `meta migrate` (metadata → DDL, ADR-0015); on a **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. A hand-applied ad-hoc change drifts the live DB from the metadata + migration history and collides at the next migrate/boot ("column already exists") — a state no migration can reproduce. Run `meta verify --db ` after any DB-touching work to catch that drift early — the URL is required, and the bare form exits 2.
- **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.
diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md
index 6c4200f7c4..1e428811d0 100644
--- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md
+++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md
@@ -3,7 +3,7 @@
> Stack: java, kotlin server, react, tanstack client.
>
> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your
-> **root `CLAUDE.md`**, not here — this file is regenerated.
+> root `AGENTS.md` or `CLAUDE.md`, not here: this file is regenerated.
The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of
truth. Code, schema and migrations are generated from it and are disposable.
@@ -14,25 +14,18 @@ truth. Code, schema and migrations are generated from it and are disposable.
1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator
list, one `` each, under `` pointing at the metadata.
- Nothing is generated until it is listed. The Spring REST tier is `SpringDtoGenerator`,
- `SpringValueObjectGenerator`, `SpringRepositoryGenerator`, `SpringFilterAllowlistGenerator` and
- `SpringControllerGenerator` (package `com.metaobjects.generator.spring`); the build refuses a
- list missing a generator another one's output needs, and names it. `JUnitRequirementTestsGenerator`
- writes one test per requirement; `JavaApiDocsGenerator` writes `api/java/AGENT-API.md`. The
- full table is in the `metaobjects-codegen` skill's `references/java.md`.
+ Nothing is generated until it is listed; the build names any generator a listed one needs.
+ Which classes to list (the Spring REST tier, requirement tests, API docs) is in the
+ `metaobjects-codegen` skill's `references/java.md`.
2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=java`
copies a generator to change what it emits.
### Kotlin: generate with `mvn metaobjects:generate`
1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator
- list, one `` each. Nothing is generated until it is listed. The Exposed
- and Spring tier is `KotlinEntityGenerator`, `KotlinExposedTableGenerator`,
- `KotlinRelationsGenerator`, `KotlinFilterAllowlistGenerator` and
- `KotlinSpringControllerGenerator` (package `com.metaobjects.generator.kotlin`).
- `KotlinRequirementTestsGenerator` writes one test per requirement; `KotlinApiDocsGenerator`
- writes `api/kotlin/AGENT-API.md`. The full table is in the `metaobjects-codegen` skill's
- `references/kotlin.md`.
+ list, one `` each. Nothing is generated until it is listed. Which
+ classes to list (the Exposed and Spring tier, requirement tests, API docs) is in the
+ `metaobjects-codegen` skill's `references/kotlin.md`.
2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=kotlin`
copies a generator to change what it emits. Several Kotlin generators truncate their output
on every run, so never put your own code in a generated file.
@@ -46,7 +39,7 @@ from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()`
`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every
hook without an error. The `metaobjects-runtime-ui` skill has the detail.
-### Schema (every stack: the Node `meta` CLI owns it)
+### Schema (every stack: the Node `meta` CLI owns it, so Java, Kotlin, C# and Python projects need Node too)
Never hand-write DDL, and never `ALTER` a live database by hand.
@@ -56,16 +49,15 @@ Never hand-write DDL, and never `ALTER` a live database by hand.
- At boot or deploy: `npx meta migrate apply-pending --db `.
- A later change: edit the metadata, regenerate, then
`npx meta migrate --dialect --slug ` (offline, from the committed snapshot).
- For a renamed field add `--rename-column .=` (a renamed object:
- `--rename-table =`), which keeps the data. A destructive change is refused until you
- allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum,
- `--allow drop-column` only when the data should go.
+ A renamed field needs `--rename-column .=` (an object:
+ `--rename-table =`) to keep its data. A destructive change is refused until you
+ allow its kind by name: `--allow drop-check` for a changed enum, `--allow drop-column` only
+ when the data should go.
### Requirements, when the metadata declares `requirement.*` nodes
-They list what the software must do. A commit that changes behaviour updates them in the same
-commit: `@status: live` once a claim is built (never before), a new node for new behaviour.
-`meta verify` checks that links resolve, not that a status is true.
+A commit that changes behaviour updates them in the same commit: `@status: live` once a claim
+is built (never before), a new node for new behaviour.
### Before you call it done
@@ -76,6 +68,21 @@ commit: `@status: live` once a claim is built (never before), a new node for new
Commit the metadata, the generated code, `.metaobjects/migrations/` and
`.metaobjects/.gen-state/.hashes.json`.
+## Calling the generated REST API
+
+When the project serves the generated routes (any port):
+
+- Filter as `?filter[][]=`: `?filter[status][eq]=active`, never
+ `?status=active` (TypeScript refuses that with 400; the other ports ignore it). Operators: `eq`, `ne`, `isNull` (`true`/`false`), `in` (comma list, at most 100), `like`
+ (case-sensitive, `%` and `_`), `gt`, `gte`, `lt`, `lte`. Filters AND; only `@filterable`
+ fields filter.
+- `?sort=:asc|desc`, `?limit=N&offset=N`; `withCount=1` returns `{"rows", "total"}`.
+ Always send `limit`: without it TypeScript and C# return every row, the other ports 50.
+- `PUT` is the same partial update as `PATCH`.
+- Errors are `{"error": ""}`: 400 for validation (only TypeScript adds `issues`) and for
+ a bad filter or sort (`invalid_filter_field`, `_op`, `_value`, `invalid_sort`, each naming
+ `field`), 404 `not_found`, 409 for a unique or foreign-key conflict.
+
## Rules
- What the metadata describes is generated, never hand-written (tables, types, validators,
@@ -84,12 +91,9 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and
broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on.
- Never spell a table or column name as a string; use the generated table or names constants.
- Wire a generator only for output you will import.
-- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a
- template, the data in a payload, a structured reply read through its generated extractor
- (`metaobjects-prompts`).
-- A grouped total, dashboard number or read-only report view (per client, per month) is an
- `object.report` over `dimension.*` / `measure.*` (`metaobjects-authoring`,
- `references/declaring-reports.md`). A hand-written `@sql` view body is the last resort.
+- An LLM prompt in a project that declares `template.prompt` is a declared template
+ (`metaobjects-prompts`); a grouped total or report view is an `object.report`
+ (`metaobjects-authoring`). Open the skill before writing either by hand.
- Never invent metadata vocabulary. Search it: `npx meta types `, and
`npx meta types . --detail` for one construct's valid attributes.
- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are
@@ -97,14 +101,14 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and
## Going deeper
-The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work:
-unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a
-generator and the full principles (`metaobjects-codegen`), REST routes and the web client
-(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and
-upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when
-its task comes up; it names the one reference page to open.
-
-This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in
-the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a
-file you edited (only `--force` does, so never `--force` one with local edits): it writes
-`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`.
+The loop above needs no skill. For less common work load a `metaobjects-*` skill by name, or open
+`.claude/skills//SKILL.md` if your assistant does not load skills: `metaobjects-authoring`
+(unusual shapes, reports, adopting existing code), `metaobjects-codegen` (owning or writing a
+generator), `metaobjects-runtime-ui` (REST routes, the web client), `metaobjects-prompts` (LLM
+prompts), `metaobjects-verify` (migration edge cases, drift, upgrades), `metaobjects-audit` (an
+adoption review). Each names the one reference page to open.
+
+`meta init --refresh-docs` regenerates this file and the skills, so keep project guidance in
+the root `AGENTS.md` or `CLAUDE.md` (or copy a skill to a new name). A refresh never overwrites a
+file you edited; it writes `.new` beside it, which you merge into `` and delete.
+Never `--force` a refresh over local edits.
diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md
deleted file mode 100644
index 6c4200f7c4..0000000000
--- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# Working with MetaObjects in this project
-
-> Stack: java, kotlin server, react, tanstack client.
->
-> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your
-> **root `CLAUDE.md`**, not here — this file is regenerated.
-
-The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of
-truth. Code, schema and migrations are generated from it and are disposable.
-
-## Start here: the whole loop, in order
-
-### Java: generate with `mvn metaobjects:generate`
-
-1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator
- list, one `` each, under `` pointing at the metadata.
- Nothing is generated until it is listed. The Spring REST tier is `SpringDtoGenerator`,
- `SpringValueObjectGenerator`, `SpringRepositoryGenerator`, `SpringFilterAllowlistGenerator` and
- `SpringControllerGenerator` (package `com.metaobjects.generator.spring`); the build refuses a
- list missing a generator another one's output needs, and names it. `JUnitRequirementTestsGenerator`
- writes one test per requirement; `JavaApiDocsGenerator` writes `api/java/AGENT-API.md`. The
- full table is in the `metaobjects-codegen` skill's `references/java.md`.
-2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=java`
- copies a generator to change what it emits.
-
-### Kotlin: generate with `mvn metaobjects:generate`
-
-1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator
- list, one `` each. Nothing is generated until it is listed. The Exposed
- and Spring tier is `KotlinEntityGenerator`, `KotlinExposedTableGenerator`,
- `KotlinRelationsGenerator`, `KotlinFilterAllowlistGenerator` and
- `KotlinSpringControllerGenerator` (package `com.metaobjects.generator.kotlin`).
- `KotlinRequirementTestsGenerator` writes one test per requirement; `KotlinApiDocsGenerator`
- writes `api/kotlin/AGENT-API.md`. The full table is in the `metaobjects-codegen` skill's
- `references/kotlin.md`.
-2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=kotlin`
- copies a generator to change what it emits. Several Kotlin generators truncate their output
- on every run, so never put your own code in a generated file.
-
-### Web client: generated by the Node `npx meta gen`
-
-The client tiers are generators in `metaobjects.config.ts`, whatever the backend: `formFile()`
-from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` from
-`@metaobjectsdev/codegen-ts-tanstack`, with the runtime packages `@metaobjectsdev/react` and
-`@metaobjectsdev/tanstack`. Wrap the app in ``:
-`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every
-hook without an error. The `metaobjects-runtime-ui` skill has the detail.
-
-### Schema (every stack: the Node `meta` CLI owns it)
-
-Never hand-write DDL, and never `ALTER` a live database by hand.
-
-- First migration (needs a reachable, empty database):
- `npx meta migrate --from-db --db --dialect --slug init` writes
- `.metaobjects/migrations/-init/up.sql` and `down.sql`.
-- At boot or deploy: `npx meta migrate apply-pending --db `.
-- A later change: edit the metadata, regenerate, then
- `npx meta migrate --dialect --slug ` (offline, from the committed snapshot).
- For a renamed field add `--rename-column .=` (a renamed object:
- `--rename-table =`), which keeps the data. A destructive change is refused until you
- allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum,
- `--allow drop-column` only when the data should go.
-
-### Requirements, when the metadata declares `requirement.*` nodes
-
-They list what the software must do. A commit that changes behaviour updates them in the same
-commit: `@status: live` once a claim is built (never before), a new node for new behaviour.
-`meta verify` checks that links resolve, not that a status is true.
-
-### Before you call it done
-
-- `mvn metaobjects:verify` (the generated code and the requirements).
-- `npx meta verify --codegen` (the generated web client).
-- `npx meta verify --db ` (the live schema matches the metadata).
-
-Commit the metadata, the generated code, `.metaobjects/migrations/` and
-`.metaobjects/.gen-state/.hashes.json`.
-
-## Rules
-
-- What the metadata describes is generated, never hand-written (tables, types, validators,
- foreign keys, schema, migrations). Hand-write only what it cannot describe, on top.
-- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or
- broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on.
-- Never spell a table or column name as a string; use the generated table or names constants.
-- Wire a generator only for output you will import.
-- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a
- template, the data in a payload, a structured reply read through its generated extractor
- (`metaobjects-prompts`).
-- A grouped total, dashboard number or read-only report view (per client, per month) is an
- `object.report` over `dimension.*` / `measure.*` (`metaobjects-authoring`,
- `references/declaring-reports.md`). A hand-written `@sql` view body is the last resort.
-- Never invent metadata vocabulary. Search it: `npx meta types `, and
- `npx meta types . --detail` for one construct's valid attributes.
-- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are
- unique within a node, and package paths use `::`.
-
-## Going deeper
-
-The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work:
-unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a
-generator and the full principles (`metaobjects-codegen`), REST routes and the web client
-(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and
-upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when
-its task comes up; it names the one reference page to open.
-
-This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in
-the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a
-file you edited (only `--force` does, so never `--force` one with local edits): it writes
-`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`.
diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/principles.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/principles.md
index 2c2f61d72d..6308156c9d 100644
--- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/principles.md
+++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/principles.md
@@ -12,7 +12,7 @@ reasons. Read it when a rule there does not settle the case in front of you.
## Principles
- **Adopting onto existing code? Metadata FOLLOWS the code.** On a migration (existing working code / live DB), author metadata + tune codegen to *reproduce* what the code already is — native types (`field.uuid` when the code uses `UUID`, not `field.string`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), names, nullability — so regen changes as little existing code as possible. The only existing code that should change is the hand-written layer codegen replaces; ask when a modeling choice is ambiguous. (Greenfield: model-first, below.)
-- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift.
+- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `AGENTS.md` or `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift.
- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps.
- The **live database** is a derived artifact too — never hand-apply a schema change to a running DB (ad-hoc `psql`/console `ALTER`/`CREATE`/`DROP`), not even to preview a column or unblock a boot. Drive every schema change from the metadata, never ad-hoc: on the Node/TS-owned migrate stack that is `meta migrate` (metadata → DDL, ADR-0015); on a **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. A hand-applied ad-hoc change drifts the live DB from the metadata + migration history and collides at the next migrate/boot ("column already exists") — a state no migration can reproduce. Run `meta verify --db ` after any DB-touching work to catch that drift early — the URL is required, and the bare form exits 2.
- **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.
diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md
index 074f87fdd1..41c8595636 100644
--- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md
+++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md
@@ -3,7 +3,7 @@
> Stack: java server, react client.
>
> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your
-> **root `CLAUDE.md`**, not here — this file is regenerated.
+> root `AGENTS.md` or `CLAUDE.md`, not here: this file is regenerated.
The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of
truth. Code, schema and migrations are generated from it and are disposable.
@@ -14,12 +14,9 @@ truth. Code, schema and migrations are generated from it and are disposable.
1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator
list, one `` each, under `` pointing at the metadata.
- Nothing is generated until it is listed. The Spring REST tier is `SpringDtoGenerator`,
- `SpringValueObjectGenerator`, `SpringRepositoryGenerator`, `SpringFilterAllowlistGenerator` and
- `SpringControllerGenerator` (package `com.metaobjects.generator.spring`); the build refuses a
- list missing a generator another one's output needs, and names it. `JUnitRequirementTestsGenerator`
- writes one test per requirement; `JavaApiDocsGenerator` writes `api/java/AGENT-API.md`. The
- full table is in the `metaobjects-codegen` skill's `references/java.md`.
+ Nothing is generated until it is listed; the build names any generator a listed one needs.
+ Which classes to list (the Spring REST tier, requirement tests, API docs) is in the
+ `metaobjects-codegen` skill's `references/java.md`.
2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=java`
copies a generator to change what it emits.
@@ -32,7 +29,7 @@ from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()`
`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every
hook without an error. The `metaobjects-runtime-ui` skill has the detail.
-### Schema (every stack: the Node `meta` CLI owns it)
+### Schema (every stack: the Node `meta` CLI owns it, so Java, Kotlin, C# and Python projects need Node too)
Never hand-write DDL, and never `ALTER` a live database by hand.
@@ -42,16 +39,15 @@ Never hand-write DDL, and never `ALTER` a live database by hand.
- At boot or deploy: `npx meta migrate apply-pending --db `.
- A later change: edit the metadata, regenerate, then
`npx meta migrate --dialect --slug ` (offline, from the committed snapshot).
- For a renamed field add `--rename-column .=` (a renamed object:
- `--rename-table =`), which keeps the data. A destructive change is refused until you
- allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum,
- `--allow drop-column` only when the data should go.
+ A renamed field needs `--rename-column .=` (an object:
+ `--rename-table =`) to keep its data. A destructive change is refused until you
+ allow its kind by name: `--allow drop-check` for a changed enum, `--allow drop-column` only
+ when the data should go.
### Requirements, when the metadata declares `requirement.*` nodes
-They list what the software must do. A commit that changes behaviour updates them in the same
-commit: `@status: live` once a claim is built (never before), a new node for new behaviour.
-`meta verify` checks that links resolve, not that a status is true.
+A commit that changes behaviour updates them in the same commit: `@status: live` once a claim
+is built (never before), a new node for new behaviour.
### Before you call it done
@@ -62,6 +58,21 @@ commit: `@status: live` once a claim is built (never before), a new node for new
Commit the metadata, the generated code, `.metaobjects/migrations/` and
`.metaobjects/.gen-state/.hashes.json`.
+## Calling the generated REST API
+
+When the project serves the generated routes (any port):
+
+- Filter as `?filter[][]=`: `?filter[status][eq]=active`, never
+ `?status=active` (TypeScript refuses that with 400; the other ports ignore it). Operators: `eq`, `ne`, `isNull` (`true`/`false`), `in` (comma list, at most 100), `like`
+ (case-sensitive, `%` and `_`), `gt`, `gte`, `lt`, `lte`. Filters AND; only `@filterable`
+ fields filter.
+- `?sort=:asc|desc`, `?limit=N&offset=N`; `withCount=1` returns `{"rows", "total"}`.
+ Always send `limit`: without it TypeScript and C# return every row, the other ports 50.
+- `PUT` is the same partial update as `PATCH`.
+- Errors are `{"error": ""}`: 400 for validation (only TypeScript adds `issues`) and for
+ a bad filter or sort (`invalid_filter_field`, `_op`, `_value`, `invalid_sort`, each naming
+ `field`), 404 `not_found`, 409 for a unique or foreign-key conflict.
+
## Rules
- What the metadata describes is generated, never hand-written (tables, types, validators,
@@ -70,12 +81,9 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and
broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on.
- Never spell a table or column name as a string; use the generated table or names constants.
- Wire a generator only for output you will import.
-- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a
- template, the data in a payload, a structured reply read through its generated extractor
- (`metaobjects-prompts`).
-- A grouped total, dashboard number or read-only report view (per client, per month) is an
- `object.report` over `dimension.*` / `measure.*` (`metaobjects-authoring`,
- `references/declaring-reports.md`). A hand-written `@sql` view body is the last resort.
+- An LLM prompt in a project that declares `template.prompt` is a declared template
+ (`metaobjects-prompts`); a grouped total or report view is an `object.report`
+ (`metaobjects-authoring`). Open the skill before writing either by hand.
- Never invent metadata vocabulary. Search it: `npx meta types `, and
`npx meta types . --detail` for one construct's valid attributes.
- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are
@@ -83,14 +91,14 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and
## Going deeper
-The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work:
-unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a
-generator and the full principles (`metaobjects-codegen`), REST routes and the web client
-(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and
-upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when
-its task comes up; it names the one reference page to open.
-
-This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in
-the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a
-file you edited (only `--force` does, so never `--force` one with local edits): it writes
-`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`.
+The loop above needs no skill. For less common work load a `metaobjects-*` skill by name, or open
+`.claude/skills//SKILL.md` if your assistant does not load skills: `metaobjects-authoring`
+(unusual shapes, reports, adopting existing code), `metaobjects-codegen` (owning or writing a
+generator), `metaobjects-runtime-ui` (REST routes, the web client), `metaobjects-prompts` (LLM
+prompts), `metaobjects-verify` (migration edge cases, drift, upgrades), `metaobjects-audit` (an
+adoption review). Each names the one reference page to open.
+
+`meta init --refresh-docs` regenerates this file and the skills, so keep project guidance in
+the root `AGENTS.md` or `CLAUDE.md` (or copy a skill to a new name). A refresh never overwrites a
+file you edited; it writes `.new` beside it, which you merge into `` and delete.
+Never `--force` a refresh over local edits.
diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md
deleted file mode 100644
index 074f87fdd1..0000000000
--- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md
+++ /dev/null
@@ -1,96 +0,0 @@
-# Working with MetaObjects in this project
-
-> Stack: java server, react client.
->
-> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your
-> **root `CLAUDE.md`**, not here — this file is regenerated.
-
-The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of
-truth. Code, schema and migrations are generated from it and are disposable.
-
-## Start here: the whole loop, in order
-
-### Java: generate with `mvn metaobjects:generate`
-
-1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator
- list, one `` each, under `` pointing at the metadata.
- Nothing is generated until it is listed. The Spring REST tier is `SpringDtoGenerator`,
- `SpringValueObjectGenerator`, `SpringRepositoryGenerator`, `SpringFilterAllowlistGenerator` and
- `SpringControllerGenerator` (package `com.metaobjects.generator.spring`); the build refuses a
- list missing a generator another one's output needs, and names it. `JUnitRequirementTestsGenerator`
- writes one test per requirement; `JavaApiDocsGenerator` writes `api/java/AGENT-API.md`. The
- full table is in the `metaobjects-codegen` skill's `references/java.md`.
-2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=java`
- copies a generator to change what it emits.
-
-### Web client: generated by the Node `npx meta gen`
-
-The client tiers are generators in `metaobjects.config.ts`, whatever the backend: `formFile()`
-from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` from
-`@metaobjectsdev/codegen-ts-tanstack`, with the runtime packages `@metaobjectsdev/react` and
-`@metaobjectsdev/tanstack`. Wrap the app in ``:
-`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every
-hook without an error. The `metaobjects-runtime-ui` skill has the detail.
-
-### Schema (every stack: the Node `meta` CLI owns it)
-
-Never hand-write DDL, and never `ALTER` a live database by hand.
-
-- First migration (needs a reachable, empty database):
- `npx meta migrate --from-db --db --dialect --slug init` writes
- `.metaobjects/migrations/-init/up.sql` and `down.sql`.
-- At boot or deploy: `npx meta migrate apply-pending --db `.
-- A later change: edit the metadata, regenerate, then
- `npx meta migrate --dialect --slug ` (offline, from the committed snapshot).
- For a renamed field add `--rename-column .=` (a renamed object:
- `--rename-table =`), which keeps the data. A destructive change is refused until you
- allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum,
- `--allow drop-column` only when the data should go.
-
-### Requirements, when the metadata declares `requirement.*` nodes
-
-They list what the software must do. A commit that changes behaviour updates them in the same
-commit: `@status: live` once a claim is built (never before), a new node for new behaviour.
-`meta verify` checks that links resolve, not that a status is true.
-
-### Before you call it done
-
-- `mvn metaobjects:verify` (the generated code and the requirements).
-- `npx meta verify --codegen` (the generated web client).
-- `npx meta verify --db ` (the live schema matches the metadata).
-
-Commit the metadata, the generated code, `.metaobjects/migrations/` and
-`.metaobjects/.gen-state/.hashes.json`.
-
-## Rules
-
-- What the metadata describes is generated, never hand-written (tables, types, validators,
- foreign keys, schema, migrations). Hand-write only what it cannot describe, on top.
-- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or
- broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on.
-- Never spell a table or column name as a string; use the generated table or names constants.
-- Wire a generator only for output you will import.
-- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a
- template, the data in a payload, a structured reply read through its generated extractor
- (`metaobjects-prompts`).
-- A grouped total, dashboard number or read-only report view (per client, per month) is an
- `object.report` over `dimension.*` / `measure.*` (`metaobjects-authoring`,
- `references/declaring-reports.md`). A hand-written `@sql` view body is the last resort.
-- Never invent metadata vocabulary. Search it: `npx meta types