@metaobjectsdev/sdk 0.16.0 → 0.17.0

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
@@ -46,11 +46,13 @@ Every emitted file carries a `@generated` header. This is load-bearing:
46
46
  Practical rule: **pattern-derivable-from-metadata = regenerate; business logic =
47
47
  hand-write in a non-generated file.** FK columns, CRUD, validator chains,
48
48
  type-safe finders, `relations()` blocks — all derived, never hand-coded. What you
49
- hand-write is what metadata genuinely can't express: regex from outside metadata,
50
- domain logic, and *irreducible* SQL views (recursive CTEs, window functions, set
51
- ops). Most views are NOT irreducible model them as an `object.projection` and the
52
- view DDL is generated (see the projection bullet below); a hand-written view for a
53
- shape origins can express is drift the drift gate can't even see.
49
+ hand-write is what metadata genuinely can't express: regex from outside metadata and
50
+ domain logic. Most views are NOT irreducible model them as an `object.projection`
51
+ and the view DDL is generated (see the projection bullet below); a hand-written view
52
+ for a shape origins can express is drift the drift gate can't even see. A genuinely
53
+ *irreducible* view body (recursive CTE, window function, set op) isn't hand-written
54
+ loose either — it goes in the `source.rdb` **`@sql`** escape (#208, ADR-0043) so the
55
+ tool still registers, fingerprints, and drift-checks it (see the projection bullet).
54
56
 
55
57
  ## Selecting generators by stable name
56
58
 
@@ -81,23 +83,50 @@ the data access too.
81
83
  standard verbs with the runtime helpers and hand-write only the custom ones (see
82
84
  the runtime skill's `mountCrudRoutes` / `mount<Verb>Route` / `expose`). You are
83
85
  never forced into all-generated or all-hand-written.
86
+ - **Entity's OWN columns + a joined extra → an entity read-view, NOT a projection.**
87
+ The most common legacy view is `SELECT o.*, c.name AS customer_name FROM orders o
88
+ JOIN customers c …` — the entity *with a read route*, not an independent exposure.
89
+ Reach for an **entity read-view** first: keep the entity's writable `table` source
90
+ and add a **non-primary** read-only source (`source.rdb` `@role: replica`
91
+ `@kind: view`), declaring only the *extra* as a derived (`origin.*`) field — the
92
+ entity's own field set already covers `o.*`, so you re-state nothing but the extra.
93
+ Codegen then routes **reads** to the view and **writes** to the table (derived
94
+ fields don't exist there and are excluded from the write codecs); a create/update
95
+ re-reads the row through the view by primary key, so the returned value carries the
96
+ derived columns (read-your-writes). Shipped all five ports (#213 write half + #214
97
+ read half). Reach for a **projection** (below) instead only when it is an
98
+ independent exposure contract — a subset, renamed base columns, a versioned/external
99
+ shape, or a row-filtered view. See `docs/features/source-kinds.md`.
84
100
  - **Derived/aggregate data → declare a projection, then USE its generated query.**
85
101
  Don't hand-write a join or an `AVG()`/`COUNT()`. Declare an `object.projection`
86
- with `origin.aggregate` / `origin.passthrough` / `origin.collection` children
87
- **and a read-only `source.rdb` `@kind: view` child** (codegen detects a
88
- projection by that read-only source, not by the subtype alone — omit it and
89
- nothing is generated). `meta gen` emits a read-only query for it (and
90
- `meta migrate` its DB view), and you **call that generated query from your
91
- route**. Declaring the projection is only half the win *consuming* its
92
- generated query is the other half.
102
+ with `origin.*` children `origin.passthrough` (a forwarded column),
103
+ `origin.aggregate` (`@agg` `count`/`sum`/`avg`/`min`/`max`, plus the #195
104
+ `any`/`all` predicate quantifiers over a `@filter` and `collect` array-rollup with
105
+ optional `@distinct`/`@orderBy`; any aggregate may be row-scoped with `@filter`),
106
+ `origin.collection` (a nested array), `origin.computed` (a row-level `@expr`), and
107
+ `origin.first` (one related row's column along `@via`/`@of`/`@orderBy`)**and a
108
+ read-only `source.rdb` `@kind: view` child** (codegen detects a projection by that
109
+ read-only source, not by the subtype alone — omit it and nothing is generated).
110
+ `meta gen` emits a read-only query for it (and `meta migrate` its DB view), and you
111
+ **call that generated query from your route**. Declaring the projection is only half
112
+ the win — *consuming* its generated query is the other half.
113
+ - **Row-filtered views are a projection `@filter`, not hand-written SQL.** An
114
+ object-level `@filter` on `object.projection` (the same `attr.filter` shape as a
115
+ preset filter) scopes the whole view's rows — it lowers to the view's outer
116
+ `WHERE` (#207). This is the metadata-managed way to author a soft-delete / status
117
+ / type view without hand-writing SQL.
93
118
  - **Never hand-author the view SQL for a shape origins can express.** The
94
119
  `CREATE VIEW` body is emitted by the Node `meta migrate` from the projection's
95
120
  `origin.*` children — hand-writing it is a second source of truth that drifts
96
121
  silently, because an unmodeled DB view is *unmanaged*: `meta verify --db` never
97
- flags it. Hand-written view SQL is legitimate only when a named construct
98
- origins can't express (recursive CTE, window function, set op) blocks
99
- projection authoringthen keep that DDL in a hand-edited migration file and
100
- justify it in review.
122
+ flags it. For a genuinely irreducible body (recursive CTE, window function, set
123
+ op) that origins can't express, carry it in the `source.rdb` **`@sql`** escape
124
+ (#208, ADR-0043)a hand-written body the tool registers, fingerprints, and
125
+ drift-checks (adopt a pre-existing view with `meta migrate --allow adopt-view`) —
126
+ rather than a hand-edited migration file where it goes accidentally unmanaged.
127
+ For a DB object owned entirely elsewhere (Flyway), mark its source
128
+ **`@unmanaged: true`** (view or table); migrate/verify then never touch it.
129
+ `@sql` and `@unmanaged` are mutually exclusive.
101
130
 
102
131
  `meta gen --list` prints every generator by stable name; the `generators` array in
103
132
  `metaobjects.config.ts` is where you opt each one in or out.
@@ -45,13 +45,33 @@ or run the default set. Output lands under `--namespace` in `--output-dir`.
45
45
  | `routes` | `<Entity>Routes.cs` — ASP.NET **Minimal API** CRUD per writable entity (`source.rdb @kind="table"`) on the cross-port REST contract (`?filter[field][op]=`, `?sort=field:asc`, `?limit`/`?offset`, `?withCount=1` envelope, 400/404 envelopes). A TPH base emits polymorphic `GET /<base>(+/{id})` + a per-subtype CRUD set at `/<base>/<discriminatorValue lowercased>` (create injects the discriminator, cross-subtype get/update/delete → 404). |
46
46
  | `filter-allowlist` | per-entity `<Entity>FilterAllowlist` (FR-009 — the server-side field+operator allowlist the routes validate against). |
47
47
  | `callable` | `<Entity>.callable.g.cs` — an FR-015 calling method for a `source.rdb @kind="storedProc"|"tableFunction"`, via EF `FromSqlInterpolated` (args from the `@parameterRef` value object in declaration order). |
48
- | `output-parser` / `extractor` / `output-prompt` / `render-helper` | the `template.output` prompt-pillar artifacts (strict parser, tolerant `extract`, output-format prompt fragment, typed render helper) see the **prompts** reference. |
48
+ | `payload` | `<Payload>.payload.cs` the strict typed payload `record` (+ any nested element records) per `template.output` `@payloadRef` (an `object.value`) that the parser/extractor bind to. |
49
+ | `output-parser` / `extractor` / `output-prompt` / `render-helper` | the `template.output` prompt-pillar artifacts — the strict parser, the tolerant `extract`, the **output-format prompt fragment** (`output-prompt`; presentation via `@promptStyle: guide`/`inline`/`exampleOnly`), and the typed render helper. See the **prompts** reference. |
49
50
  | `template` | the generic Mustache `templateGenerator()` primitive. |
50
51
 
51
52
  Metadata lives under `metaobjects/` (or wherever you point `--metadata-dir`) in the
52
53
  same canonical JSON every port reads — fused-key form, `source.rdb` + `@table`,
53
54
  `@column` for a renamed physical column.
54
55
 
56
+ **Entity read-view (write-through).** An `object.entity` that keeps its writable `table`
57
+ primary source and adds a `@role: replica` `@kind: view` source is a write-through
58
+ read-view (#214): the generated EF entity carries the derived `origin.*` fields read-only
59
+ and `db-context` registers that read model against the replica view (`.ToView(...)`);
60
+ reads route to the view, writes to the table (derived fields excluded), and a
61
+ create/update re-reads the row via the view by primary key (read-your-writes). The replica
62
+ view's DDL is emitted by the Node `meta migrate` from the same origin assembly as a
63
+ projection view.
64
+
65
+ ## Docs — `dotnet meta docs`
66
+
67
+ ```bash
68
+ dotnet meta docs metaobjects --out Docs # → Docs/api/csharp (AGENT-API.md + per-entity pages)
69
+ ```
70
+
71
+ `dotnet meta docs` emits this project's C# SDK api surface (`api/csharp`), including
72
+ `AGENT-API.md` — the exact imports, signatures, and payload field shapes for the
73
+ generated code. **Before calling any generated code, read `api/csharp/AGENT-API.md`.**
74
+
55
75
  ## Persistence + routes are the deployed artifact
56
76
 
57
77
  C# generates a *complete* server stack: the entity classes + `AppDbContext` ARE the
@@ -74,6 +74,19 @@ A `metaobjects:verify` Maven goal exists for **codegen-drift** (re-generate and
74
74
  committed output). Schema migration and live-DB drift are NOT Java goals — they run
75
75
  through the Node `meta` tool (see the migration reference).
76
76
 
77
+ ## Docs — `mvn metaobjects:docs`
78
+
79
+ A separate `docs` goal (`DocsMojo` — NOT a `<generator>`) emits this project's SDK api
80
+ surface (default `target/docs/api/java`), including `AGENT-API.md` — the exact imports,
81
+ signatures, and payload field shapes for the generated code.
82
+
83
+ ```bash
84
+ mvn metaobjects:docs # → target/docs/api/java (AGENT-API.md + per-entity pages)
85
+ ```
86
+
87
+ **Before calling any generated code, read `api/java/AGENT-API.md`** — it carries the
88
+ concrete imports and signatures so you don't have to guess them.
89
+
77
90
  ## `codegen-spring` generators
78
91
 
79
92
  All live in `metaobjects-codegen-spring` under
@@ -82,19 +95,44 @@ first group together:
82
95
 
83
96
  | Generator | Output |
84
97
  |---|---|
85
- | `SpringControllerGenerator` | `<Entity>Controller.java` per writable entity (`source.rdb` `@kind="table"`) — Spring Web MVC, five CRUD endpoints on the cross-port REST contract (`?sort`, `?limit`/`?offset`, `?withCount=1` envelope, 404/400 envelopes). A TPH `@discriminator` base emits ONE controller: polymorphic `GET /<base>(+/{id})` plus a per-subtype CRUD set at `/<base>/<discriminatorValue lowercased>` — create injects the discriminator from the URL (never the body); get/update/delete scoped to the subtype (cross-subtype → 404); discriminator immutable. |
98
+ | `SpringControllerGenerator` | `<Entity>Controller.java` per writable entity (`source.rdb` `@kind="table"`) — Spring Web MVC, five CRUD endpoints on the cross-port REST contract (`?filter[field][op]=`, `?sort`, `?limit`/`?offset`, `?withCount=1` envelope, 404/400 envelopes). A TPH `@discriminator` base emits ONE controller: polymorphic `GET /<base>(+/{id})` plus a per-subtype CRUD set at `/<base>/<discriminatorValue lowercased>` — create injects the discriminator from the URL (never the body); get/update/delete scoped to the subtype (cross-subtype → 404); discriminator immutable. |
86
99
  | `SpringDtoGenerator` | `<Entity>Dto.java` as a Java 21 `record`; wrapped primitives (`Long`/`Integer`/`Boolean`) so missing JSON props deserialise to `null`; currency = `Long` (integer minor units). A TPH `@discriminator` base's DTO is the **union** of every subtype's columns (subtype-only fields folded nullable, validation dropped), so one wire shape backs the polymorphic + per-subtype endpoints. |
87
100
  | `SpringRepositoryGenerator` | `<Entity>Repository.java` — a hand-stubbed `interface` the consumer implements with their persistence layer (Spring Data JPA / jOOQ / JDBC). For a TPH base the interface is polymorphic + per-subtype-scoped (`listByType`/`findByIdAndType`/`createWithType`/`updateByIdAndType`/`deleteByIdAndType`) over the single table; subtype entities emit no own controller/DTO/repository — they fold into the base. |
88
- | `SpringPayloadGenerator` | a Java 21 `record` per template payload VO |
89
- | `SpringOutputParserGenerator` | the `template.output` parser-on-receipt (see the prompts reference) |
101
+ | `SpringValueObjectGenerator` | a Java 21 `record` per `object.value` reached through a `field.object @storage: jsonb` column (single or `@isArray`, transitively through nested VOs) — the typed component the Jackson jsonb codec serializes to/from (carries jakarta validation, unlike a plain payload record). Program D typed-jsonb VOs. |
102
+ | `SpringPayloadGenerator` | a Java 21 `record` per `template` payload VO |
103
+ | `SpringOutputParserGenerator` | the `template.output` strict parser-on-receipt (see the prompts reference) |
104
+ | `SpringOutputPromptGenerator` | the FR-010 output-format prompt fragment for a `template.output` (presentation via `@promptStyle: guide`/`inline`/`exampleOnly`) |
105
+ | `SpringRenderHelperGenerator` | the typed render helper for a `template.prompt` payload |
106
+ | `LlmTraceHelperGenerator` | `<Entity>TraceHelper.java` per concrete entity — the LLM-trace helper |
90
107
  | `SpringFilterAllowlistGenerator` | per-entity filter allowlist |
91
108
 
92
109
  **Projections (read-only views).** An `object.projection` (read-only `source.rdb`
93
110
  `@kind: view` child) is served read-only through OMDB at the ObjectManager layer
94
111
  (mutating ops throw); no controller is generated (controllers cover writable entities
95
112
  only). Its `CREATE VIEW` DDL is emitted by the Node `meta migrate` from the
96
- projection's `origin.*` children — never hand-author the view SQL for a shape origins
97
- can express (an unmodeled view is unmanaged and drifts silently).
113
+ projection's `origin.*` children — `origin.passthrough`, `origin.aggregate` (`@agg`
114
+ `count`/`sum`/`avg`/`min`/`max`, plus the #195 `any`/`all` quantifiers over a `@filter`
115
+ and `collect` array-rollup with `@distinct`/`@orderBy`), `origin.collection`,
116
+ `origin.computed` (`@expr`), `origin.first`; an object-level `@filter` scopes the whole
117
+ view's rows (#207 — lowers to the outer `WHERE`). Never hand-author the view SQL for a
118
+ shape origins can express (an unmodeled view is unmanaged and drifts silently); carry a
119
+ genuinely irreducible body (recursive CTE, window function, set op) in the `source.rdb`
120
+ **`@sql`** escape (#208) so the tool still owns it, or mark a Flyway-owned object
121
+ `@unmanaged: true`.
122
+
123
+ **Entity read-view (write-through).** An `object.entity` that keeps its writable `table`
124
+ primary source and adds a `@role: replica` `@kind: view` source is a write-through
125
+ read-view (#214): OMDB routes reads through the replica view and writes to the table
126
+ (derived `origin.*` fields excluded from the write path); a create/update re-reads the
127
+ row via the view by primary key (read-your-writes). The replica view's DDL is emitted by
128
+ `meta migrate` from the same origin assembly as a projection view.
129
+
130
+ ### Value-object jsonb columns
131
+
132
+ A `field.object` with `@storage: jsonb` (single or `@isArray`) is a typed jsonb column
133
+ backed by an `object.value`: `SpringValueObjectGenerator` emits that VO as a Java record
134
+ and OMDB's Jackson codec serializes it to/from the column (a single VO, or `List<VO>` for
135
+ an array) — the same typed-jsonb round-trip the other ports ship.
98
136
 
99
137
  Metadata lives under `src/main/metaobjects/` in the same canonical JSON the other
100
138
  ports read — fused-key form, `source.rdb` + `@table`, `@column` for a renamed
@@ -88,6 +88,19 @@ A `metaobjects:verify` Maven goal exists for **codegen-drift** (re-generate and
88
88
  vs committed output). Schema migration and live-DB drift are NOT JVM goals — they run
89
89
  through the Node `meta` tool (see the migration reference).
90
90
 
91
+ ## Docs — `mvn metaobjects:docs -Dmetaobjects.docs.language=kotlin`
92
+
93
+ The shared `docs` goal (`DocsMojo`) renders the Kotlin SDK api surface via
94
+ `KotlinApiDocsRenderer` when pointed at the Kotlin language (default `target/docs/api/kotlin`),
95
+ including `AGENT-API.md` — the exact imports, signatures, and payload field shapes for the
96
+ generated code.
97
+
98
+ ```bash
99
+ mvn metaobjects:docs -Dmetaobjects.docs.language=kotlin # → target/docs/api/kotlin (AGENT-API.md + per-entity pages)
100
+ ```
101
+
102
+ **Before calling any generated code, read `api/kotlin/AGENT-API.md`.**
103
+
91
104
  ## `codegen-kotlin` generators
92
105
 
93
106
  All live in `metaobjects-codegen-kotlin` under
@@ -99,8 +112,12 @@ All live in `metaobjects-codegen-kotlin` under
99
112
  | `KotlinExposedTableGenerator` | `<Entity>Table.kt` — Exposed `Table` object (PK + FK + `@storage` columns) for entities with `source.rdb`. A TPH `@discriminator` base emits ONE `Table` for the whole hierarchy — every subtype-only column folded in `.nullable()` (a row of another subtype stores null there) — single-table inheritance; subtype entities emit no table of their own. |
100
113
  | `KotlinRelationsGenerator` | `<Entity>Relations.kt` — extension fns for `@cardinality="many"` query helpers |
101
114
  | `KotlinSpringControllerGenerator` | `<Entity>Controller.kt` — Spring `@RestController`, five CRUD endpoints on the cross-port REST contract, for writable entities (`source.rdb` `@kind="table"`). A TPH `@discriminator` base emits ONE controller: polymorphic `GET /<base>(+/{id})` plus a per-subtype CRUD set at `/<base>/<discriminatorValue lowercased>` — create injects the discriminator from the URL (never the body); get/update/delete are scoped to the subtype (cross-subtype → 404); the discriminator is immutable. |
115
+ | `KotlinRepositoryGenerator` | `<Entity>RepositoryBase.kt` — an `open class` consumer persistence seam per writable entity (the Kotlin peer of Java's `SpringRepositoryGenerator`, but with method bodies). Carries the #203 `@autoSet` CRUD stamping: `onCreate` columns stamped once at insert, `onUpdate` columns re-stamped on every write, both excluded from the caller-supplied set. |
102
116
  | `KotlinPayloadGenerator` | `<Template>Payload.kt` — `@Serializable` payload data class from a template's `@payloadRef` |
103
- | `KotlinOutputParserGenerator` | the `template.output` parser-on-receipt (see the prompts reference) |
117
+ | `KotlinOutputParserGenerator` | the `template.output` strict parser-on-receipt (see the prompts reference) |
118
+ | `KotlinExtractorGenerator` | the FR-010 tolerant `extract` mapper for a `template.output` (all-nullable mirror → strict payload) |
119
+ | `KotlinOutputPromptGenerator` | the FR-010 output-format prompt fragment for a `template.output` (presentation via `@promptStyle: guide`/`inline`/`exampleOnly`) |
120
+ | `KotlinRenderHelperGenerator` | the typed render helper for a `template.prompt` payload |
104
121
  | `KotlinValidatorGenerator` | `MetadataStartupValidator.kt` + `ExposedTableValidator.kt` (once per project) |
105
122
  | `KotlinSpringConfigGenerator` | `MetadataExposedConfig.kt` — `@Configuration` wiring `Database.connect()` + the startup validator (once per project) |
106
123
  | `KotlinStoredProcGenerator` | stored-procedure call wrappers for `source.rdb` `@kind="storedProc"` |
@@ -109,9 +126,22 @@ All live in `metaobjects-codegen-kotlin` under
109
126
  **Projections (read-only views).** For an `object.projection` (read-only `source.rdb`
110
127
  `@kind: view` child), `KotlinExposedTableGenerator` emits a read-only Exposed `Table`
111
128
  wrapper (same column mapping, no write path). The `CREATE VIEW` DDL is emitted by the
112
- Node `meta migrate` from the projection's `origin.*` children — never hand-author the
113
- view SQL for a shape origins can express (an unmodeled view is unmanaged and drifts
114
- silently).
129
+ Node `meta migrate` from the projection's `origin.*` children — `origin.passthrough`,
130
+ `origin.aggregate` (`@agg` `count`/`sum`/`avg`/`min`/`max`, plus the #195 `any`/`all`
131
+ quantifiers over a `@filter` and `collect` array-rollup with `@distinct`/`@orderBy`),
132
+ `origin.collection`, `origin.computed` (`@expr`), `origin.first`; an object-level
133
+ `@filter` scopes the whole view's rows (#207 — lowers to the outer `WHERE`). Never
134
+ hand-author the view SQL for a shape origins can express (an unmodeled view is unmanaged
135
+ and drifts silently); carry a genuinely irreducible body (recursive CTE, window function,
136
+ set op) in the `source.rdb` **`@sql`** escape (#208) so the tool still owns it, or mark a
137
+ Flyway-owned object `@unmanaged: true`.
138
+
139
+ **Entity read-view (write-through).** An `object.entity` that keeps its writable `table`
140
+ primary source and adds a `@role: replica` `@kind: view` source is a write-through
141
+ read-view (#214): reads route through the replica view and writes to the table (derived
142
+ `origin.*` fields excluded); a create/update re-reads the row via the view by primary key
143
+ (read-your-writes). The replica view's DDL is emitted by `meta migrate` from the same
144
+ origin assembly as a projection view.
115
145
 
116
146
  Metadata lives under `src/main/metaobjects/` in the same canonical JSON the other
117
147
  ports read — fused-key form, `source.rdb` + `@table`, `@column` for a renamed
@@ -28,6 +28,16 @@ the exact `gen` code path so the two can't diverge). `--templates` is the prompt
28
28
  drift gate (see the prompts reference). Schema migration + live-DB drift are **not**
29
29
  `metaobjects` — they run through the Node `meta` tool (see the migration reference).
30
30
 
31
+ ## Docs — `metaobjects docs`
32
+
33
+ ```bash
34
+ metaobjects docs ./metadata --out ./docs # → ./docs/api/python (AGENT-API.md + per-entity pages)
35
+ ```
36
+
37
+ `metaobjects docs` emits this project's Python SDK api surface (`api/python`), including
38
+ `AGENT-API.md` — the exact imports, signatures, and payload field shapes for the
39
+ generated code. **Before calling any generated code, read `api/python/AGENT-API.md`.**
40
+
31
41
  ## Generators
32
42
 
33
43
  Wire generators by their stable name (`--generators <names>`), or run the default set.
@@ -40,9 +50,19 @@ a renamed physical column).
40
50
  | `entity` | one **Pydantic model** per `object.entity` / projection (the `entity-model` generator): typed fields from the metadata, nullability from `@required`, `@maxLength`/validators, enum fields → a Python `Enum`. This is the typed data model. A TPH concrete subtype (`@discriminatorValue`) pins the inherited `@discriminator` field to a `Literal[...]` so the model rejects a foreign-subtype tag. |
41
51
  | `routes` | a **FastAPI `APIRouter`** per writable entity (`source.rdb @kind="table"`) on the cross-port REST contract (`?filter[field][op]=`, `?sort=field:asc`, `?limit`/`?offset`, `?withCount=1` envelope, 400/404 envelopes). The router declares a repository **`Protocol`** you implement and inject. A TPH `@discriminator` base emits ONE polymorphic router: `GET /<base>(+/{id})` plus a per-subtype CRUD set at `/<base>/<discriminatorValue lowercased>` — create injects the discriminator from the URL (never the body); get/update/delete scoped to the subtype (cross-subtype → 404); discriminator immutable. Its repository `Protocol` is subtype-keyed (`subtype=None` for the polymorphic base) so your implementation applies the single-table discriminator scope. |
42
52
  | `filter-allowlist` | per-entity filter allowlist (FR-009 — the server-side field+operator allowlist the routes validate against). |
43
- | `payload` / `output-parser` / `output-prompt` / `extractor` / `render-helper` / `trace-helper` | the `template.output` prompt-pillar artifacts — see the **prompts** reference. |
53
+ | `payload` / `output-parser` / `output-prompt` / `extractor` / `render-helper` / `trace-helper` | the `template.output` prompt-pillar artifacts — the payload VO, the strict parser, the **output-format prompt fragment** (`output-prompt`; presentation via `@promptStyle: guide`/`inline`/`exampleOnly`), the tolerant `extract`, the typed render helper, and the LLM-trace helper. See the **prompts** reference. |
44
54
  | `template` | the generic Mustache `template` primitive. |
45
55
 
56
+ **Projections + entity read-views.** An `object.projection` (read-only `source.rdb`
57
+ `@kind: view` child) gets a read-only Pydantic model from the `entity` generator; its
58
+ `CREATE VIEW` DDL is emitted by the Node `meta migrate` from the projection's `origin.*`
59
+ children (`passthrough` / `aggregate` / `collection` / `computed` / `first`) — never
60
+ hand-write the view SQL for a shape origins can express. An `object.entity` that adds a
61
+ `@role: replica` `@kind: view` source alongside its writable `table` is a write-through
62
+ **entity read-view** (#214): the generated read model carries the derived `origin.*`
63
+ fields and writes exclude them — reads route to the view, writes to the table (your
64
+ repository implements the split).
65
+
46
66
  ## Discriminator inheritance (TPH)
47
67
 
48
68
  Python codegen fully supports **table-per-hierarchy (TPH) inheritance**
@@ -77,13 +97,17 @@ persistence layer and no runnable server**. Two things you hand-write:
77
97
 
78
98
  ## Known gaps (current — may require a hand-edit)
79
99
 
80
- - **Single-field, `int` PKs only.** The generated router/repository assume a single
81
- `int` primary key (`id: int`). Non-`int` single-field PKs and composite PKs need a
82
- hand-edit until specified.
83
- - **DTO = `dict[str, Any]`.** Request bodies for `POST`/`PATCH`/`PUT` are typed
84
- `dto: dict[str, Any]` and responses return `Any`; the repository `Protocol` uses
85
- `Any` for the row type. The typed Pydantic model from the `entity` generator exists
86
- you can tighten the router signatures to it by hand.
100
+ - **Composite PKs need a hand-edit.** The generated router/repository key on a single
101
+ primary key whose Python type is **derived from the PK field's subtype** (`field.uuid`
102
+ PK `uuid.UUID`, `field.long` → `int`, `field.string` → `str`) via the same mapper the
103
+ Pydantic model uses — so a `field.uuid` PK is `uuid.UUID`, not `int`. A **composite** PK
104
+ falls back to `@fields[0]` and needs a hand-edit until specified.
105
+ - **DTO param is `dict[str, Any]`.** The `POST`/`PATCH`/`PUT` body param is typed
106
+ `dto: dict[str, Any]` and responses return `Any`; the repository `Protocol` uses `Any`
107
+ for the row type. This does **not** mean constraints are unenforced — the router
108
+ validates the body against the generated `<Entity>Create` / `<Entity>Patch` Pydantic
109
+ models before the repository call (FR-036: field constraints run on POST/PATCH over
110
+ HTTP). You can further tighten the router signatures to the typed model by hand.
87
111
 
88
112
  ## Re-scaffold this context
89
113
 
@@ -79,14 +79,30 @@ From `@metaobjectsdev/codegen-ts/generators` (server-side, framework-neutral):
79
79
  | `barrel()` | `index.ts` re-exporting each `<Entity>.ts` (one-shot, not per-entity) |
80
80
  | `promptRender()` | `render<Name>()` per `template.prompt` |
81
81
  | `outputParser()` | `<Name>.output.ts` (`parse*` / `safeParse*`) per `template.output` |
82
+ | `callableFile()` | `<Entity>.callable.ts` — an FR-015 `call<Entity>` wrapper for a `source.rdb` `@kind: storedProc`/`tableFunction` (args from the `@parameterRef` value object, in declaration order) |
82
83
 
83
84
  **Projections (read-only views).** For an `object.projection` (a read-only `source.rdb`
84
85
  `@kind: view` child), `entityFile()` emits a `pgView(...)` + read-only Zod + a read-only
85
86
  finder (no create/update/delete). The `CREATE VIEW` DDL is generated by `meta migrate`
86
87
  from the projection's `origin.*` children — `origin.passthrough` (a forwarded column),
87
- `origin.aggregate` (a `count`/`sum`/`avg`/`min`/`max`, optionally row-scoped with
88
- `@filter`), `origin.collection` (a nested array). **Never hand-write the view SQL** for a
89
- shape origins can express; an unmodeled view is unmanaged and drifts silently.
88
+ `origin.aggregate` (`@agg` `count`/`sum`/`avg`/`min`/`max`, plus the #195 `any`/`all`
89
+ predicate quantifiers over a `@filter` and `collect` array-rollup with optional
90
+ `@distinct`/`@orderBy`; any aggregate row-scoped with `@filter`), `origin.collection` (a
91
+ nested array), `origin.computed` (a row-level `@expr`), `origin.first` (one related row's
92
+ column along `@via`/`@of`/`@orderBy`). An object-level `@filter` on the projection scopes
93
+ the whole view's rows (#207 — lowers to the outer `WHERE`, the metadata-managed
94
+ soft-delete/status view). **Never hand-write the view SQL** for a shape origins can
95
+ express (an unmodeled view is unmanaged and drifts silently); for a genuinely irreducible
96
+ body (recursive CTE, window function, set op), carry it in the `source.rdb` **`@sql`**
97
+ escape (#208) so the tool still owns it, or mark a Flyway-owned object `@unmanaged: true`.
98
+
99
+ **Entity read-view (write-through).** An `object.entity` that keeps its writable `table`
100
+ primary source and adds a `@role: replica` `@kind: view` source is a write-through
101
+ read-view (#214): `entityFile()` routes generated **reads** through the replica `pgView`
102
+ (the read Zod carries the derived `origin.*` fields via `z.infer`), while `queriesFile()`
103
+ writes target the table with derived fields excluded from the insert/update codecs; a
104
+ create/update re-reads the row via the view by primary key (read-your-writes). The replica
105
+ view's DDL is emitted by `meta migrate` from the same origin assembly as a projection view.
90
106
 
91
107
  ## Discriminator inheritance (TPH)
92
108
 
@@ -17,8 +17,8 @@ fetch it from GitHub / metaobjects.dev) and let it produce the report below.
17
17
 
18
18
  # MetaObjects Fit & Migration Assessment
19
19
 
20
- _Assessment prompt v1 (post-Phase-0 refinement). Grounded against MetaObjects npm `0.15.x` /
21
- Maven `7.7.x` — verify every capability claim against the current release before asserting it._
20
+ _Assessment prompt v1 (post-Phase-0 refinement). Grounded against MetaObjects npm `0.17.x` /
21
+ Maven `7.9.x` — verify every capability claim against the current release before asserting it._
22
22
 
23
23
  You are an AI assistant running a **pre-adoption fit assessment** for MetaObjects
24
24
  (https://github.com/metaobjectsdev/metaobjects — the cross-language metadata standard:
@@ -235,12 +235,27 @@ signature class. Hunt all ten classes:
235
235
  8. Hand-written `CREATE VIEW` / read-only SQL mirroring a read model. Run the
236
236
  **necessity test**: expressible when every output column is a passthrough
237
237
  (`origin.passthrough @from/@via`), a count/sum/avg/min/max (`origin.aggregate
238
- @agg/@of/@via`, row-scoped with `@filter`), a child collection (`origin.collection`),
239
- or `extends`-borrowed and joins follow declared relationships/`identity.reference`
240
- FKs. Expressible projection candidate (note: an unmodeled hand view is *unmanaged*
241
- invisible to `verify --db`; modeling it is what makes it gateable). Not
242
- expressible BESPOKE with a NAMED construct (recursive CTE, window fn, set op,
243
- `DISTINCT ON`, lateral join). "It's an aggregation" is not a justification.
238
+ @agg/@of/@via`, row-scoped with `@filter`), a predicate quantifier (`origin.aggregate
239
+ @agg: any|all`), an array rollup (`origin.aggregate @agg: collect`), a non-aggregate
240
+ derived scalar (`origin.computed @expr`), an argmax-style "one related row's column"
241
+ pick (`origin.first @via` covers the common `DISTINCT ON` / lateral-join shape), a
242
+ child collection (`origin.collection`), a soft-delete/status/type row-scope (an
243
+ object-level `@filter` on `object.projection`), or `extends`-borrowed and joins
244
+ follow declared relationships/`identity.reference` FKs. Expressible → projection
245
+ candidate (note: an unmodeled hand view is *unmanaged* — invisible to `verify --db`;
246
+ modeling it is what makes it gateable). `DISTINCT ON` and lateral join are **not**
247
+ automatic BESPOKE justifications — check `origin.first` first; they earn BESPOKE only
248
+ when the pick can't collapse to one argmax over one `@via` path (a multi-column
249
+ tiebreak, or a lateral doing more than pick-one-row). Not expressible → BESPOKE with a
250
+ NAMED construct (recursive CTE, window fn, set op, a `DISTINCT ON`/lateral join
251
+ `origin.first` genuinely can't express). "It's an aggregation" is not a justification.
252
+ BESPOKE has a better ending than "hand-write it outside the tool": if the body is
253
+ irreducible but still **yours** to own, carry it in the `source.rdb` **`@sql`** escape
254
+ (#208, ADR-0043) — a hand-written view/proc body the tool registers, fingerprints, and
255
+ drift-checks (adopt a pre-existing view with `meta migrate --allow adopt-view`, no
256
+ rewrite needed); if the object is owned **elsewhere** (Flyway, another team's
257
+ migration), mark its source `@unmanaged: true` so `verify --db` reports it as an
258
+ external, declared object instead of silently missing it.
244
259
  9. A closed variant-set hand-modeled per instance (N sibling modules on one payload
245
260
  shape) → VOCAB CANDIDATE (advisory only; ADR-0037 ordered test).
246
261
  10. One prompt's text/payload/parse scattered across services — a renamed field silently
@@ -273,8 +288,14 @@ concrete — lead with it.
273
288
  | the metadata itself | strict provenance (ADR-0023): unknown attrs fail load |
274
289
 
275
290
  State the honest limits in the same section: `verify` cannot catch semantic mismodeling
276
- (a uuid modeled as string passes `--db`), cannot see unmodeled DB objects, and
277
- `--templates` coverage depends on CLI version. A gate, not a proof system.
291
+ (a uuid modeled as string passes `--db`), cannot see a genuinely unmodeled DB object
292
+ (nothing ever declared it), and `--templates` coverage depends on CLI version. Name the
293
+ two declared middle states before writing something off as unmodeled: an
294
+ irreducible-but-owned view/proc registered via `source.rdb @sql` (#208) IS fingerprinted
295
+ and drift-checked, and an object owned by another team's tooling can be declared
296
+ `@unmanaged: true` so `verify --db` reports it as external rather than missing it
297
+ silently — "unmodeled" applies only to what was never declared at all. A gate, not a
298
+ proof system.
278
299
 
279
300
  ### P4 — Fit rubric (worked, not vibes)
280
301
 
@@ -293,7 +314,7 @@ this table worked row-by-row is invalid output):
293
314
  | DB not Postgres/SQLite/D1 | schema pillar (`migrate`, `verify --db`) OUT — say so plainly; data-access unaffected |
294
315
  | Non-entity-shaped domain (no persistent typed records to speak of) | NOT A FIT — structural, and visible in the code |
295
316
  | Few entities today (< ~5) | **NOT a flat verdict — this is the M8 trap.** Small-today ≠ small-forever, and git cannot tell you which. Use the trajectory answer (Input 3): *done at this size* → MARGINAL/NOT A FIT (say so plainly — the leverage won't repay the tooling); *expected to grow / add a language / add an LLM surface* → FIT, and note adopting later costs more (you'd retrofit a spine onto more drift). **Unanswered → emit both branches, never guess.** |
296
- | Schema owned by another team (DBA-gated) | migrate pillar restricted; model read-only ("metadata follows the schema"); flag the org constraint |
317
+ | Schema owned by another team (DBA-gated) | migrate pillar restricted; model read-only ("metadata follows the schema"); objects that stay owned by that team's own tooling can be declared `source.rdb @unmanaged: true` instead of silently excluded; flag the org constraint |
297
318
  | Deep hand-tuned ORM investment | churn warning, not a disqualifier: the plan must reproduce those mappings (`@column`/`@table`/`@dbColumnType`) and price it |
298
319
  | Team rejects generated code in the repo | flag; regen-every-build works but `verify --codegen` semantics differ — call the tradeoff |
299
320
 
@@ -355,9 +376,21 @@ promising it or counting it in benefits:
355
376
  from behavior).
356
377
  - UUIDs as bare strings → `field.uuid` (never `field.string` + `@dbColumnType: uuid`).
357
378
  - hand `COUNT/SUM` subqueries, read-model SQL → `object.projection` + `origin.passthrough`
358
- / `origin.aggregate` (+`@filter` for scoped aggregates) / `origin.collection`.
379
+ / `origin.aggregate` (`count`/`sum`/`avg`/`min`/`max`, +`@filter` for scoped
380
+ aggregates, +`any`/`all` predicate quantifiers, +`collect` array rollups) /
381
+ `origin.collection` / `origin.computed` (a non-aggregate derived scalar) /
382
+ `origin.first` (an argmax-style "one related row's column" pick — the usual
383
+ `DISTINCT ON`/lateral-join case) — plus a soft-delete/status/type view via the
384
+ object-level `@filter` on `object.projection` instead of a hand `WHERE`.
359
385
  **Calibration: most projection fields in real spines are passthrough/`extends`
360
386
  re-exposures; aggregates are the minority — lead with passthrough.**
387
+ - a hand-maintained denormalized/computed column kept in sync by app code (an "update
388
+ the other row on write" helper mirroring a joined value onto the entity's own row) →
389
+ an **entity read-view** on the same `object.entity` before reaching for a projection:
390
+ keep the writable `table` source, add a non-primary read-only `view` source, and
391
+ declare the extra as a derived `origin.*` field — writes still target the table,
392
+ reads route through the view and re-read by PK (#214; see `docs/features/source-kinds.md`
393
+ "entity read-view vs projection" for when to use which).
361
394
  - hand junction joins → `relationship @cardinality: many @through` (junction declares
362
395
  two `identity.reference` children; FR-018).
363
396
  - copy-pasted base-field blocks → abstract base + `extends`.
@@ -491,14 +524,18 @@ Every prose prediction gets a claim. Ceiling statements (P5-b) MUST carry
491
524
  access still works; say it plainly.
492
525
  - **TS**: full stack — Drizzle/Zod/Fastify codegen, filter-operator routes, TanStack/React
493
526
  UI runtime, migrations. The only port with UI codegen + runtime.
494
- - **Java/Spring**: generated DTO records, controllers, filter allowlists (pagination/sort;
495
- filter ops deferred), repository *interfaces* (consumer implements existing ORM sits
496
- behind unchanged), payload records, output parsers hand-write the Jackson one-liner;
497
- entities stay hand-written on this lane; Maven `meta:gen` / `meta:verify`
498
- (`codegen`/`templates` modes no live-DB mode).
527
+ - **Java/Spring**: generated DTO records, controllers (parse + validate the full FR-009
528
+ filter-op grammar via the generated `FilterAllowlist` + the `FilterParser` runtime
529
+ helper — per-field op allowlist, `in`-list cap, `eq` sugar; api-contract is 20/20 both
530
+ lanes), filter + sort allowlists (pagination/sort), repository *interfaces* (consumer
531
+ implementsexisting ORM sits behind unchanged; the consumer's repository impl
532
+ translates the returned predicates to its persistence DSL), payload records, output
533
+ parsers hand-write the Jackson one-liner; entities stay hand-written on this lane;
534
+ Maven `mvn metaobjects:generate` / `mvn metaobjects:verify` (`codegen`/`templates`
535
+ modes — no live-DB mode).
499
536
  - **Kotlin/JVM**: `codegen-kotlin` generates entity + Exposed table + Spring controller +
500
537
  payload + relations + filter allowlist + validators + output parsers (runs via Maven
501
- `meta:gen`).
538
+ `mvn metaobjects:generate`).
502
539
  - **C#**: generated EF Core entities + `AppDbContext` + CRUD minimal-API routes +
503
540
  render/payload/verify via `dotnet meta gen`/`verify`; no ObjectManager runtime tier;
504
541
  no migrate surface (TS-owned).
@@ -21,18 +21,35 @@ value-object declaring exactly what data the text expects.
21
21
 
22
22
  | Subtype | Use | Extra attrs |
23
23
  |---|---|---|
24
- | `template.prompt` | LLM-targeted | `@maxTokens`, `@requiredSlots`, `@model` |
25
- | `template.output` | email / docs / config / export | (generic only) |
24
+ | `template.prompt` | LLM-targeted | `@maxTokens`, `@requiredSlots`, `@requiredTags`, `@model`, `@responseRef` |
25
+ | `template.output` | email / docs / config / export | `@kind: document \| email` (default `document`), `@promptStyle`, `@requiredTags`; `@kind: email` adds `@subjectRef` / `@htmlBodyRef` / `@textBodyRef` |
26
26
 
27
27
  Both carry the generic attrs:
28
28
 
29
29
  | Attr | Required | Purpose |
30
30
  |---|---|---|
31
31
  | `@payloadRef` | yes | the `object.value` declaring the payload shape |
32
- | `@textRef` | yes | the 2-layer logical text reference `group/source`, resolved by a provider |
32
+ | `@textRef` | yes for `template.prompt` and a `template.output @kind: document` (the default) — a `template.output @kind: email` carries **no** `@textRef`; it uses `@subjectRef` + `@htmlBodyRef` (+ optional `@textBodyRef`) instead | the 2-layer logical text reference `group/source`, resolved by a provider |
33
33
  | `@format` | no | `text` (default) / `html` / `xml` / `csv` / `json` / `markdown` / `spreadsheet` — drives the escaper |
34
34
  | `@maxChars` | no | build-time size budget |
35
35
 
36
+ `template.output @kind: email` renders a structured `EmailDocument` (subject + HTML
37
+ body + optional plain-text body) instead of one string — the TS render helper emits
38
+ an `EmailDocument`-returning function for it (see the `render-example-email`
39
+ conformance fixture). `@promptStyle` (`guide` / `inline` / `exampleOnly`, FR-010)
40
+ selects how the output-format prompt fragment presents the payload shape to an LLM
41
+ (see "the output-format prompt fragment" below); `@requiredTags` names output tags
42
+ the rendered text must contain (`verify` checks it) on both subtypes.
43
+ `template.prompt` additionally carries `@responseRef` — naming the response
44
+ value-object the prompt expects, for typed LLM-call trace derivation.
45
+
46
+ A third, structurally different subtype is also registered core vocabulary:
47
+ **`template.toolcall`** (`@toolName` + `@payloadRef`, ADR-0011) — a vendor-agnostic
48
+ LLM tool-call envelope with no renderable text body (the body IS the
49
+ `@payloadRef`-typed output schema, so it does not carry the generic `@textRef`/
50
+ `@format` attrs above). The vocabulary exists today; MCP exposure of declared
51
+ prompts/tools is roadmap, not shipped — don't promise it.
52
+
36
53
  ## The payload is an `object.value` projection
37
54
 
38
55
  The payload is **not** an entity — it's an `object.value` whose every field carries
@@ -44,6 +61,13 @@ an `origin.*` child saying where its value comes from. Three origin subtypes:
44
61
  | `origin.aggregate @agg <count\|sum\|avg\|min\|max>` | `count`→long, `avg`→double, others match source |
45
62
  | `origin.collection @via "Parent.rel"` | a list of a nested payload, assembled from a relationship |
46
63
 
64
+ These are the **payload-assembly** origins — the vocabulary this skill covers.
65
+ **Projection** read models (`object.projection` over an entity) carry a fuller origin
66
+ vocabulary — the `@agg` predicate quantifiers `any`/`all`, the `collect` array rollup,
67
+ plus `origin.computed` (a closed `@expr` grammar) and `origin.first` (an argmax-style
68
+ pick) — those live in the `metaobjects-authoring` skill and
69
+ `docs/features/source-kinds.md`, not here: don't reach for them on a payload VO.
70
+
47
71
  Declaring the payload as a projection is what makes payload bloat visible: adding a
48
72
  field to the prompt is a diff on the `object.value`, and a renamed source field
49
73
  breaks the build instead of silently degrading the prompt.
@@ -184,6 +208,43 @@ file; `verify` catches payload-VO ↔ parser drift at build time too.
184
208
  The three-step consumer pattern is identical everywhere: render the prompt → call
185
209
  your LLM client → parse the response with the generated parser.
186
210
 
211
+ ## `template.output` also generates the output-format prompt fragment (FR-010)
212
+
213
+ For every **json/xml-format** `template.output` whose `@payloadRef` resolves to a
214
+ value-object, codegen additionally emits an `output-prompt` artifact: a
215
+ `render<Name>Format(...)`-shaped function backed by the render engine's
216
+ output-format renderer — the "produce your answer like this" instruction fragment
217
+ you splice into the prompt text so the model returns exactly the shape the parser
218
+ above expects. It's generated only for `json`/`xml` outputs (`text`/`html`/`csv`/
219
+ `markdown`/`spreadsheet` don't get a fragment) and skipped under the same
220
+ unresolved-`@payloadRef` rule as the parser; the fragment and the parser's
221
+ `extract()` codegen agree on the same root name.
222
+
223
+ `@promptStyle` on the `template.output` controls the fragment's presentation
224
+ (default `guide`):
225
+
226
+ | `@promptStyle` | Presentation |
227
+ |---|---|
228
+ | `guide` (default) | a prose field list ("Fill in each field…") followed by an example skeleton |
229
+ | `inline` | a single skeleton whose field values are inline placeholders / enum choices |
230
+ | `exampleOnly` | just a filled example skeleton, nothing else |
231
+
232
+ Guidance is **never** emitted as code comments — models routinely ignore comments,
233
+ so the instruction has to live in the rendered text itself.
234
+
235
+ The fragment is **baked directly from the payload's field tree at codegen time**
236
+ (not hand-authored Mustache text), so it cannot itself drift out of sync with the
237
+ payload the way a hand-written `@textRef` can — regenerating it after a payload
238
+ change is the gate. The JVM render module (Java, shared by Kotlin) additionally
239
+ exposes a field-presence check, `Verify.checkOutputPrompt(fragment,
240
+ requiredFieldNames)`, for asserting a *rendered instance* of the fragment actually
241
+ names every required field — useful in a test of the renderer output itself,
242
+ distinct from the `{{field}}`-vs-payload drift check `verify` runs on hand-authored
243
+ template text. Check the language reference for whether this project's port ships
244
+ the equivalent.
245
+
187
246
  ---
188
247
 
189
- For this project's server-language parser specifics, read every `references/*.md` file in this skill's directory (one per server language in this project's stack).
248
+ For this project's server-language parser + output-format-fragment specifics, read
249
+ every `references/*.md` file in this skill's directory (one per server language in
250
+ this project's stack).
@@ -9,6 +9,7 @@ the payload generator, so the parser and the payload VO can't silently drift.
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
  - Consumer dependency
@@ -52,6 +53,26 @@ generator also emits a tolerant `Extract(string[, ExtractOptions])` (self-contai
52
53
  components) returning an `ExtractionResult` with a nullable `<Payload>Extracted` mirror
53
54
  — a classified per-field report rather than a throw.
54
55
 
56
+ ## The output-format prompt fragment (FR-010)
57
+
58
+ For every json/xml-format `template.output`, `MetaObjects.Codegen`'s
59
+ `OutputPromptGenerator` (stable name `output-prompt-generator`) emits a
60
+ `<TemplateName>.prompt.cs` declaring a static `<TemplateName>Prompt` class with a
61
+ `RenderFormat()` / `RenderFormat(PromptOverrides)` pair, backed by the render
62
+ engine's `OutputFormatRenderer` — the "produce your answer like this" fragment for
63
+ the model. It runs as part of the same `dotnet meta gen` invocation as the payload
64
+ and parser generators:
65
+
66
+ ```bash
67
+ dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog
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 an unresolved
73
+ `@payloadRef` — the same skip contract as the parser generator. The baked spec's
74
+ root name is the payload class name, agreeing with the parser's root.
75
+
55
76
  ## The three-step consumer pattern
56
77
 
57
78
  Render the prompt → call your LLM client (provider-agnostic; nothing is generated