Skip to content

Commit d4ab919

Browse files
committed
Merge remote-tracking branch 'origin/main' into fm/mo-generator-authoring
# Conflicts: # docs/llms/llms.txt
2 parents 9fd381d + cffa1ae commit d4ab919

258 files changed

Lines changed: 5964 additions & 247 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken.
7171
- **Kotlin** — `codegen-kotlin` (KotlinPoet on JVM): entity + Exposed table + Spring controller + payload + relations + filter allowlist + validator + stored-proc + output-parser generators. `integration-tests-kotlin` runs the persistence-conformance corpus through Exposed against Testcontainers Postgres.
7272

7373
**Cross-port conformance corpora** (every port runs the shared corpus):
74-
- Metamodel: `fixtures/conformance/` (413 fixtures; 32 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
74+
- Metamodel: `fixtures/conformance/` (413 fixtures; 34 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
7575
- Render: `fixtures/render-conformance/`. TS / C# / Java / Kotlin / Python byte-identical.
7676
- Persistence: `fixtures/persistence-conformance/`. **Query** scenarios run on every port (TS / C# / Java / Kotlin / Python), each provisioning its test DB by executing the committed, TS-produced `canonical/schema.postgres.sql` (Postgres only — Derby dropped for the cross-port query corpus, ADR-0015; the FR-044 `report-*` scenarios also run on SQLite, MySQL and D1 where a port has the engine, see [docs/CONFORMANCE.md](docs/CONFORMANCE.md)). The **migration** scenarios are exercised by **TS only** (TS owns schema migrations). **The corpus now gates WRITES, not just reads (SP-H):** an `op: roundtrip` scenario type INSERTs through each port's runtime/ORM write codec (NOT raw SQL), reads the row back, and asserts the wire-normalized value. The `AllTypes` entity (`roundtrip-all-types.yaml`) carries one field of **every** persistable `field.*` subtype — string/int/long/double/float/decimal/boolean/date/time/timestamp(+tz)/currency/enum/uuid/object — plus an **array-of-VO** `field.object @isArray @storage:jsonb` column (`labels`, written as 2-element / empty-`[]` / single-element arrays across the three rows) — so every subtype write+read (incl. the array-of-value-object jsonb codec) round-trips through every port against Testcontainers PG. (`field.byte`/`field.short`/`field.class` were cut as non-functional registration-only stubs — the matrix tracks only genuinely-supported subtypes; see `fixtures/registry-conformance/README.md` → "Per-subtype write-round-trip matrix".)
7777
- API-contract: `fixtures/api-contract-conformance/`. TS / C# / Java / Kotlin / Python all green — each port runs **two lanes**: a hand-rolled reference server AND its **generated** API artifact booted over HTTP (the deployed controller/routes; TS+C# full-stack vs Testcontainers PG, Java/Kotlin/Python generated controller + in-memory repo behind the consumer seam). The generated fan-out found 10 real deployment bugs golden snapshots missed. Three sub-corpora run the **generated lane only, on all five ports** — `write-through/`, `projection/` (F22: a view-only `object.projection` serves GET list + GET by id and answers every write verb with `405 {"error": "method_not_allowed"}`) and `report/` (FR-044: a view-backed `object.report` serves GET list, answers `POST` with that same `405`, and mounts no `/{id}` route). That is deliberate, not a gap: what is under test is whether a port's GENERATOR emits those routes, and a hand-rolled reference server would answer every scenario by construction. The `m2m/` sub-corpus also gates **TPH x M:N together** (base-declared, subtype-declared, abstract-mid-declared, a non-subtype source onto a subtype TARGET, and the cross-subtype source id answering `200 []`) — the two corpora were originally built disjoint (`tph/` had no relationships, `m2m/` no discriminators), which is precisely how that defect class survived.

‎CHANGELOG.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,26 @@ here.**
2525

2626
### Added
2727

28+
- **Jinja templates in Python and TypeScript, held to the same payload check (ADR-0061, closes #483).**
29+
A `template.prompt` or `template.output` body may be a Jinja file —
30+
`<group>/<source>.jinja`, `.j2` or `.jinja2` — instead of a `.mustache` one. Nothing in the
31+
metadata changes: `@textRef` is the same `group/source`, and the engine comes from the file
32+
(or from an explicit `engine` option on `render`, `verify` and `InMemoryProvider`).
33+
Python renders with jinja2, an optional extra (`pip install "metaobjects[jinja]"`);
34+
TypeScript renders with nunjucks, now a dependency of `@metaobjectsdev/render`. `verify`
35+
(every CLI's template gate) and the render-helper generator check every payload reference a
36+
Jinja body makes, walking the parsed template: paths, loop targets, filter arguments and
37+
literal includes, imports and `extends`. A drift is `ERR_VAR_NOT_ON_PAYLOAD` as before. A
38+
reference the check cannot settle statically is `ERR_TEMPLATE_NOT_STATIC`, and a body that
39+
does not parse is `ERR_TEMPLATE_SYNTAX`. A generated helper over a Jinja body passes the
40+
engine and the payload field tree to `render`, so the check runs again at runtime. Two new
41+
corpora, `fixtures/jinja-render-conformance/` (15) and `fixtures/jinja-verify-conformance/`
42+
(31), hold the two ports to identical output and findings. Java, Kotlin and C# render
43+
Mustache only: their providers and `verify` now refuse a Jinja-only body with
44+
`ERR_TEMPLATE_ENGINE_UNSUPPORTED` instead of reporting the ref as unresolved. Mustache stays
45+
the default, generated Mustache helpers are byte-identical, and `metamodelVersion` does not
46+
move.
47+
2848
- **Prisma as the ORM, declared once in the model (TypeScript, #479).** Set `orm: "prisma"` in
2949
`metaobjects.config.ts` and wire the new `prisma-schema` generator (`prismaSchemaFile()`), and
3050
`meta gen` writes `schema.prisma` from the model for the `postgresql`, `mysql` or `sqlite`

‎agent-context/skills/metaobjects-prompts/SKILL.md‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ drift at build time instead of letting a prompt silently degrade.
166166
`@textRef` is a 2-layer logical reference `group/source` (folder/file,
167167
table/key, collection/document). The prompt text itself **never lives in
168168
metadata** — at runtime a configured **provider** resolves the reference to the
169-
actual Mustache text:
169+
actual template text (Mustache in every port; Python and TypeScript also accept Jinja, see
170+
below):
170171

171172
- a filesystem provider (L1 = folder, L2 = file) — the dev default:
172173
`lobby/welcome` → `<root>/lobby/welcome.mustache`. It is `FilesystemProvider` in every
@@ -181,6 +182,18 @@ actual Mustache text:
181182
Locale, A/B, dynamic, and evolutionary prompt variants all live behind the
182183
provider seam without touching metadata.
183184

185+
**Jinja bodies (Python and TypeScript only, ADR-0061).** A body may be a Jinja file —
186+
`lobby/welcome.jinja` (or `.j2` / `.jinja2`) — instead of `welcome.mustache`; the
187+
`@textRef` does not change, and the metadata never names an engine. Python renders it with
188+
jinja2 (the `metaobjects[jinja]` extra), TypeScript with nunjucks. The same `verify` gate
189+
checks it against the payload, walking the parsed template, and reports what it cannot
190+
settle statically (`user[key]`, a computed include name) as `ERR_TEMPLATE_NOT_STATIC`. Java,
191+
Kotlin and C# refuse a Jinja body with `ERR_TEMPLATE_ENGINE_UNSUPPORTED`, so a model that
192+
must render in those ports keeps Mustache. Prefer Mustache for new templates: it is the
193+
cross-port engine, and its logic-less shape is what pushes conditionals into the payload
194+
(next sections). Jinja is for adopting existing Jinja templates without rewriting them; per
195+
port reference files say how.
196+
184197
## `render()` is deterministic + byte-stable
185198

186199
Rendering is a pure function: `(payload VO, resolved text) → string`. Same inputs,
@@ -244,7 +257,8 @@ against each value of the dimension and snapshot every variant.
244257
## `verify` fails the build on prompt-drift
245258

246259
For every template, the verify step resolves the text, parses each `{{...}}`
247-
reference, and checks it exists on the payload VO. If the text references
260+
reference (Mustache, or a Jinja body in Python and TypeScript), and checks it exists on the
261+
payload VO. If the text references
248262
`{{authorName}}` but the payload only has `displayName`, **the build fails.** This
249263
is the prompt-vs-payload drift gate — run it in CI. It walks both `template.prompt`
250264
and `template.output` nodes the same way (both RENDER; only the direction of what comes

‎agent-context/skills/metaobjects-prompts/references/csharp.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,9 @@ template's tokens against the payload field tree — each `{{...}}` that doesn't
142142
yields a `VerifyError(Code, Path)` (empty list = no drift). Assert it is empty in an
143143
xUnit test to fail the build on prompt/payload drift; `dotnet meta verify` additionally
144144
catches a stale committed parser.
145+
146+
**Mustache only (ADR-0061).** Jinja bodies are a Python and TypeScript engine. A ref whose
147+
only body is a `.jinja` / `.j2` / `.jinja2` file is refused here with
148+
`ERR_TEMPLATE_ENGINE_UNSUPPORTED` (by the filesystem provider, and as a drift line by
149+
`verify`), rather than reported as missing. Keep templates the C# port renders in
150+
Mustache.

‎agent-context/skills/metaobjects-prompts/references/java.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,9 @@ JUnit test in the Maven `test` phase to fail the build on prompt/payload drift.
148148
`metaobjects:verify` codegen-drift goal additionally catches a stale committed
149149
parser. The emitted parser imports Jackson (`com.fasterxml.jackson`), normally
150150
already on a Spring Boot classpath.
151+
152+
**Mustache only (ADR-0061).** Jinja bodies are a Python and TypeScript engine. A ref whose
153+
only body is a `.jinja` / `.j2` / `.jinja2` file is refused here with
154+
`ERR_TEMPLATE_ENGINE_UNSUPPORTED` (by the filesystem provider, and as a drift line by
155+
`verify`), rather than reported as missing. Keep templates the JVM ports render in
156+
Mustache.

‎agent-context/skills/metaobjects-prompts/references/kotlin.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,9 @@ resolve against the payload — empty list = no drift. Assert it is empty in a J
166166
test in the Maven `test` phase to fail the build on prompt/payload drift. The
167167
`metaobjects:verify` codegen-drift goal additionally catches a stale committed
168168
parser.
169+
170+
**Mustache only (ADR-0061).** Jinja bodies are a Python and TypeScript engine. A ref whose
171+
only body is a `.jinja` / `.j2` / `.jinja2` file is refused here with
172+
`ERR_TEMPLATE_ENGINE_UNSUPPORTED` (by the filesystem provider, and as a drift line by
173+
`verify`), rather than reported as missing. Keep templates the JVM ports render in
174+
Mustache.

‎agent-context/skills/metaobjects-prompts/references/python.md‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ silently drift.
1818
- Recommended LLM caller (bring-your-own)
1919
- Consumer dependency
2020
- Drift gate
21+
- Jinja templates
2122

2223
## Wire the generators
2324

@@ -188,4 +189,27 @@ required_slots=None, required_tags=None) -> list[VerifyError]` walks a Mustache
188189
template's tokens against the payload field tree — each `{{...}}` reference that
189190
doesn't resolve yields a `VerifyError` (empty list = no drift). Assert it is empty in a
190191
pytest test to fail the build on prompt/payload drift; `metaobjects verify`
191-
additionally catches a stale committed parser.
192+
additionally catches a stale committed parser. Pass `engine="jinja"` to check a Jinja body.
193+
194+
## Jinja templates
195+
196+
Existing Jinja prompt templates adopt the gate as they are (ADR-0061). Install the extra,
197+
`pip install "metaobjects[jinja]"`, and keep the file as `<group>/<source>.jinja` (or `.j2`,
198+
`.jinja2`) under the template root; `@textRef` stays `group/source`. `FilesystemProvider`
199+
reads `.mustache` first, then the Jinja extensions, and `engine_of(ref)` reports which one it
200+
found; `render` and `metaobjects verify` pick the engine from it. For an
201+
`InMemoryProvider`, pass `InMemoryProvider(templates, engine="jinja")`, or pass `engine="jinja"`
202+
to `RenderRequest` / `verify` directly.
203+
204+
- `verify` walks the parsed jinja2 template: paths per segment, `for` targets bound to element
205+
fields, `set` / macro parameters as locals, filter arguments, and literal `include` /
206+
`import` / `extends` through the provider. `ERR_TEMPLATE_NOT_STATIC` marks what it cannot
207+
settle; `ERR_TEMPLATE_SYNTAX` a body that does not parse.
208+
- The generated render helper over a Jinja body passes `engine="jinja"` and the payload field
209+
tree (`verify=[PayloadField(...)]`) to `render`, so a body that drifted after the build is
210+
refused at runtime with `RenderError("render verify failed: ERR_… (path)")`.
211+
- A Pydantic payload renders as its JSON-mode dump, as with Mustache. Printed values follow
212+
the Mustache renderer: `true` / `false`, a whole float as an integer, `None` as empty.
213+
- To render the same in TypeScript (nunjucks), stay inside the shared vocabulary in
214+
`fixtures/jinja-render-conformance/`: test a list with `| length`, avoid `{% with %}`,
215+
Python methods and printing whole lists or dicts.

‎agent-context/skills/metaobjects-prompts/references/typescript.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,18 @@ error in every port. The emitted parser imports `zod`;
219219
it's usually already a dependency (Drizzle / `runtime-ts` lean on it), else
220220
`npm i zod`.
221221

222+
**Jinja bodies (ADR-0061).** A body may be `<group>/<source>.jinja` (or `.j2`, `.jinja2`)
223+
instead of `.mustache`; `@textRef` does not change. `@metaobjectsdev/render` renders it with
224+
nunjucks (a dependency of the package, nothing to install) and `meta verify` checks it the same
225+
way, walking the parsed template; it names the `.jinja` file and line on a drift.
226+
`FilesystemProvider.engineOf(ref)` reports the engine of the file it found; for an
227+
`InMemoryProvider` pass `new InMemoryProvider(map, { engine: "jinja" })`, or pass
228+
`engine: "jinja"` to `render` / `verify`. A generated `render<Name>()` helper over a Jinja body
229+
passes `engine: "jinja"`, so it renders as Jinja through any runtime provider, and the
230+
runtime check runs on it as on a Mustache body. What the check cannot settle statically is
231+
`ERR_TEMPLATE_NOT_STATIC`. nunjucks treats an empty list as true (jinja2 does not), so test a
232+
list with `{% if items | length %}` when the same template must render in Python.
233+
222234
## See which fields a template consumes
223235

224236
List `docsFile()` in `generators` and run `meta gen` to emit the model surface to the

‎bun.lock‎

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)