@metaobjectsdev/sdk 0.16.0 → 0.17.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/agent-context/skills/metaobjects-audit/SKILL.md +25 -12
  2. package/agent-context/skills/metaobjects-audit/references/capability-checklist.md +54 -20
  3. package/agent-context/skills/metaobjects-audit/references/csharp.md +7 -4
  4. package/agent-context/skills/metaobjects-audit/references/java.md +12 -10
  5. package/agent-context/skills/metaobjects-audit/references/kotlin.md +11 -7
  6. package/agent-context/skills/metaobjects-audit/references/python.md +7 -4
  7. package/agent-context/skills/metaobjects-audit/references/typescript.md +1 -1
  8. package/agent-context/skills/metaobjects-authoring/SKILL.md +171 -39
  9. package/agent-context/skills/metaobjects-codegen/SKILL.md +45 -16
  10. package/agent-context/skills/metaobjects-codegen/references/csharp.md +21 -1
  11. package/agent-context/skills/metaobjects-codegen/references/java.md +43 -5
  12. package/agent-context/skills/metaobjects-codegen/references/kotlin.md +34 -4
  13. package/agent-context/skills/metaobjects-codegen/references/python.md +32 -8
  14. package/agent-context/skills/metaobjects-codegen/references/typescript.md +19 -3
  15. package/agent-context/skills/metaobjects-fit-assessment/SKILL.md +55 -18
  16. package/agent-context/skills/metaobjects-prompts/SKILL.md +65 -4
  17. package/agent-context/skills/metaobjects-prompts/references/csharp.md +21 -0
  18. package/agent-context/skills/metaobjects-prompts/references/java.md +32 -3
  19. package/agent-context/skills/metaobjects-prompts/references/kotlin.md +33 -3
  20. package/agent-context/skills/metaobjects-prompts/references/python.md +29 -3
  21. package/agent-context/skills/metaobjects-prompts/references/typescript.md +34 -1
  22. package/agent-context/skills/metaobjects-runtime-ui/SKILL.md +1 -1
  23. package/agent-context/skills/metaobjects-runtime-ui/references/csharp.md +90 -0
  24. package/agent-context/skills/metaobjects-runtime-ui/references/python.md +94 -0
  25. package/agent-context/skills/metaobjects-verify/SKILL.md +50 -15
  26. package/agent-context/skills/metaobjects-verify/references/migration.md +67 -14
  27. package/dist/agent-docs/body.d.ts +1 -1
  28. package/dist/agent-docs/body.d.ts.map +1 -1
  29. package/dist/agent-docs/body.js +3 -1
  30. package/dist/agent-docs/body.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/agent-docs/body.ts +3 -1
@@ -8,6 +8,7 @@ provider/LLM-call layer; you compose the call yourself.
8
8
  ## Contents
9
9
  - Wire the generator
10
10
  - What it emits
11
+ - The output-format prompt fragment (FR-010)
11
12
  - The three-step consumer pattern
12
13
  - Recommended LLM caller (bring-your-own)
13
14
  - Drift gate
