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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ here.**

### Docs

- **Writing your own generator is documented as the normal path.** `llms.txt`, `llms-full.txt`
and the README now say a team should expect to write generators, and that documentation is a
generator. New recipe, [a typed client for the reference routes](docs/recipes/typed-client-for-the-reference-routes.md),
with a worked generator that fails `verify --codegen` when the routes move. The generator recipes
advise `handEdits: "overwrite"` for output that carries a safety rule (the default `"keep"`
merges a hand edit that removes it). One install pair that compiles the stock generated
TypeScript replaces the lines that disagreed. The `llms.txt` copy no longer repeats the homepage
lede or leans on "spine" and "substrate", and its longest sentences are split.

- **Port docs and `llms-full.txt` now say what a fresh reader got wrong.** Each port page opens with its keep-your-ORM path (TypeScript `runtime: false`, Java and Kotlin records and data classes, sourceless C# POCOs as DTOs, Pydantic in Python); three-way merge is scoped to TypeScript everywhere it was claimed unscoped, with what the other ports do instead (committed hash manifest, refusal by name, eject); framework baselines are stated (Java 21 rather than the contradictory 17, Spring Boot 3.x with Boot 2 unsupported, .NET 8, Python 3.11, Node first and Bun second); and each port page carries its known gaps with absolute links.

### Added
Expand Down Expand Up @@ -72,6 +81,40 @@ here.**
`fixtures/dependency-conformance/` (the 11 check cases pin the Node `meta deps check`); Kotlin loads through the same loader and adds a codegen test.
Spring runtime auto-load and `verify` drift for dependencies are later slices.

- **Own your generators, and give back the ones you did not change.** A team is expected to
write generators of its own, because a generator it owns is what keeps output from drifting.
Two things now help it carry fewer copies than it needs. `meta gen` prints one line naming
every owned generator copy that is still the shipped reference, unchanged. `meta eject --undo
[<name>...] [--force]` removes them: with no names, every unchanged copy (and the template
modules only they used); with names, those, refusing the whole call and removing nothing if a
named copy differs. It never edits `metaobjects.config.ts`; it prints the import to swap back.
The other ports have the same switch: `metaobjects eject --undo` (Python), `mvn
metaobjects:eject -Dundo=true` (Java and Kotlin) and `dotnet meta eject --undo` (C#). There is no
`doctor` command in any port; `eject --list` is the report. See
[the CLI notes](docs/features/cli.md#owned-generators-the-unchanged-copy-notice-eject---undo-and-gen---strict).

- **`gen --strict`: an unknown attribute is an error, as it is in `verify`.** `gen` has always
loaded leniently, so a typo such as `@sensitve` was a warning and the run went on with changed
output. `meta gen --strict` (TypeScript), `metaobjects gen --strict` (Python) and `dotnet meta gen
--strict` (C#) exit 1 and write nothing. `mvn metaobjects:generate` already loaded strictly
(`-Dmeta.lax=true` relaxes it), so Java and Kotlin needed no new switch.

- **`meta init --with-docs` (TypeScript, C#) and a new `metaobjects init [--with-docs]`
(Python).** The scaffold wires the documentation generators and their `docs` target, so the
first `gen` writes documentation. The default scaffold is unchanged: nothing is generated until
you choose it. In C# it adds `ApiDocsGenerator` to the new `codegen/Generators.cs`.

- **`entityTypeNames(obj)` is exported from `@metaobjectsdev/codegen-ts`.** It returns the six
type names the entity module exports (`<Entity>`, `Insert`, `Update`, `Create`, `Patch`,
`Filter`; `<Entity>Row` for a TPH base), and the templates that emit them now call it, so an
owned generator that refers to them cannot fall behind a rename. `docs/ports/typescript.md`
has the table and says which to pass to `create<Entity>` and `update<Entity>` (#490).

- **Python generated routers take an `apiPrefix` setting.** It is a top-level key of
`metaobjects.config.yaml`, the counterpart of the TypeScript key of the same name. It defaults
to `/api`, so existing output is byte-identical, and `""` mounts routers at `/<plural>`. The generated API docs use
the same prefix (#488).

- **Java, Kotlin and C# can compile and match `scope` patterns.** `com.metaobjects.config.Scope`
(Java; Kotlin inherits it) and `MetaObjects.Config.Scope` (C#) port the scope-pattern grammar from
TypeScript (`compilePattern` / `compileScope` / `matchesScope`), and both run the shared
Expand Down Expand Up @@ -192,6 +235,21 @@ here.**
the command and rolls the sync back. A stale `docs/CONFORMANCE.md` cell that claimed Python passed
only 2 of the dependency corpus's cases is corrected.

- **The "Model / metadata" link in the Python, C#, Java and Kotlin API docs no longer points at
a page that does not exist.** Those ports write no model pages, so the relative link was dead.
It is now written only when `modelBaseUrl` is set.

- **TypeScript API docs describe only what the run writes.** The `api-docs` pages listed the
stock entity module (the model, its Zod schemas and `relations()`), queries and Fastify routes
for every entity, whatever the generator list held. A project that wires none of them, such as
the `meta init --with-docs` scaffold, now gets pages without them. An owned generator that
writes one counts when it carries the `emitsEntityModule`, `emitsQueries` or `mountsRoutes`
marker.

- **The metamodel reference index reads as prose.** The generated index said "NOT documentation"
in capitals and told a reader of an HTML page to "follow a link into `types/<family>.md`". It
now says what the page covers and that each row links to its type page.

- **A failed `meta deps sync` no longer leaves a lock and snapshot that cannot load.** The
command wrote `deps.lock.json` and the `.metaobjects/deps/` snapshot before loading the
combined model, and on a load failure it exited 1 but kept the new files. Because the lock
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ npx meta eject entity queries names barrel # copy them in; add the lin
npx meta gen && npx meta verify --codegen # generate, then fail if generated code has drifted
```

The generated TypeScript imports `drizzle-orm`, `zod`, `fastify` and a database driver, which
`npm` does not add for you. This pair installs everything the stock `entity`, `queries` and `routes`
output needs to compile (SQLite shown; swap in `pg` and `@types/pg` for Postgres):

```bash
npm i -D @metaobjectsdev/cli @metaobjectsdev/codegen-ts @metaobjectsdev/metadata typescript @types/better-sqlite3
npm i @metaobjectsdev/runtime-ts drizzle-orm zod fastify better-sqlite3
```

Expect to write generators of your own. The reference ones cover the common cases, and a
generator you own (`npx meta generator new <name>`) is regenerated on every model change and
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/).

Expand Down
20 changes: 20 additions & 0 deletions docs/features/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,26 @@ at `-Dmeta.verify.templateRoot`). The one goal covers BOTH Java (`codegen-spring
migrate engine, ADR-0015"); an unknown mode fails listing the valid ones. (Schema `--db` remains
Node-only by the ADR-0015 design — see below.)

## Owned generators: the unchanged-copy notice, `eject --undo`, and `gen --strict`

You are expected to own generators, but not to carry a copy that never changed. An ejected copy
that is still the shipped reference stays at the release it was copied from, so `gen` names
those copies in one line and a flag takes them back. A copy you changed is never touched
unless you name it and pass the force flag.

| Port | Notice | Take unchanged copies back | Unknown attribute in `gen` |
| --- | --- | --- | --- |
| TypeScript | `meta gen` | `meta eject --undo [<name>...] [--force]` | warns; `meta gen --strict` is an error |
| Python | `metaobjects gen` | `metaobjects eject --undo [<name>...] [--force]` | warns; `metaobjects gen --strict` is an error |
| Java / Kotlin | `metaobjects:generate` | `mvn metaobjects:eject -Dundo=true [-Dnames=a,b] [-Dforce=true]` | already an error (as `verify`); `-Dmeta.lax=true` relaxes both goals |
| C# | `dotnet meta gen` | `dotnet meta eject --undo [<name>...] [--force]` | warns; `dotnet meta gen --strict` is an error |

Undo with no names removes every copy that is the shipped reference; with names, any named copy
that differs refuses the whole call and removes nothing. It never edits your generator list
(`metaobjects.config.ts`, `codegen/Generators.cs`, `pom.xml`, `metaobjects.config.yaml`): it prints the
edit that points each entry back at the shipped generator. `--strict` exits 1 and writes
nothing. No port has a `doctor` command: `eject --list` (or the matching goal) is the report.

## `gen` refuses rather than guesses — and `--baseline` is how you answer it

Every port's `gen` decides per file whether it may overwrite, from a committed hash
Expand Down
33 changes: 32 additions & 1 deletion docs/features/own-your-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ describes. A generator is a name plus a function from the model to files; `verif
scaffolds a working one and wires it. Every port's 20-line shape, run end to end, plus
JSON Schema and OpenAPI 3.1 examples to copy:
[Write your own generator](../recipes/write-your-own-generator.md). Eject a reference
(below) only when one already emits something close to what you want.
(below) only when one already emits something close to what you want. A worked TypeScript
example that sits on top of the stock routes: [a typed fetch client for the reference
routes](../recipes/typed-client-for-the-reference-routes.md).

**A stock generator already emits what you want? Import it.** Every stock generator runs
as shipped from its package, with nothing copied into your repo, and an upgrade is how its
Expand Down Expand Up @@ -686,6 +688,35 @@ Where you set it:
one-off for one run: it overwrites every refused file of that run, of every generator,
including files the manifest has no record of.

### Safety-relevant output: set `handEdits: "overwrite"`

A generator whose output enforces something (a redaction rule, an audit log, an auth check)
needs one more setting. The default is `handEdits: "keep"`: someone can hand-edit the emitted
file, `meta gen` merges that edit into fresh output, and `meta verify --codegen` still passes
after the merge. Run on a scratch project, a line appended to a generated client survived
`meta gen` (status `merged`) and `verify --codegen` exited 0. For a redaction rule that is a
silent hole.

Set `handEdits: "overwrite"` on that generator. A hand edit is then discarded and named in the
run's warnings, and the committed file always equals fresh output. One line, on the entry in
`metaobjects.config.ts`:

```ts
generators: [entityFile(), { ...redactionGenerator(), handEdits: "overwrite" }],
```

Seen: with it set, `meta verify --codegen` exits 1 on an edited file (`edited by hand; its
generator sets handEdits: "overwrite", so 'meta gen' replaces it with fresh output`), and
`meta gen` prints `overwrote hand-edited <path> with fresh output` and restores it.
The setting is per generator, and an edited file is discarded only when it is one `gen` has a record of writing.

The same trap exists one layer up, in the model. `meta gen` loads metadata **leniently**: an
unknown attribute is a warning, and generation goes on. A typo'd `@sensitve` on a field
silently changes what your generator sees, so a redaction keyed on `@sensitive` quietly stops
applying. `meta gen --strict` makes an unknown attribute an error (exit 1, nothing written).
`meta verify` always rejects it. Use `--strict` in CI for any project whose generators
enforce something.

## The sibling module (`<Entity>.extra.ts`) is a convention, not a mechanism

Generated TypeScript files point at a sibling module for code metadata can't express, and
Expand Down
10 changes: 6 additions & 4 deletions docs/llms/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ Underneath those two verbs, the model is a cross-language metadata standard for
1. **Code generation** -- idiomatic per-language code generated from the same metadata model.
2. **Runtime metadata access** -- load the metadata at runtime to drive dynamic behavior: CRUD operations, validation, relationships, dynamic admin UIs. (Typed tool payloads are declared today; MCP exposure of tools is on the roadmap.)
3. **Drift detection** -- catch divergence between code and metadata before it ships, surfacing drift as build-time breakage.
4. **Prompt construction** -- treat LLM prompts as governed metadata: a typed payload (a projection), external provider-resolved prompt text, byte-identical cross-language render, and build-time prompt-to-payload drift detection.
5. **Requirements and testing** -- declare what the software is supposed to *do* as a node in the same model, so `implementedBy` is resolved rather than trusted, `verify` reports the ledger on every run in every port, and each port's `requirement-tests` generator scaffolds a test per claim.
4. **Prompt construction** -- keep LLM prompts in typed metadata: a typed payload (a projection), external provider-resolved prompt text, byte-identical cross-language render, and build-time prompt-to-payload drift detection.
5. **Requirements and testing** -- declare what the software is supposed to *do* as a node in the same model, so `implementedBy` is checked against the model rather than taken on faith, `verify` reports the ledger on every run in every port, and each port's `requirement-tests` generator scaffolds a test per claim.
6. **Libraries** -- reusable declared design opted into by name (`"libraries": ["iam"]`): entities, requirements and implied generators as one drift-gated artifact. Since 1.0.4, in all five ports; `ai` is `stable`, `iam` is `preview`.

The metamodel is the **durable spine**; generated code is the **disposable artifact**. Substrate is local-first: typed metadata lives in your repo, and the generated code is idiomatic per-language output with **no proprietary runtime** — the entity/model tier is dependency-free, and the optional client, prompt-render, and runtime tiers are ordinary Apache-2.0 packages you could vendor or fork. If the `@metaobjectsdev/*` (npm) or `com.metaobjects:*` (Maven) packages disappeared tomorrow, your generated code keeps working in every language.
The model is the part you keep, and the generated code can be thrown away and regenerated. Your typed metadata lives in your repo, and the generated code is idiomatic per-language output with **no proprietary runtime**: the entity and model tier has no dependencies, and the optional client, prompt-render and runtime tiers are ordinary Apache-2.0 packages you could vendor or fork. If the `@metaobjectsdev/*` (npm) or `com.metaobjects:*` (Maven) packages disappeared tomorrow, your generated code keeps working in every language.

**Write your own generators; expect to.** Documentation is a generator too (`docsFile()`, `apiDocsFile()`; `meta init --with-docs` wires them). The reference generators cover the common cases, but a team should plan to write the ones its application needs: an OpenAPI file, a typed client, a service layer in your house style. A generator you own is regenerated on every model change and gated by `verify --codegen`, which is what keeps the output from drifting. In TypeScript, `meta generator new <name>` writes a working one and wires it. See [Write your own generator](https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/write-your-own-generator.md) for every port, and [a typed client for the reference routes](https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/typed-client-for-the-reference-routes.md) for a worked example.

---

Expand All @@ -36,7 +38,7 @@ If you are helping someone decide whether to adopt MetaObjects in an existing pr
If you are an AI assistant helping someone adopt MetaObjects, do this **first** — the scaffolded local context is richer and version-matched, and it loads automatically into the project's agent context:

1. **Install the port's package:**
- TypeScript / Node: `npm install -D @metaobjectsdev/cli @metaobjectsdev/codegen-ts`
- TypeScript / Node: `npm i -D @metaobjectsdev/cli @metaobjectsdev/codegen-ts @metaobjectsdev/metadata typescript @types/better-sqlite3`, then `npm i @metaobjectsdev/runtime-ts drizzle-orm zod fastify better-sqlite3` (SQLite; use `pg` and `@types/pg` for Postgres). The generated files import `drizzle-orm`, `zod`, `fastify` and a driver, and `npm` does not add them for you.
- 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.1.4`.
- Python: `pip install metaobjects`
- C#: install the MetaObjects .NET tool (invoked as `dotnet meta`).
Expand Down
Loading
Loading