Skip to content

Commit aa8bfb8

Browse files
authored
docs: scope three-way merge to TypeScript and state port baselines and gaps (#473)
* docs: keep-your-ORM paths, TypeScript-scoped three-way merge, baselines and known gaps per port * docs: name db-context, exposed-table and Pydantic v2 in the keep-your-ORM notes * no-mistakes(review): apply reviewer inline fixes * no-mistakes(document): Remove stale "no 3-way merge" claim from CLI README
1 parent 57dd9bd commit aa8bfb8

13 files changed

Lines changed: 178 additions & 22 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The metamodel is the **durable spine**; generated code is the **disposable artif
1212

1313
The first four ship per-language today across the five ports (TS / C# / Java / Python / Kotlin), with cross-port conformance corpora verifying byte-identical behavior. The fifth ships its vocabulary, its `verify` checks and a `requirement-tests` generator in every port, and its authoring lint in TypeScript only. The sixth is content the other five compose, shipped as named opt-in artifacts:
1414

15-
1. **Codegen** *(reference helpers, not core — see "Core vs helpers" above; ejectable in every port)* — emit per-language code (Drizzle/Zod + Fastify for TS, EF Core + ASP.NET for C#, Spring REST + DTO + Repository for Java via `codegen-spring`, Pydantic + FastAPI for Python, KotlinPoet + Exposed + Spring for Kotlin via `codegen-kotlin`). Hand-edit-preserving regen via three-way merge.
15+
1. **Codegen** *(reference helpers, not core — see "Core vs helpers" above; ejectable in every port)* — emit per-language code (Drizzle/Zod + Fastify for TS, EF Core + ASP.NET for C#, Spring REST + DTO + Repository for Java via `codegen-spring`, Pydantic + FastAPI for Python, KotlinPoet + Exposed + Spring for Kotlin via `codegen-kotlin`). Hand-edit-preserving regen via three-way merge (TypeScript only).
1616
2. **Runtime metadata** — load metadata at runtime, drive behavior dynamically (CRUD, validation, relationships, dynamic admin UIs, LLM tool registration). On Kysely (TS), a DB-API 2 driver via ObjectManager (Python), modernized JDBC + Spring-tx via OMDB (Java), Exposed (Kotlin). **C# runtime metadata is on the roadmap** — its EF Core output is generated code, not a metadata-driven runtime; the runtime `MetaObjects` package depends only on YamlDotNet and ships no data access.
1717
3. **Drift detection** — `meta verify` catches divergence between code and metadata (covers entity codegen, prompt templates, output parsers, schema). Quality-of-life on top of codegen + runtime.
1818
4. **Prompt construction** — a prompt is code, not a string scattered across services. Declare a prompt's payload as a typed projection (payload bloat becomes a diff), keep its text external and provider-resolved, and render it deterministically: snapshot-testable, cache-stable (no whitespace change silently breaking exact-prefix prompt-cache hits), and drift-checked at build time so a renamed field can't degrade a prompt. Conformance-gated, so the guarantee holds in every language port. **Render + payload-VO codegen + `verify` + parser-on-receipt for a *responding* `template.prompt` — one carrying `@responseRef` (FR-006) — + the output-format prompt fragment & tolerant `extract` parser (FR-010) ship in all five ports today** (since 0.24.0 the whole inbound tier keys off `@responseRef`; a `template.output` is outbound-only and emits no parser — ADR-0052) — the library-side building blocks of the pillar are complete. The one remaining library-side piece is MCP exposure of declared prompts/tools (see `spec/roadmap.md`); the application-level consolidation (eval harness, end-to-end declared-prompt orchestration) and consumer adoption are exercised in adopter projects, not in this library repo. Designed in `docs/superpowers/specs/2026-05-22-fr-004-cross-language-prompt-construction-design.md`.

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ here.**
1010

1111
## [Unreleased]
1212

13+
### Docs
14+
15+
- **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.
16+
1317
### Added
1418

1519
- **Report export: `GET /<segment>?format=csv|json|xml` on a served report, in all five ports.**

‎docs/features/codegen-data-shapes.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ MetaObjects **major** release; deprecations are announced before removal.
1414
|---|---|---|
1515
| **Data extraction** | per-port walker (TS / C# / Python / Java / Kotlin) | typed data dict |
1616
| **Template rendering** | shared Mustache via `@metaobjectsdev/render` | Mustache spec |
17-
| **File emission** | the runner + overwrite policy | three-way merge |
17+
| **File emission** | the runner + overwrite policy | hash manifest (refuse an edited file); three-way merge in TypeScript only |
1818

1919
The middle layer is **language-agnostic**: the same Mustache template that
2020
renders `<Entity>.md` from TypeScript walks identically from C# or Python.

‎docs/features/migrations-and-drift.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ There are **7 drift sources**, and the toolchain has a guard for each.
1515
| **Code-vs-API-doc** | Cross-port codegen from the same metadata. | Build time |
1616
| **DB-vs-metadata** | `meta verify --db` (TS CLI) — introspects the live DB and fails if it has drifted from metadata. Includes modeled projection **view bodies** (a changed `CREATE VIEW` is drift, reported with what differs: the first differing excerpt of the text on SQLite/D1, a fingerprint mismatch on Postgres). A view `meta migrate` drops and recreates only because the migration alters a table it reads, with its definition unchanged, is not drift and is not reported; a hand-authored *unmodeled* view is unmanaged and never flagged. A schema concern owned by the Node toolchain regardless of server language; on the JVM ports the runtime auto-create/validator path was removed (ADR-0015) and the `metaobjects:verify` Maven goal is not available. Cloudflare D1 has no client wire protocol, so it can't go through `--db`'s Kysely-driver introspection — use `meta verify --dialect d1 [--d1 <binding>] [--remote]` instead (the same wrangler-shelled-out path `meta migrate --dialect d1` uses); `--remote` is required to check the *deployed* database, not the local `wrangler dev` shadow copy. Pointing `--db file:` at wrangler's local D1 state directory (`.wrangler/state/**/d1/**`) still runs, but only verifies that local copy — `verify` warns when it detects this. | CI on every PR |
1717
| **Migration-vs-metadata** | The Node `meta migrate` emits migrations FROM metadata diffs — they cannot drift from metadata by construction. Schema migrations for **every** port are owned by this Node toolchain (`@metaobjectsdev/cli migrate`, ADR-0015); the C# and Python migrate surfaces were removed. | Build time |
18-
| **Generated-edited** | `@generated` headers in emitted code + three-way merge that preserves hand-edits inside non-generated regions. | Code review |
18+
| **Generated-edited** | `@generated` headers in emitted code + a committed hash manifest that refuses an edited file by name; in TypeScript only, a three-way merge also preserves hand-edits on the machine that generated the file. | Code review |
1919
| **Prompt-vs-payload** | FR-004 `Renderer.verify` parses `{{...}}` references in templates and checks each one exists on the payload VO. | Build time + runtime |
2020
| **Generated-vs-runtime** | Kotlin / Java `MetadataStartupValidator` (from Spring `ApplicationReadyEvent`) re-loads metadata at startup and asserts the generated table objects match. | App startup |
2121

‎docs/features/own-your-codegen.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ survive depends on what the toolchain can see, and it is worth knowing which cas
430430
are in:
431431

432432
- **On the machine that generated the file**, `.metaobjects/.gen-state/` holds a full
433-
snapshot of what was last written, and `meta gen` does a real three-way merge: your
433+
snapshot of what was last written, and the TypeScript `meta gen` does a real three-way merge (C#, Python, Java and Kotlin do not merge; see below): your
434434
edit and the new generated content are combined, and you only see a conflict when
435435
they touch the same lines.
436436
- **Anywhere else** — a fresh clone, a teammate's checkout, CI — the snapshot bodies are

‎docs/llms/llms-full.txt‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ Reference generators you own, ejectable in every port. They emit per-language co
5757

5858
**What is guaranteed, and what is yours** (ADR-0034 Amendment 3). The core is the promise: the metamodel, loader, canonical format and registry; runtime metadata access (the `ObjectManager`, not the HTTP adapters that mount it); schema migrations (`meta migrate`); the drift gates (`meta verify`); prompt render and the reply parser. Every generator that writes application code into the repo is a reference helper — a starting point that compiles and passes its reference fixtures, which the adopter copies with `meta eject` and owns. A defect in the core is a MetaObjects bug; a defect in a generator is fixed in the reference, and the adopter's copy is theirs. The test is mechanical: what the tool guarantees is core; what it writes into your repo is a helper.
5959

60-
- TypeScript: Drizzle + Zod + Fastify integration (and React / TanStack on the client side)
61-
- Java: Spring REST + DTO + repository interfaces (the persistence impl is consumer-supplied) via `codegen-spring`
62-
- Kotlin: KotlinPoet + Exposed + Spring via `codegen-kotlin`
63-
- C#: EF Core + ASP.NET
64-
- Python: Pydantic + FastAPI
60+
- TypeScript: Drizzle + Zod + Fastify integration (and React / TanStack on the client side). **Keep your ORM:** a target with `runtime: false` makes the `entity` generator write Zod schemas and inferred types only, with no Drizzle and no `runtime-ts` import
61+
- Java: Spring REST + DTO + repository interfaces (the persistence impl is consumer-supplied) via `codegen-spring`. **Keep your ORM:** `SpringDtoGenerator` and `SpringValueObjectGenerator` write records (jakarta validation) and your JPA entity stays hand-written. Baseline Java 21, Spring Boot 3.x (Boot 2 is not supported)
62+
- Kotlin: KotlinPoet + Exposed + Spring via `codegen-kotlin`. **Keep your ORM:** `KotlinEntityGenerator` alone writes plain data classes; JVM 21, Spring Boot 3.x
63+
- C#: EF Core + ASP.NET (.NET 8). **Keep your EF model:** an object declared with no `source.rdb` generates a POCO with DataAnnotations and no EF mapping, a contract separate from your entities
64+
- Python: Pydantic + FastAPI (Python 3.11+). **Keep your ORM:** the `entity` generator writes Pydantic models with no ORM of any kind
6565

66-
Hand-edit-preserving regeneration uses a three-way merge so engineers can extend generated entities without losing their work when metadata changes. M:N relationship codegen (FR-018) ships in all five ports: entity navigation, idiomatic ORM wiring (Drizzle m2m / EF Core `UsingEntity` / Spring repo interface / Exposed / Pydantic), and REST traversal (`GET /<source-plural>/{id}/<relation>`).
66+
Hand-edit-preserving regeneration is **TypeScript-only**: `meta gen` three-way-merges your edits into regenerated output on the machine that generated the file. C#, Python, Java and Kotlin do not merge. Each decides from a committed hash manifest (`.metaobjects/.gen-state/.hashes.json`): a file that still matches what the generator wrote is overwritten, an edited one is refused by name (Java and Kotlin can be told to replace it with `handEdits=overwrite`), and `eject` hands you the generator to own instead. M:N relationship codegen (FR-018) ships in all five ports: entity navigation, idiomatic ORM wiring (Drizzle m2m / EF Core `UsingEntity` / Spring repo interface / Exposed / Pydantic), and REST traversal (`GET /<source-plural>/{id}/<relation>`).
6767

6868
### Runtime metadata
6969

‎docs/llms/llms.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ MetaObjects pillars are capabilities of the same metadata spine, not separate pr
5050
- **Requirements and testing** — vocabulary, the `verify` checks and a `requirement-tests` generator in all five ports; the authoring lint is TypeScript-only; dogfooded on maintainer-owned projects, no outside adopter yet.
5151
- **Libraries** — since 1.0.4, in all five ports: `ai` is `stable`, `iam` is `preview` (the same labels `meta gen --list` prints).
5252

53-
- **Codegen** — reference generators you own (ejectable in every port) that emit per-language code from a single metadata model. Drizzle + Zod + Fastify (TypeScript), Spring REST + DTO + repository interfaces (the persistence impl is consumer-supplied) (Java via `codegen-spring`), KotlinPoet + Exposed + Spring (Kotlin via `codegen-kotlin`), EF Core + ASP.NET (C#), Pydantic + FastAPI (Python). Hand-edit-preserving regeneration via three-way merge. Includes M:N relationship codegen (FR-018) in all five ports — entity navigation, idiomatic ORM wiring, and REST traversal (`GET /<source-plural>/{id}/<relation>`).
53+
- **Codegen** — reference generators you own (ejectable in every port) that emit per-language code from a single metadata model. Drizzle + Zod + Fastify (TypeScript), Spring REST + DTO + repository interfaces (the persistence impl is consumer-supplied) (Java via `codegen-spring`), KotlinPoet + Exposed + Spring (Kotlin via `codegen-kotlin`), EF Core + ASP.NET (C#), Pydantic + FastAPI (Python). Hand-edit-preserving regeneration via three-way merge is TypeScript-only; the other ports refuse an edited generated file by name and let you eject the generator to own it. Keep your ORM: contract-only output (TypeScript `runtime: false`: Zod and types; Java and Kotlin: records and data classes; C#: sourceless POCOs; Python: Pydantic) leaves your Prisma, JPA or EF Core models untouched. Includes M:N relationship codegen (FR-018) in all five ports — entity navigation, idiomatic ORM wiring, and REST traversal (`GET /<source-plural>/{id}/<relation>`).
5454
- **Runtime metadata** — load metadata at runtime and drive behavior dynamically: CRUD, validation, relationships, dynamic admin UIs. (Typed tool payloads are declared today; MCP exposure of tools is on the roadmap.) Kysely (TS), a DB-API 2 driver (pg8000 / psycopg) via ObjectManager (Python), modernized JDBC + Spring-tx via OMDB (Java), Exposed (Kotlin); C# runtime metadata is on the roadmap (its EF Core output is generated code, not a metadata-driven runtime). Runtime queries return native in-process types (ADR-0019); wire canonicalization happens only at the serialization boundary.
5555
- **Drift detection** — catch divergence between generated code and metadata before it ships. `verify` is one verb with explicit subverbs (ADR-0021): `verify --codegen` (regen-and-diff against committed output), `verify --templates` (prompt `{{field}}` ↔ payload-VO drift), and `verify --db` (live-DB schema drift, Node `meta` only). Surfaces drift as build-time breakage rather than a production incident.
5656
- **Prompt construction** — treat LLM prompts as governed metadata instead of strings scattered across services. A typed payload declared as a projection (so payload bloat and token cost are a diff, not a mystery), external provider-resolved prompt text, and a logic-less Mustache engine that renders deterministically: snapshot-testable in CI, byte-stable so an exact-prefix prompt-cache hit doesn't break on a stray whitespace, and drift-checked at build time so a renamed field can't silently degrade a prompt. Conformance-gated, so the guarantee holds in every language port. Render + payload-VO codegen + `verify` + `template.output` parser-on-receipt (FR-006) + the output-format prompt fragment & tolerant `extract` parser (FR-010/FR-011) ship in all five ports today.

‎docs/ports/csharp.md‎

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,43 @@
22

33
A first-class full-stack target on .NET 8. Loader + conformance + EF Core codegen
44
+ render engine + the `meta` CLI all ship. Targets EF Core + ASP.NET Core +
5-
Postgres + Npgsql.
5+
Postgres + Npgsql. The baseline is **.NET 8** (`net8.0`); the tool and the packages are not
6+
built for earlier runtimes.
7+
8+
## Regeneration and hand edits
9+
10+
`dotnet meta gen` has no three-way merge (that is TypeScript-only). It decides per file from a
11+
committed hash manifest, `.metaobjects/.gen-state/.hashes.json`: a file that still matches what
12+
the generator wrote is overwritten, and an edited one is refused by name. To change what a
13+
generator emits, `dotnet meta eject <name>` copies it into `codegen/generators/` for you to own.
14+
Commit the manifest.
15+
16+
## Keep your EF Core model: contracts as plain DTOs
17+
18+
Contracts and entities are not forced into one type. Declare an object with **no
19+
`source.rdb`** (a *sourceless* object) and `EntityGenerator` writes a POCO with DataAnnotations
20+
and **no EF mapping**: no `[Table]`, no `DbSet<>`, no `AppDbContext` entry, no CRUD routes, no
21+
filter allowlist. A sourced entity gets `[Table]`/`[Key]`/`[Column]` and the full EF Core
22+
surface. So the same model can hold sourceless request/response contracts that you hand to your
23+
own controllers and sourced entities where you want MetaObjects to own the mapping; your
24+
existing hand-written EF entities are not touched.
25+
26+
A declaration with no `source.rdb` (this one is a plain DTO POCO):
27+
28+
```json
29+
{ "object.entity": {
30+
"name": "AuthorSummary",
31+
"children": [
32+
{ "field.long": { "name": "id" } },
33+
{ "field.string": { "name": "name", "@required": true, "@maxLength": 200 } },
34+
{ "identity.primary": { "@fields": "id" } }
35+
]
36+
}}
37+
```
38+
39+
Wire only the generators you want in `codegen/Generators.cs`; leave `db-context` out if you keep your own `DbContext`, since the generated `AppDbContext` emits Postgres column types (`uuid`, `jsonb`, `timestamptz`) today. Note that the generated CRUD
40+
routes use the *entity* POCO, as described under [Use](#use); the DTO shape above is for
41+
objects you keep sourceless.
642

743
## Install
844

@@ -720,8 +756,17 @@ ship too — the generated `<Entity>FilterAllowlist` (`FilterAllowlistGenerator`
720756
feeds `FilterParser.Parse` + `EfCoreFilterDispatch.ApplyFilter`, both wired
721757
directly into the generated list handler. A read-only projection
722758
(`source.rdb @kind: view`) and a view-backed report get the same filter and sort
723-
on their list route, against an allowlist of their own fields. Remaining gaps are in
724-
[`server/csharp/MetaObjects.Codegen/Generators/KNOWN_GAPS.md`](../../server/csharp/MetaObjects.Codegen/Generators/KNOWN_GAPS.md).
759+
on their list route, against an allowlist of their own fields.
760+
761+
**Known gaps** (full list with workarounds in
762+
[KNOWN_GAPS.md](https://github.com/metaobjectsdev/metaobjects/blob/main/server/csharp/MetaObjects.Codegen/Generators/KNOWN_GAPS.md)):
763+
no `filter[or]` / `filter[and]` boolean grouping and a single sort key only (both outside the
764+
cross-port contract today); the `<Entity>View` / `<Entity>Views` names are reserved for a
765+
write-through entity's generated read model; an `EmitMappedClass` override does not reach that
766+
read model; M:N traversal on a write-through entity is not routed through the replica view; the
767+
ordered-comparison filter fallback in `EfCoreFilterDispatch` needs a hand-written typed
768+
dispatcher for some column types; there is no metadata-driven runtime tier (EF Core output is
769+
generated code), and no `migrate` surface (schema is TypeScript-owned).
725770

726771
## Capability snapshot
727772

0 commit comments

Comments
 (0)