@@ -52,6 +53,28 @@ rather than a throw. The payload record itself comes from `SpringPayloadGenerato
52
53
  — the parser is a companion to it, so the parser and payload VO can't silently
53
54
  drift.
54
55
 
56
+ ## The output-format prompt fragment (FR-010)
57
+
58
+ For every json/xml-format `template.output`, `codegen-spring`'s
59
+ `SpringOutputPromptGenerator` emits a `<TemplateShortName>OutputPrompt` class with a
60
+ static `renderFormat()` / `renderFormat(PromptOverrides)` pair, backed by
61
+ `OutputFormatRenderer` from the `metaobjects-render` module — the "produce your
62
+ answer like this" fragment for the model. Wire it alongside
63
+ `SpringOutputParserGenerator` in the Maven plugin's `<generators>` list:
64
+
65
+ ```xml
66
+ <generator>
67
+ <classname>com.metaobjects.generator.spring.SpringOutputPromptGenerator</classname>
68
+ <args><outputDir>${project.build.directory}/generated-sources/java</outputDir></args>
69
+ </generator>
70
+ ```
71
+
72
+ `@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`)
73
+ controls the fragment's presentation; guidance is never emitted as comments. Skipped
74
+ for `template.prompt` nodes, non-json/xml `@format`, and unresolved `@payloadRef` —
75
+ the same skip contract as the parser generator. The `SPEC`'s root name is the
76
+ capitalized payload class name, agreeing with the parser's extract-codegen root.
77
+
55
78
  ## The three-step consumer pattern
56
79
 
57
80
  Render the prompt → call your LLM client (provider-agnostic; nothing is generated
@@ -87,9 +110,15 @@ NpcResponsePayload npc = NpcResponseParser.parse(response); // the generated p
87
110
  Non-Spring JVM apps: **LangChain4j** (`ChatLanguageModel.generate(prompt)`) is the
88
111
  equivalent one-call seam.
89
112
 
90
- > The typed-trace recorder + render→call→record convenience loop ship in TypeScript
91
- > today; the JVM port is planned (ADR-0024). Until then the call is your code and the
92
- > generated parser is the typed receive side.
113
+ > The typed-trace **recorder** has shipped on this port too — `LlmTraceHelperGenerator`
114
+ > emits a `record<Entity>(...)` helper (per concrete entity extending `LlmCallBase`
115
+ > with a `@responseRef`-carrying `template.prompt`) that extracts the typed response,
116
+ > builds the base trace row, and persists it via the OMDB `LlmCallRecorder` seam. What's
117
+ > still TS-only is the **`call<Entity>` render→call→record convenience loop** — Java
118
+ > intentionally does not emit it, because the `LlmClient` seam it wraps is BYO /
119
+ > vendor-neutral here (ADR-0024). So you compose render → your LLM call → the
120
+ > generated `record<Entity>(...)` yourself; the parser above is the standalone
121
+ > receive side if you don't even want the recorder.
93
122
 
94
123
  ## Drift gate
95
124
 
@@ -10,6 +10,7 @@ the parser and the payload VO can't silently drift.
10
10
  ## Contents
11
11
  - Wire the generators
12
12
  - What it emits
13
+ - The output-format prompt fragment (FR-010)
13
14
  - The three-step consumer pattern
14
15
  - Consumer dependency
15
16
  - Recommended LLM caller (bring-your-own)
@@ -62,6 +63,28 @@ null) and a `extractLenient(loader, text)` overload that delegates to the runtim
62
63
  components. The lenient mirror type (`<Name>Extracted`) uses nullable fields per
63
64
  the Kotlin null-safety port — a missing/malformed component is `null`, not a throw.
64
65
 
66
+ ## The output-format prompt fragment (FR-010)
67
+
68
+ For every json/xml-format `template.output`, `codegen-kotlin`'s
69
+ `KotlinOutputPromptGenerator` emits a `<TemplateShortName>OutputPrompt.kt` `object`
70
+ with `renderFormat()` / `renderFormat(overrides: PromptOverrides)`, backed by
71
+ `OutputFormatRenderer` from the `metaobjects-render` module — the "produce your
72
+ answer like this" fragment for the model. Wire it alongside
73
+ `KotlinOutputParserGenerator` in the Maven plugin's `<generators>` list:
74
+
75
+ ```xml
76
+ <generator>
77
+ <classname>com.metaobjects.generator.kotlin.KotlinOutputPromptGenerator</classname>
78
+ <args><outputDir>${project.build.directory}/generated-sources/kotlin</outputDir></args>
79
+ </generator>
80
+ ```
81
+
82
+ `@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`)
83
+ controls the fragment's presentation; guidance is never emitted as comments. Skipped
84
+ for `template.prompt` nodes, non-json/xml `@format`, and unresolved `@payloadRef` —
85
+ the same skip contract as the parser generator. The `SPEC`'s root name is the
86
+ capitalized payload class name, agreeing with the parser's extract-codegen root.
87
+
65
88
  ## The three-step consumer pattern
66
89
 
67
90
  Render the prompt → call your LLM client (provider-agnostic; nothing is generated
@@ -115,9 +138,16 @@ val npc = NpcResponseParser.parseNpcResponse(response) // the generated parser
115
138
  (`ChatLanguageModel.generate(prompt)`) for non-Spring JVM — both provider-agnostic,
116
139
  both a one-call seam Kotlin uses idiomatically.
117
140
 
118
- > The typed-trace recorder + render→call→record convenience loop ship in TypeScript
119
- > today; the JVM port is planned (ADR-0024). Until then the call is your code and the
120
- > generated parser is the typed receive side.
141
+ > The typed-trace **recorder** has shipped on the JVM `codegen-spring`'s
142
+ > `LlmTraceHelperGenerator` emits a Java `record<Entity>(...)` helper (per concrete
143
+ > entity extending `LlmCallBase` with a `@responseRef`-carrying `template.prompt`);
144
+ > Kotlin code calls it directly (same JVM, same classpath) — there is no separate
145
+ > Kotlin-native (`codegen-kotlin`/KotlinPoet) trace-helper emitter yet. What's TS-only
146
+ > is the **`call<Entity>` render→call→record convenience loop** — neither JVM
147
+ > generator emits it, because the `LlmClient` seam it wraps is BYO / vendor-neutral on
148
+ > the JVM (ADR-0024). So you compose render → your LLM call → the generated Java
149
+ > `record<Entity>(...)` yourself; the parser above is the standalone receive side if
150
+ > you don't even want the recorder.
121
151
 
122
152
  ## Drift gate
123
153
 
@@ -10,6 +10,7 @@ drift.
10
10
  ## Contents
11
11
  - Wire the generators
12
12
  - What it emits
13
+ - The output-format prompt fragment (FR-010)
13
14
  - The three-step consumer pattern
14
15
  - Recommended LLM caller (bring-your-own)
15
16
  - Consumer dependency
@@ -54,6 +55,24 @@ best-effort variant — `extract_lenient_<name>(text) -> ExtractionResult[<Name>
54
55
  per-field report rather than a raise. The lenient mirror (`<Name>PayloadExtracted`)
55
56
  uses `Optional[...]` fields — a missing/malformed component is `None`, not a raise.
56
57
 
58
+ ## The output-format prompt fragment (FR-010)
59
+
60
+ For every json/xml-format `template.output`, the `output-prompt` generator (run via
61
+ `metaobjects gen`) emits one `<template_name>_output_prompt.py` module exposing
62
+ `render_<name>_format(overrides=None) -> str`, backed by the render engine's
63
+ `render_output_format()` — the "produce your answer like this" fragment for the
64
+ model:
65
+
66
+ ```bash
67
+ metaobjects gen ./metadata --out ./generated --generators payload,output-prompt
68
+ ```
69
+
70
+ `@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`)
71
+ controls the fragment's presentation; guidance is never emitted as comments. Skipped
72
+ for `template.prompt` nodes, non-json/xml `@format`, and unresolved `@payloadRef` —
73
+ the same skip contract as the `output-parser` generator. The baked spec's root name
74
+ is the payload class name, agreeing with the parser's `extract_<name>()` root.
75
+
57
76
  ## The three-step consumer pattern
