Skip to content

Commit 26ce4d9

Browse files
dmealingclaude
andcommitted
docs(codegen): SP-1 design — declarative Mustache template-generator parity
First sub-project of the codegen authoring-parity program (follow-on to ADR-0034). Every port already ships the Mustache TemplateGenerator primitive (render engine is byte-equivalent), but only TS lets a consumer wire their own template generator declaratively — the others need a code walk the declarative surfaces (Maven XML, --generators) can't express. SP-1 makes the common walks built-in + named (perEntity/perPackage/wholeModel), so a consumer declares {template, scope, outputPattern} with no code on any port. Lands the perPackage scope helper the concepts guide §10 calls for; Kotlin gains a template generator for the first time. Neutral, byte-gated contract: scope names + per-scope data dict + output-pattern grammar + template resolution (new fixtures/template-codegen-conformance/). Wiring stays idiomatic per port (no shared config format). Increments: TS reference → JVM → Python → C#. Native-generator registration parity is SP-2; the agent-context docs rewrite is SP-3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky
1 parent 47d3e47 commit 26ce4d9

1 file changed

Lines changed: 192 additions & 0 deletions

File tree

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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

Comments
 (0)