|
| 1 | +# SP-1 — Declarative Mustache template-generator parity (cross-port) |
| 2 | + |
| 3 | +_Status: Proposed · 2026-06-28_ |
| 4 | + |
| 5 | +Part of the codegen authoring-parity program (the follow-on to ADR-0034 |
| 6 | +scaffold-and-own). Program order, decided with the user: **Mustache first (this |
| 7 | +spec), then native-generator authoring parity (SP-2), then the agent-context docs |
| 8 | +pass (SP-3).** Groovy is explicitly dropped — Mustache covers the "scriptable, |
| 9 | +no-compile, cross-language" authoring need. |
| 10 | + |
| 11 | +See also: [ADR-0034 (scaffold-and-own)](../../../spec/decisions/ADR-0034-codegen-scaffold-and-own.md), |
| 12 | +[ADR-0020 (codegen tiering — idiomatic-per-port vs neutral-shared)](../../../spec/decisions/ADR-0020-codegen-tiering-native-vs-neutral.md), |
| 13 | +[the cross-port template-generator design (2026-05-28)](../../../spec/design-docs/2026-05-28-cross-port-template-generator.md), |
| 14 | +[docs/features/codegen-concepts.md §3 (authoring menu) + §10 (scopes)](../../features/codegen-concepts.md). |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## 1. Problem |
| 19 | + |
| 20 | +Every port already ships the two codegen authoring **primitives**: |
| 21 | + |
| 22 | +- a **native generator** interface (`Generator` / `IGenerator` / `GeneratorBase`), |
| 23 | +- a **Mustache `TemplateGenerator`** factory whose render engine is byte-equivalent |
| 24 | + across all five ports (`fixtures/render-conformance/`). |
| 25 | + |
| 26 | +What is **not** at parity is how a consumer wires their **own** Mustache-template |
| 27 | +generator. Only TS has a config-is-code entrypoint where a consumer lists generator |
| 28 | +instances and supplies a `walk` closure. The other ports drive codegen declaratively |
| 29 | +(Maven XML, `--generators <names>` against a **sealed** registry) and cannot express a |
| 30 | +code-defined walk: |
| 31 | + |
| 32 | +| Port | Mustache template generator: consumer-usable today? | |
| 33 | +|---|---| |
| 34 | +| **TS** | ✅ `templateGenerator({ name, template, walk })` in `metaobjects.config.ts`; project `templates/` overrides framework defaults. | |
| 35 | +| **Python** | ⚠️ `template_generator(...)` exists but **programmatic-only** (`run_gen(generators=[…])`); the CLI `--generators` resolves a hard-coded registry. | |
| 36 | +| **Java** | ❌ cross-port `render/TemplateGenerator.generate(...)` exists + is conformance-gated, but is **not wired into the Maven plugin** — a consumer must hand-write a `Generator` wrapper class. Legacy `MustacheTemplateGenerator` is `@Deprecated`. | |
| 37 | +| **Kotlin** | ❌ **no template generator at all** (KotlinPoet-only; Mustache deliberately omitted). | |
| 38 | +| **C#** | ⚠️ `TemplateGenerator.Create(...)` factory exists, but the registry's CLI entry is a **no-op primitive** (empty template, in-memory empty provider) usable only for `--list`. Real use is programmatic. | |
| 39 | + |
| 40 | +**Root cause:** the only thing stopping declarative use is that the **walk is code**. |
| 41 | +If the common walks are *built-in and named*, a consumer can declare |
| 42 | +`{ template, scope, outputPattern }` with **no code**, which every port's |
| 43 | +declarative surface (Maven XML / CLI flag / config) can express. |
| 44 | + |
| 45 | +## 2. Goal |
| 46 | + |
| 47 | +A consumer authors a working code generator, on **any** port, with **no generator |
| 48 | +code** for the common cases: |
| 49 | + |
| 50 | +1. Drop a Mustache template in the project templates dir, e.g. |
| 51 | + `templates/service/entity-service.mustache`. |
| 52 | +2. Declare a template generator in the port's own build/config surface: |
| 53 | + `template` (ref), `scope` (`perEntity` | `perPackage` | `wholeModel`), |
| 54 | + `outputPattern` (e.g. `"{package}/{name}Service.java"`), `format?` (escaper). |
| 55 | +3. Run the port's normal gen verb. |
| 56 | + |
| 57 | +A built-in **named scope walk** supplies a standard data dict to the template; the |
| 58 | +output-pattern names each emitted file. This simultaneously lands the **`perPackage`** |
| 59 | +scope helper that `codegen-concepts.md` §10 calls for (object + app scopes already |
| 60 | +exist; package is the gap). |
| 61 | + |
| 62 | +Walk-as-code (exotic walks beyond the three scopes) stays available where a port |
| 63 | +already has it (TS), but is **not** the parity target and is **not** added to ports |
| 64 | +that lack it. Native hand-written-generator registration is **SP-2**, not here. |
| 65 | + |
| 66 | +## 3. The neutral contract (shared, byte-gated) — everything else is per-port |
| 67 | + |
| 68 | +Per ADR-0020, the only cross-port-shared, conformance-gated artifacts are the four |
| 69 | +below. Wiring, file I/O, and registration stay idiomatic per port. |
| 70 | + |
| 71 | +### 3.1 Scope names (exact strings, all ports) |
| 72 | + |
| 73 | +`perEntity` · `perPackage` · `wholeModel` |
| 74 | + |
| 75 | +### 3.2 The template data dict per scope (the portable shape templates reference) |
| 76 | + |
| 77 | +v1 — deliberately minimal but useful. Built from the loaded metadata; reuses the |
| 78 | +existing docs data builder (`buildEntityDocData` and its peers) where possible so the |
| 79 | +codegen template data model and the docs data model do not drift. |
| 80 | + |
| 81 | +- **`perEntity`** → one file per concrete `object.entity` / projection: |
| 82 | + ``` |
| 83 | + { |
| 84 | + name, package, |
| 85 | + fields: [ { name, type, required, isArray, maxLength?, enumValues? } ], |
| 86 | + identities: [ { kind, fields: [name…] } ], |
| 87 | + relationships: [ { name, cardinality, targetRef } ] |
| 88 | + } |
| 89 | + ``` |
| 90 | +- **`perPackage`** → one file per package: `{ package, entities: [ <perEntity dict> … ] }` |
| 91 | +- **`wholeModel`** → one file total: `{ packages: [ { package, entities: [ <perEntity dict> … ] } ] }` |
| 92 | + |
| 93 | +`type` is the **neutral metamodel field subtype** (`string`, `int`, `long`, `currency`, |
| 94 | +`enum`, …) — NOT a language type. Templates that need a language type map it themselves |
| 95 | +(that mapping is per-port and out of the neutral contract). Abstract entities and |
| 96 | +non-instance shapes follow the same emit-eligibility rules the native generators use |
| 97 | +(an abstract never emits an instance artifact). |
| 98 | + |
| 99 | +The v1 dict deliberately omits views/origins/currency-locale/storage facets; those are a |
| 100 | +fast-follow once v1 is gated (§6). |
| 101 | + |
| 102 | +### 3.3 Output-pattern grammar (fixed, tiny) |
| 103 | + |
| 104 | +Placeholders, expanded per walk unit: `{name}` (object name), `{Name}` (PascalCase), |
| 105 | +`{package}` (package rendered as a path, `::` → `/`). `perPackage` patterns may use |
| 106 | +`{package}`; `wholeModel` patterns are literal (no per-unit placeholder). Unknown |
| 107 | +placeholder → hard error at gen time (no silent passthrough). |
| 108 | + |
| 109 | +### 3.4 Template resolution |
| 110 | + |
| 111 | +Project `templates/<ref>.mustache` overrides framework defaults via each port's existing |
| 112 | +Provider chain — already byte-equal across ports, unchanged here. |
| 113 | + |
| 114 | +### 3.5 The conformance corpus |
| 115 | + |
| 116 | +New `fixtures/template-codegen-conformance/`: metadata + a small set of templates + a |
| 117 | +manifest of `{ template, scope, outputPattern, format? }` specs + the expected emitted |
| 118 | +files. **Every port runs it and must produce byte-identical output.** Because the render |
| 119 | +engine is already conformance-equal, the only genuinely new gated surface is the |
| 120 | +data dict (3.2) + scope walks (3.1) + output-pattern (3.3). |
| 121 | + |
| 122 | +## 4. Per-port wiring (idiomatic; NOT a new shared config format) |
| 123 | + |
| 124 | +Each port expresses the same spec in its own idiom. No new cross-port config file is |
| 125 | +introduced — consistent with "each port runs codegen through its own build tool." |
| 126 | + |
| 127 | +- **TS** (`@metaobjectsdev/codegen-ts`): `templateGenerator({ name, template, scope, |
| 128 | + outputPattern, format? })`. `scope` selects a built-in walk; the existing |
| 129 | + `walk` option stays for power users (mutually exclusive with `scope`). Reference |
| 130 | + implementation + the `perPackage` engine helper land here. |
| 131 | +- **Java + Kotlin** (Maven plugin): a `<templateGenerator>` config element |
| 132 | + (`<template>`, `<scope>`, `<outputPattern>`, `<format>`) the plugin turns into a real |
| 133 | + `Generator` wrapping the cross-port `render/TemplateGenerator` + the named walk. **This |
| 134 | + is where Kotlin gains a template generator** — it is JVM and reuses the shared engine; |
| 135 | + no KotlinPoet involvement. Multiple `<templateGenerator>` elements allowed. |
| 136 | +- **C#** (`dotnet meta`): a `--template-spec <file>` surface (JSON: an array of |
| 137 | + `{ name, template, scope, outputPattern, format? }`) — a CLI-only port, so the spec is |
| 138 | + a file, not a closure. Turns the no-op registry primitive into a real consumer-usable |
| 139 | + generator. (A `metaobjects.config`-style file is explicitly out of scope; ADR-0015 keeps |
| 140 | + C# CLI-flag-driven.) |
| 141 | +- **Python** (`metaobjects gen`): the same `--template-spec <file>` surface as C#, |
| 142 | + replacing programmatic-only use. |
| 143 | + |
| 144 | +The walk + data dict + pattern expansion is shared-by-contract (gated); the registration |
| 145 | +surface is per-port. |
| 146 | + |
| 147 | +## 5. Increment plan (each step conformance-gated) |
| 148 | + |
| 149 | +Mirrors how cross-port features land in this repo — TS reference first, then fan out, |
| 150 | +flipping the corpus on per port as it lands. |
| 151 | + |
| 152 | +1. **SP-1a — TS reference.** Named scope walks (`perEntity`/`perPackage`/`wholeModel`) |
| 153 | + + the `perPackage` engine helper + `outputPattern` + data-dict builder, wired into |
| 154 | + `templateGenerator`. Author `fixtures/template-codegen-conformance/` and gate TS |
| 155 | + against it. (Lands the concepts-guide §10 `perPackage` gap.) |
| 156 | +2. **SP-1b — JVM (Java + Kotlin).** Wire `<templateGenerator>` into the Maven plugin |
| 157 | + over the existing `render/TemplateGenerator`; implement the three named walks + |
| 158 | + data dict + pattern on the JVM; gate both Java and Kotlin against the corpus. Kotlin |
| 159 | + gains the template generator here. |
| 160 | +3. **SP-1c — Python.** `--template-spec` surface + the walks/data-dict/pattern; gate. |
| 161 | +4. **SP-1d — C#.** `--template-spec` surface + the walks/data-dict/pattern; gate; |
| 162 | + replace the no-op registry primitive. |
| 163 | + |
| 164 | +Each increment is its own PR through the no-mistakes gate (tests run locally in an |
| 165 | +isolated worktree; CI is the known-flaky `java-reactor` so it is not relied upon for |
| 166 | +merge — admin-merge after local green, per the repo flow). |
| 167 | + |
| 168 | +## 6. Out of scope (SP-1) |
| 169 | + |
| 170 | +- **Native hand-written generator registration/selection parity** (registry mutation / |
| 171 | + SPI / stable-name selection of a consumer's own `Generator`, documented extension |
| 172 | + seams) — that is **SP-2**. |
| 173 | +- **The agent-context docs rewrite** (teach the decision framework + own-your-generators |
| 174 | + on every port) — **SP-3**, written over the enhanced reality this program creates. |
| 175 | +- **Consumer code walks beyond the three named scopes** — TS keeps its `walk` escape |
| 176 | + hatch; other ports do not gain one. |
| 177 | +- **Richer data-dict fields** (views, origins, currency locale, storage facets, enum |
| 178 | + display labels) — a fast-follow once the v1 dict is gated. |
| 179 | +- **Groovy** — dropped; Mustache covers the need. |
| 180 | + |
| 181 | +## 7. Risks / open points |
| 182 | + |
| 183 | +- **Data-dict scope creep.** v1 is intentionally thin. The gate makes additions cheap to |
| 184 | + verify but every field added is a cross-port obligation — add only on demonstrated need. |
| 185 | +- **Reusing the docs data builder.** If the docs builder's shape is awkward for codegen, |
| 186 | + the codegen dict may need its own builder that shares helpers rather than the exact |
| 187 | + struct. Decide during SP-1a against the real fixture; do not force-fit. |
| 188 | +- **`perPackage` naming.** The helper name (`perPackage`) and an optional `appLevel` |
| 189 | + alias for `oncePerRun` are cosmetic; confirm during SP-1a so all ports adopt the same |
| 190 | + scope strings. |
| 191 | +- **C#/Python `--template-spec` ergonomics.** A JSON spec file is the lowest-risk |
| 192 | + declarative surface for the flag-driven ports; revisit only if it proves clumsy. |
0 commit comments