58
77
 
59
78
  Render the prompt → call your LLM client (provider-agnostic; nothing is generated
@@ -96,9 +115,16 @@ to enforce the typed shape instead, **Instructor** or **Pydantic-AI** return a
96
115
  validated Pydantic model — but that overlaps MetaObjects' own typed `extract`, so pick
97
116
  one boundary, not both.
98
117
 
99
- > The typed-trace recorder + render→call→record convenience loop ship in TypeScript
100
- > today; the Python port is planned (ADR-0024). Until then the call is your code and
101
- > the generated parser is the typed receive side.
118
+ > The typed-trace **recorder** has shipped on this port too — the `trace-helper`
119
+ > generator emits a `record_<entity>(recorder, input, redact=None)` helper (per
120
+ > concrete entity extending `LlmCallBase` with a `@responseRef`/`@payloadRef`-carrying
121
+ > `template.prompt`) that tolerantly extracts the typed response, builds the base
122
+ > trace row, and persists it once. What's still TS-only is the **`call<Entity>`
123
+ > render→call→record convenience loop** — Python intentionally does not emit it,
124
+ > because the `LlmClient` seam it wraps is BYO / vendor-neutral here (ADR-0024). So
125
+ > you compose render → your LLM call → the generated `record_<entity>(...)` yourself;
126
+ > the parser above is the standalone receive side if you don't even want the
127
+ > recorder.
102
128
 
103
129
  ## Consumer dependency
104
130
 
@@ -9,6 +9,7 @@ call yourself.
9
9
  ## Contents
10
10
  - Wire the generator
11
11
  - What it emits
12
+ - The output-format prompt fragment (FR-010)
12
13
  - The three-step consumer pattern
13
14
  - Recommended LLM caller (bring-your-own)
14
15
  - Drift gate
@@ -66,7 +67,7 @@ The dual API mirrors Zod's idiomatic shape: `parse*` throws a `ZodError`,
66
67
  structurally identical to the `promptRender()` payload VO, so you can pass values
67
68
  between the render and parse sides interchangeably.
68
69
 
69
- Field-type → Zod mapping: `field.string` → `z.string()`; `field.int`/`long`/`short`/`byte`
70
+ Field-type → Zod mapping: `field.string` → `z.string()`; `field.int`/`long`
70
71
  → `z.number().int()`; `field.double`/`float` → `z.number()`; `field.boolean` →
71
72
  `z.boolean()`; `field.object` (with `@objectRef`) → a nested `z.object({...})`;
72
73
  `isArray: true` → wrapped in `z.array(...)`. Any subtype outside this scalar set —
@@ -75,6 +76,38 @@ including `field.enum` — falls through to `z.unknown()` in the strict
75
76
  in the entity insert/update schemas, not in this output parser; the lenient extract
76
77
  path carries the enum-as-string handling).
77
78
 
79
+ ## The output-format prompt fragment (FR-010)
80
+
81
+ For every json/xml-format `template.output`, the `outputPrompt()` generator (same
82
+ import path, `@metaobjectsdev/codegen-ts/generators`) emits a
83
+ `<TemplateName>.prompt.ts` exporting `render<TemplateName>Format(overrides?)` —
84
+ backed by the render engine's `renderOutputFormat()`. This is the "produce your
85
+ answer like this" fragment you splice into the prompt text so the model returns the
86
+ shape the parser above expects:
87
+
88
+ ```ts
89
+ // metaobjects.config.ts
90
+ import { entityFile, queriesFile, barrel, promptRender, outputParser, outputPrompt }
91
+ from "@metaobjectsdev/codegen-ts/generators";
92
+
93
+ export default defineConfig({
94
+ outDir: "src/generated",
95
+ generators: [
96
+ entityFile(), queriesFile(), barrel(),
97
+ promptRender(), outputParser(), outputPrompt(),
98
+ ],
99
+ });
100
+ ```
101
+
102
+ `@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`)
103
+ controls the fragment's presentation: `guide` is a prose field list + example
104
+ skeleton, `inline` is one skeleton with inline placeholders/enum choices,
105
+ `exampleOnly` is just the filled skeleton. Guidance is never emitted as comments —
106
+ models ignore them. Skipped for non-json/xml `@format` outputs and for any output
107
+ whose `@payloadRef` doesn't resolve to a value-object — the same skip contract as
108
+ `outputParser()`. The baked spec's root name matches the payload class name, so the
109
+ fragment and the parser's `extract()` agree on the same root.
110
+
78
111
  ## The three-step consumer pattern
79
112
 
80
113
  Render the prompt → call your LLM client (provider-agnostic; nothing is generated
@@ -76,7 +76,7 @@ a field → HTTP 400.
76
76
  | `in`, `like` | yes | `in` only | – |
77
77
  | `gt`, `gte`, `lt`, `lte` | – | yes | – |
78
78
 
79
- These eight (`eq` `ne` `gt` `gte` `lt` `lte` `in` `like` `isNull`) are the whole
79
+ These nine (`eq` `ne` `gt` `gte` `lt` `lte` `in` `like` `isNull`) are the whole
80
80
  closed set — every port implements these and only these.
81
81
 
82
82
  ### Sort + pagination
@@ -0,0 +1,90 @@
1
+ # C# server runtime
2
+
3
+ The C# runtime tier **is the generated EF Core stack itself** — there is no separate
4
+ `ObjectManager` engine to wire. `EntityGenerator` + `DbContextGenerator` emit
5
+ `<Entity>.g.cs` classes plus one `AppDbContext.g.cs` that ARE the persistence layer (EF
6
+ Core), and `RoutesGenerator` emits ASP.NET minimal-API routes that mount on your
7
+ `WebApplication`. Unlike the other ports, C# leaves **no repository seam** — the generated
8
+ code is what runs. Schema is TS-owned (ADR-0015); EF Core is pure data-access at runtime.
9
+
10
+ ## The generated persistence layer
11
+
12
+ `dotnet meta gen` emits an entity POCO per `object.entity` / projection and a single
13
+ `AppDbContext` with a `DbSet<T>` per entity (naively pluralized) plus an `OnModelCreating`
14
+ that carries the mapping:
15
+
16
+ ```csharp
17
+ // generated AppDbContext.g.cs
18
+ public class AppDbContext : DbContext
19
+ {
20
+ public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
21
+
22
+ public DbSet<Author> Authors { get; set; } = default!;
23
+
24
+ protected override void OnModelCreating(ModelBuilder b)
25
+ {
26
+ // .HasConversion<string>() (enums), .OwnsOne(...).ToJson(...) / .OwnsMany(...).ToJson(...)
27
+ // (jsonb object fields), .HasPrecision(p,s) (decimals), .ToView("v_...") (projections),
28
+ // .HasDiscriminator(...).HasValue<Sub>(...) (TPH single-table).
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## Query + persist with EF Core
34
+
35
+ Register the generated context against your provider, then query/write with plain LINQ +
36
+ `SaveChangesAsync` — the generated entities and `AppDbContext` are ordinary EF Core:
37
+
38
+ ```csharp
39
+ builder.Services.AddDbContext<AppDbContext>(o => o.UseNpgsql(connString));
40
+ // ...
41
+ var authors = await db.Authors.AsNoTracking()
42
+ .Where(a => a.Name == "Ada").ToListAsync();
43
+
44
+ db.Authors.Add(new Author { Name = "Ada" });
45
+ await db.SaveChangesAsync(); // server-generated PK round-trips back onto the entity
46
+ ```
47
+
48
+ ## Return-type contract
49
+
50
+ The EF Core query path materializes **native in-process CLR types**, never wire strings
51
+ (ADR-0019), verified by the port's runtime-return-type test:
52
+
53
+ - `field.decimal` → `decimal` — exact, **lossless end-to-end**, no float round-tripping.
54
+ - `field.long` → `long`; other scalars to their native CLR types.
55
+ - a default `field.timestamp` (instant) → `DateTimeOffset` over a `timestamptz` column; a
56
+ naive `@localTime:true` timestamp → `DateTime` — native temporals, not strings.
57
+ - an untyped `@dbColumnType:jsonb` open-JSON column → a `System.Text.Json.JsonDocument` (the
58
+ parsed value, not raw text); a **typed** `field.object @storage:jsonb` (with `@objectRef`)
59
+ materializes the generated value-object class via `OwnsOne`/`OwnsMany(...).ToJson(...)`.
60
+
61
+ Wire canonicalization (currency → integer minor units, temporals → ISO-8601, UUID →
62
+ canonical hex) happens only when a row leaves over HTTP — at the serialization boundary in
63
+ the generated routes — never inside the EF Core query path. Currency is integer minor units
64
+ on the wire and in storage (a native `long` in-process); the server never formats —
65
+ formatting is client-side in the universal (TS/Angular) web client.
66
+
67
+ ## Serving the REST contract
68
+
69
+ `RoutesGenerator` emits `<Entity>Routes.g.cs` — full CRUD per writable entity
70
+ (`source.rdb @kind="table"`), read-only list/get for a projection/view, and a collection-GET
71
+ for a composite-/no-PK entity — with a `Map<Entity>Routes(this IEndpointRouteBuilder,
72
+ string prefix = "/api")` extension, on the
73
+ cross-port REST contract (five CRUD endpoints, `?filter[field][op]=`, `?sort=field:asc`,
74
+ `?limit`/`?offset`, `?withCount=1` envelope, 400/404 envelopes). A TPH `@discriminator`
75
+ base emits polymorphic `GET /<base>(+/{id})` plus a per-subtype CRUD set at
76
+ `/<base>/<discriminatorValue lowercased>` (create injects the discriminator, cross-subtype
77
+ get/update/delete → 404). Wire it after registering the context:
78
+
79
+ ```csharp
80
+ var app = builder.Build();
81
+ app.MapAuthorRoutes("/api"); // the generated extension; you pass the prefix at mount time (default "/api")
82
+ app.Run();
83
+ ```
84
+
85
+ Filter operators (`eq` `ne` `gt` `gte` `lt` `lte` `in` `like` `isNull`) ship via the
86
+ per-entity `<Entity>FilterAllowlist` (from `FilterAllowlistGenerator`); the generated list
87
+ handler calls the `FilterParser` / `EfCoreFilterDispatch` runtime helpers in
88
+ `MetaObjects.Codegen`, so your ASP.NET host references that assembly at runtime. The same
89
+ universal TS/Angular web client consumes those routes unchanged — the wire format matches
90
+ the Java, Kotlin, and Python backends byte-for-byte.
@@ -0,0 +1,94 @@
1
+ # Python server runtime
2
+
3
+ The Python runtime tier is the **`metaobjects.runtime.ObjectManager`** — a
4
+ metadata-driven query/CRUD engine that reads the same metadata at runtime and drives
5
+ persistence with no per-entity ORM boilerplate. It is the cross-port analog of Java's
6
+ OMDB: a method-based API (`find_by_id` / `find_many` / `count` / `create` / `update` /
7
+ `delete` / `relate`) that compiles a Filter dict to parameterized SQL and runs it via a
8
+ pluggable driver. It is **pure data-access** (CRUD / query / codec); schema is owned by
9
+ the Node `meta` migration tool, not the runtime.
10
+
11
+ ## Construct an `ObjectManager`
12
+
13
+ `ObjectManager(root, driver)` takes a loaded metadata `root` plus a `PostgresDriver`
14
+ wrapping any DB-API 2 connection (pg8000 / psycopg):
15
+
16
+ ```python
17
+ import pg8000
18
+ from metaobjects import load_directory
19
+ from metaobjects.runtime import ObjectManager, PostgresDriver
20
+
21
+ result = load_directory("metaobjects") # same canonical JSON every port reads
22
+ conn = pg8000.connect(...) # any DB-API 2 connection
23
+ om = ObjectManager(result.root, PostgresDriver(conn))
24
+ ```
25
+
26
+ ## CRUD + query
27
+
28
+ Rows come back as **plain dicts keyed by metadata field name**, values native (see
29
+ below). Write calls take a field-keyed dict of values in their native authoring forms;
30
+ the write codec coerces each to the native Python type the driver binds:
31
+
32
+ ```python
33
+ # Create — returns the inserted row (incl. any server-generated PK) via RETURNING
34
+ author = om.create("Author", {"name": "Ada"})
35
+
36
+ # Read
37
+ one = om.find_by_id("Author", author["id"])
38
+ rows = om.find_many("Author", {"name": {"like": "Ada%"}},
39
+ sort=[("createdAt", "desc")], limit=25, offset=0)
40
+ n = om.count("Author", {"name": {"eq": "Ada"}})
41
+
42
+ # Update (partial, by PK) / Delete
43
+ om.update("Author", author["id"], {"name": "Ada Lovelace"}) # None if no row matched
44
+ om.delete("Author", author["id"]) # bool
45
+ ```
46
+
47
+ `create()` stamps `@autoSet` timestamps (`createdAt` / `updatedAt`) with a shared `now()`,
48
+ **overriding** any caller-supplied value, and `update()` strips `onCreate`-only columns. For
49
+ a data import/restore that must preserve original timestamps, use `insert_preserving()` (#203).
50
+
51
+ The Filter dict is `{field: value}` (equality shortcut), `{field: {op: value}}` (typed
52
+ ops), or `{"and": [filter, ...]}` (combinator). The operator set is the closed cross-port
53
+ nine — `eq` `ne` `gt` `gte` `lt` `lte` `in` `like` `isNull`. `relate()` traverses an M:N
54
+ relationship from a source record to its related rows (hetero / directed / symmetric
55
+ self-joins), resolved generically from the junction's `identity.reference` children.
56
+
57
+ ## Return-type contract
58
+
59
+ `ObjectManager` returns **native in-process Python types**, never wire strings (ADR-0019),
60
+ verified by the port's runtime-return-type test:
61
+
62
+ - `field.decimal` → `decimal.Decimal` — exact, **lossless end-to-end**, never via float.
63
+ - temporal fields → native `datetime` / `date` / `time`.
64
+ - `field.object` / jsonb → a native `dict` (pg8000 decodes jsonb).
65
+ - `field.uuid` → `uuid.UUID`; `field.currency` → a native `int` (integer minor units).
66
+
67
+ Wire canonicalization (currency → integer minor units, temporals → ISO-8601, UUID →
68
+ canonical hex) happens only at the serialization boundary, never inside the query path.
69
+ Compute with `Decimal` in-process; let the encoding layer emit the wire form. Currency is
70
+ integer minor units on the wire; the runtime never formats — formatting is client-side in
71
+ the universal (TS/Angular) web client. (Port-specific note: because pg8000 returns a plain
72
+ `int` for both INTEGER and BIGINT, `ObjectManager` exposes each query's per-column Postgres
73
+ OIDs via `last_column_oids` so the **canonical serializer** — the persistence-conformance
74
+ boundary — can apply the cross-port BIGINT→string rule. The generated FastAPI router serves
75
+ `field.long` as a JSON number, and the row *values* always stay native.)
76
+
77
+ ## Serving the REST contract
78
+
79
+ The `routes` generator (run via `metaobjects gen`) emits a **FastAPI `APIRouter`** per
80
+ writable entity (`source.rdb @kind="table"`) on the cross-port REST contract (five CRUD
81
+ endpoints, `?filter[field][op]=`, `?sort=field:asc`, `?limit`/`?offset`, `?withCount=1`
82
+ envelope, 400/404 envelopes). Each router declares a repository **`Protocol`** you
83
+ implement and inject via FastAPI's `app.dependency_overrides` — back it with
84
+ `ObjectManager`, or your own SQLAlchemy Core / asyncpg code. There is **no** generated
85
+ `main.py`; create one and mount the routers (`app.include_router(...)`). The same
86
+ universal TS/Angular web client consumes those routes unchanged.
87
+
88
+ Two shape notes (do **not** hand-edit generated files around them): the generated router
89
+ assumes a **single-field** PK (a composite PK takes `@fields[0]`), and the PK's type is
90
+ **derived from its field subtype** — a `field.uuid` PK binds `uuid.UUID` on the route and
91
+ Protocol, not `int`. The PATCH body is typed `dict[str, Any]` **deliberately**: the generated
92
+ `<Entity>Create` / `<Entity>Patch` Pydantic models already validate constraints over HTTP
93
+ (FR-036), and the `dict` seam preserves the FR-035 present-key PATCH tristate (absent ≠
94
+ explicit-null) — retyping the PATCH parameter to the model would collapse that tristate.
@@ -19,10 +19,21 @@ metadata that should define it. The ones a developer must actively guard:
19
19
  - **DB-vs-metadata** — the live database schema has diverged from the metadata
20
20
  (a column the metadata no longer declares, a missing index, a type mismatch). A
21
21
  **modeled projection's** view body is compared too — a changed `CREATE VIEW` emits
22
- a `replace-view`. But a **hand-authored, unmodeled view is *unmanaged*** (reported
23
- as informational, never failed, never dropped), so a hand-written view standing in
24
- for an expressible `object.projection` is the one drift class `verify --db` can't
25
- catchthe `metaobjects-audit` skill is the only gate that sees it.
22
+ a `replace-view`. For a genuinely irreducible view body (recursive CTE, window
23
+ function, set operation) that `origin.*` can't express, use the `source.rdb`
24
+ `@sql` escape: a hand-written body the tool still registers, fingerprints, and
25
+ drift-checksemitted verbatim instead of synthesized (adopt a pre-existing
26
+ hand-written view once with `meta migrate --allow adopt-view`). For a DB object
27
+ — view **or table** — owned entirely elsewhere (Flyway, a hand-migration,
28
+ another app's schema), mark it `@unmanaged: true`; `meta migrate` never
29
+ touches it and `verify --db` reports it as *external (declared)* rather than
30
+ silently. `@sql` and `@unmanaged` are mutually exclusive on one source. But a
31
+ hand-authored view carrying **neither** marker is *unmanaged by omission*
32
+ (reported as informational, never failed, never dropped) — so an **undeclared**
33
+ hand-written view standing in for an expressible `object.projection` is the one
34
+ drift class `verify --db` can't catch; the `metaobjects-audit` skill is the only
35
+ gate that sees it. See `references/migration.md` → "DDL-ownership escape
36
+ valves (`@sql` / `@unmanaged`) — #208" for the full contract.
26
37
  - **Generated-vs-metadata (codegen)** — committed generated code no longer matches
27
38
  what the current metadata would emit (someone edited a `@generated` file, or
28
39
  forgot to regenerate after changing metadata).
@@ -49,16 +60,24 @@ it and call the generated query/field instead of keeping the hand-rolled version
49
60
  This is the most common way a build ends up *declaring* a projection yet still
50
61
  hand-aggregating in a route — verify catches exactly that.
51
62
 
63
+ **A bare `verify` is a partial check, not the full gate.** The Node/C# default runs
64
+ only `--templates`; Java/Python's bare default runs only `--codegen` — either way,
65
+ paired with the advisory anti-pattern pass above, never all three subverbs. Treat a
66
+ bare run as a smoke test: the real done-check is running the subverbs your project
67
+ uses explicitly — `verify --codegen`, and, where a DB exists, `verify --db <url>`.
68
+
52
69
  ## The `verify` subverbs
53
70
 
54
71
  `verify` has three drift checks. Run them in CI.
55
72
 
56
73
  - **`--db`** — schema drift. Introspects the live database and fails if it has
57
74
  diverged from metadata. This is a **schema concern, so it is the Node toolchain's
58
- job regardless of your server language** (see migrations below). On the JVM ports
59
- a runtime startup validator *can* catch generated-table drift at app boot as an
60
- optional complementary check (if your project wires one), but the authoritative
61
- DB-vs-metadata gate is the Node `verify --db`.
75
+ job regardless of your server language** (see migrations below). The JVM ports
76
+ have no schema surface of their own — ADR-0015 Decision 2 removed the old
77
+ dev/test auto-create validator (OMDB is pure data-access) — so a JVM, Python, or
78
+ C# project's dev/test databases are provisioned the same way production is: by
79
+ applying the Node-emitted SQL. The Node `verify --db` is the only DB-vs-metadata
80
+ gate, for every port.
62
81
 
63
82
  - **`--codegen`** — regeneration drift. Re-runs generation and diffs the result
64
83
  against the committed generated files; a non-empty diff means someone edited
@@ -70,6 +89,20 @@ hand-aggregating in a route — verify catches exactly that.
70
89
  if any reference isn't on the payload VO. This is the build-time gate for the
71
90
  prompt-construction pillar.
72
91
 
92
+ **Only `--db` is Node-universal.** `--codegen` / `--templates` run through each
93
+ port's own build tool, not the Node `meta`:
94
+
95
+ | Port | Codegen drift | Template drift | Schema drift |
96
+ |---|---|---|---|
97
+ | TS | `meta verify --codegen` | `meta verify --templates` | `meta verify --db <url>` |
98
+ | Java / Kotlin | `mvn metaobjects:verify -Dmeta.verify.mode=codegen` | `mvn metaobjects:verify -Dmeta.verify.mode=templates` | Node `meta verify --db` only |
99
+ | C# | `dotnet meta verify --codegen` | `dotnet meta verify --templates` | Node `meta verify --db` only |
100
+ | Python | `metaobjects verify --codegen` | `metaobjects verify --templates` | Node `meta verify --db` only |
101
+
102
+ Every non-TS port's `verify` rejects `--db` outright (exit 2, "schema verify is the
103
+ migrate engine") — schema drift always runs through the Node `meta verify --db`,
104
+ per the shared-migration-engine doctrine below.
105
+
73
106
  A clean run is silent; a failure names the entity/template, the drifted artifact,
74
107
  and (for templates) the missing reference. **Bias toward trusting the tool** — a
75
108
  verify failure almost always means the metadata changed and a derived artifact
@@ -121,9 +154,11 @@ What this means in practice:
121
154
 
122
155
  - Dialects: `postgres` (default), `sqlite`, and `d1` (Cloudflare D1, TS-only).
123
156
  - The JVM and Python ports have **no** migration command of their own — their
124
- former migrate goals/modules were removed. A JVM service may auto-create
125
- dev/test tables at startup for convenience, but production schema is always the
126
- Node migrate engine's output.
157
+ former migrate goals/modules were removed, and (ADR-0015 Decision 2) the JVM
158
+ runtime's own dev/test schema auto-create path
159
+ (`MetaClassDBValidatorService` + the drivers' DDL) was removed too: OMDB is
160
+ pure data-access. Every port's schema — dev, test, and production alike — is
161
+ always the Node migrate engine's output.
127
162
 
128
163
  So even in a Java or Python or C# project, schema migration and `verify --db` run
129
164
  through the Node `meta` tool. The per-port `gen`/codegen tooling stays native to
@@ -136,9 +171,9 @@ database by hand** — no `psql`/console `ALTER TABLE` / `CREATE` / `DROP`, not
136
171
  to patch a mismatch, not to "just unblock" a boot. It is the single most common way a database ends up
137
172
  in a state no migration can reproduce:
138
173
 
139
- - The column now exists but no migration recorded it, so the next `meta migrate` (or a JVM app's
140
- boot-time migrator) tries to add it again and dies on `column ... already exists` — or worse,
141
- silently diverges and the drift only surfaces days later.
174
+ - The column now exists but no migration recorded it, so the next `meta migrate` tries to add it
175
+ again and dies on `column ... already exists` — or worse, silently diverges and the drift only
176
+ surfaces days later.
142
177
  - "I'll just add it real quick so I can see it in the tool" is the exact rationalization to catch. It
143
178
  doesn't *feel* like a schema change, so it skips the metadata-first check — but it is one.
144
179
 
@@ -159,7 +194,7 @@ render, persistence, API-contract, verify). When a test or conformance fixture
159
194
  fails:
160
195
 
161
196
  - A **loader** failure cites an `ERR_*` code (e.g. `ERR_RESERVED_ATTR`,
162
- `ERR_UNKNOWN_EXTENDS`, `ERR_MISSING_REQUIRED_ATTR`, `ERR_BAD_ATTR_VALUE`,
197
+ `ERR_UNRESOLVED_SUPER`, `ERR_MISSING_REQUIRED_ATTR`, `ERR_BAD_ATTR_VALUE`,
163
198
  `ERR_YAML_COERCION`) — fix the metadata, not the loader.
164
199
  - A **render/verify** failure means the rendered bytes or the template-drift
165
200
  result diverged from the pinned expectation — usually a payload/text mismatch.
@@ -4,9 +4,11 @@ Schema migration is owned by **one shared TypeScript engine** regardless of your
4
4
  server language (ADR-0015). The Node `meta` CLI (`@metaobjectsdev/cli`, on top of
5
5
  `@metaobjectsdev/migrate-ts`) is the migration + live-DB-drift toolchain for **TS,
6
6
  Java, Kotlin, C#, and Python alike**. The non-TS ports have **no** migration command
7
- of their own — their former migrate goals/modules were removed. A JVM service may
8
- auto-create dev/test tables at startup for convenience, but production schema is
9
- always the Node migrate engine's output.
7
+ of their own — their former migrate goals/modules were removed, and (ADR-0015
8
+ Decision 2) the JVM runtime's own dev/test schema auto-create path
9
+ (`MetaClassDBValidatorService` + the drivers' DDL) was removed too: OMDB is pure
10
+ data-access. Every port's schema — dev, test, and production alike — is always the
11
+ Node migrate engine's output.
10
12
 
11
13
  So even in a Java / Python / C# / Kotlin project you run `meta migrate` and
12
14
  `meta verify --db` through Node. Only schema crosses to Node; per-port `gen`/codegen
@@ -78,8 +80,9 @@ next-step hint pointing to the exact `baseline` command.
78
80
  a ledger table:
79
81
 
80
82
  ```bash
81
- meta migrate --db postgresql://... --apply # run pending up.sql
82
- meta migrate --db postgresql://... --rollback # run down.sql for the last migration
83
+ meta migrate --db postgresql://... --apply # run pending up.sql
84
+ meta migrate --db postgresql://... --rollback <target> # run down.sql for migrations newer than <target>
85
+ meta migrate --db postgresql://... --rollback "" # roll back everything (empty target)
83
86
  ```
84
87
 
85
88
  ## Dialects
@@ -95,9 +98,9 @@ next-step hint pointing to the exact `baseline` command.
95
98
  `meta verify --db` introspects the live database and fails if its schema has
96
99
  diverged from the metadata (a column the metadata no longer declares, a missing
97
100
  index, a type mismatch). This is the **authoritative** DB-vs-metadata gate for every
98
- port — wire it into CI. On the JVM ports a runtime startup validator can catch
99
- generated-table drift at app boot as a complementary check, but the gate that owns
100
- DB drift is the Node `meta verify --db`.
101
+ port — wire it into CI. The JVM ports have no runtime schema-validation surface of
102
+ their own (ADR-0015 Decision 2 removed it); the Node `meta verify --db` is the only
103
+ gate that owns DB drift, for every port.
101
104
 
102
105
  A clean run is silent; a failure names the drifted table/column. Bias toward
103
106
  trusting the tool — a drift failure almost always means the metadata changed and the
@@ -129,8 +132,10 @@ A non-unique recency index is `index.lookup`:
129
132
 
130
133
  The `@where` / `@using` / `@expr` / `@orders` attributes are **index** physical
131
134
  escapes on `identity.secondary` / `index.lookup` — they are NOT a raw-SQL escape
132
- hatch for views. There is no attribute that injects hand-written SQL into a
133
- projection view body (by design — see below).
135
+ hatch for views. For a genuinely-irreducible view body, use the `source.rdb`
136
+ **`@sql`** escape (a tool-managed, opaque hand-written body — see the
137
+ "DDL-ownership escape valves" section below); for a DB object owned entirely by
138
+ Flyway / a hand-migration, use **`@unmanaged`**.
134
139
 
135
140
  ## Projection views (generated view DDL)
136
141
 
@@ -151,10 +156,58 @@ canonical view-SQL emitter shared with drift detection.
151
156
  `metaobjects-audit` skill, not here.
152
157
 
153
158
  **Do not hand-author view SQL for a shape origins can express** — model it as a
154
- projection so the view DDL is generated and drift-checked. The only case for
155
- hand-written view DDL is a genuinely irreducible view (recursive CTE, window
156
- function, set operation) that origins can't express; carry that in a hand-edited
157
- migration file.
159
+ projection so the view DDL is generated and drift-checked. For a genuinely
160
+ irreducible view (recursive CTE, window function, set operation) that origins
161
+ can't express, use the `@sql` escape below rather than a hand-edited migration
162
+ file — that keeps the view tool-managed (emitted, fingerprinted, drift-checked)
163
+ instead of accidentally unmanaged.
164
+
165
+ ## DDL-ownership escape valves (`@sql` / `@unmanaged`) — #208
166
+
167
+ Two mutually-exclusive `source.rdb` attributes express *who owns a DB object's
168
+ DDL* (ADR-0043). They are the escape from "a projection's view is always
169
+ synthesized from its `origin.*` children."
170
+
171
+ **`@sql`** — a hand-written view body the tool **registers, fingerprints, and
172
+ drift-checks but never authors or parses**. The value is the body *inside*
173
+ `CREATE VIEW <name> AS …` (not the `CREATE` wrapper, not the name). Legal only on
174
+ a read-only kind; v1 migrate lowers it on `@kind: view` only (matview/proc → a
175
+ hard error, deferred). Authored sigil-free in YAML as a block scalar:
176
+
177
+ ```yaml
178
+ object.projection:
179
+ name: OrgTree
180
+ children:
181
+ - source.rdb:
182
+ kind: view
183
+ view: v_org_tree
184
+ sql: |
185
+ WITH RECURSIVE t AS (
186
+ SELECT id, parent_id FROM org WHERE parent_id IS NULL
187
+ UNION ALL SELECT o.id, o.parent_id FROM org o JOIN t ON o.parent_id = t.id)
188
+ SELECT * FROM t
189
+ - field.long: { name: id, extends: Org.id }
190
+ - identity.primary: { extends: Org.pk }
191
+ ```
192
+
193
+ The `extends`-bound identity/fields declare the read model's shape and row
194
+ identity *without* triggering wrong synthesis (the suppression rule). The view is
195
+ emitted verbatim with a fingerprint COMMENT stamp; a second `meta migrate` is a
196
+ no-op. **Adopting a pre-existing hand-written view** at that name: the first diff
197
+ reports `replace-view` **blocked** (an unstamped view is indistinguishable from
198
+ someone else's SQL) — run **`meta migrate --allow adopt-view`** once to stamp it,
199
+ then it converges. `@sql` **forbids** `origin.*` children and a `@filter` on the
200
+ same host (two sources of truth → load error).
201
+
202
+ **`@unmanaged: true`** — this DB object (a view **or a table**) is managed
203
+ elsewhere (Flyway / a hand-migration owns its DDL). `meta migrate` never creates,
204
+ drops, or drift-checks it; `meta verify --db` reports it as *external (declared)*.
205
+ Legal on any `@kind`, including `table` (the Flyway-owned-entity case). An FK from
206
+ a managed table into an `@unmanaged` table resolves its physical name, but the
207
+ external object must exist before that FK is applied (a documented ordering caveat,
208
+ not enforced).
209
+
210
+ `@sql` and `@unmanaged` are **mutually exclusive** on one source.
158
211
 
159
212
  ## Adopting an existing database (non-destructive)
160
213