Skip to content

Commit 40de49d

Browse files
dmealingclaude
andcommitted
docs(codegen): SP-1 — resolve scope-naming + CLI-spec open points
Fold the two researched decisions into the SP-1 design: - Scope names perEntity/perPackage/perModel (one vocabulary for code helpers + declarative scope value); oncePerRun → soft-deprecated alias. Grounded in Telosys / EF Core T4 / Prisma / OpenAPI Generator; "run" rejected as ambiguous under multi-target output. - C#/Python declarative surface: a JSON spec file (--template-spec + auto-discovered default + JSON Schema), identical on both ports. Grounded in Smithy / OpenAPI `files` / Buf / .config/dotnet-ef.json; YAML/TOML/inline-flags rejected for stdlib-parity + the protoc cautionary tale. §7 reframed as Decisions + remaining risks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky
1 parent 26ce4d9 commit 40de49d

1 file changed

Lines changed: 66 additions & 20 deletions

File tree

docs/superpowers/specs/2026-06-28-mustache-codegen-parity-design.md

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ code** for the common cases:
5050
1. Drop a Mustache template in the project templates dir, e.g.
5151
`templates/service/entity-service.mustache`.
5252
2. Declare a template generator in the port's own build/config surface:
53-
`template` (ref), `scope` (`perEntity` | `perPackage` | `wholeModel`),
53+
`template` (ref), `scope` (`perEntity` | `perPackage` | `perModel`),
5454
`outputPattern` (e.g. `"{package}/{name}Service.java"`), `format?` (escaper).
5555
3. Run the port's normal gen verb.
5656

@@ -70,7 +70,23 @@ below. Wiring, file I/O, and registration stay idiomatic per port.
7070

7171
### 3.1 Scope names (exact strings, all ports)
7272

73-
`perEntity` · `perPackage` · `wholeModel`
73+
`perEntity` · `perPackage` · `perModel`
74+
75+
These are both the declarative `scope` values (in the per-port config below) and the
76+
TS engine helper names (`perEntity(fn)` / `perPackage(fn)` / `perModel(fn)`). One
77+
vocabulary across the code API and the declarative surface. The grammar is parallel —
78+
each name is exactly the slice the generator sees once per call (one entity, one
79+
package, the whole model) — which is how Telosys ("for each entity"), EF Core T4
80+
("for each entity type"), and Prisma/EF (the whole "model") describe these scopes.
81+
82+
The existing TS helper `oncePerRun` is renamed to `perModel`; `oncePerRun` stays as a
83+
**soft-deprecated alias** (drop in a future major — the same deprecation pattern as the
84+
`@metaobjectsdev/codegen-ts/generators` export). "Run" is avoided deliberately: the
85+
runner has per-target `RenderContext`s and multi-target output, so "per run" reads
86+
ambiguously as "per target"; `perModel` ties the scope to the metadata model (the
87+
durable spine), not an execution event. (`appLevel` may be offered later as a
88+
documented alias for the deployment-framing reader, but is not canonical — "app" is a
89+
deployment concept, not a model-breadth one.)
7490

7591
### 3.2 The template data dict per scope (the portable shape templates reference)
7692

@@ -88,7 +104,7 @@ codegen template data model and the docs data model do not drift.
88104
}
89105
```
90106
- **`perPackage`** → one file per package: `{ package, entities: [ <perEntity dict> … ] }`
91-
- **`wholeModel`** → one file total: `{ packages: [ { package, entities: [ <perEntity dict> … ] } ] }`
107+
- **`perModel`** → one file total: `{ packages: [ { package, entities: [ <perEntity dict> … ] } ] }`
92108

93109
`type` is the **neutral metamodel field subtype** (`string`, `int`, `long`, `currency`,
94110
`enum`, …) — NOT a language type. Templates that need a language type map it themselves
@@ -103,7 +119,7 @@ fast-follow once v1 is gated (§6).
103119

104120
Placeholders, expanded per walk unit: `{name}` (object name), `{Name}` (PascalCase),
105121
`{package}` (package rendered as a path, `::``/`). `perPackage` patterns may use
106-
`{package}`; `wholeModel` patterns are literal (no per-unit placeholder). Unknown
122+
`{package}`; `perModel` patterns are literal (no per-unit placeholder). Unknown
107123
placeholder → hard error at gen time (no silent passthrough).
108124

109125
### 3.4 Template resolution
@@ -127,19 +143,40 @@ introduced — consistent with "each port runs codegen through its own build too
127143
- **TS** (`@metaobjectsdev/codegen-ts`): `templateGenerator({ name, template, scope,
128144
outputPattern, format? })`. `scope` selects a built-in walk; the existing
129145
`walk` option stays for power users (mutually exclusive with `scope`). Reference
130-
implementation + the `perPackage` engine helper land here.
146+
implementation + the `perPackage` engine helper + the `oncePerRun``perModel` rename
147+
(alias retained) land here.
131148
- **Java + Kotlin** (Maven plugin): a `<templateGenerator>` config element
132149
(`<template>`, `<scope>`, `<outputPattern>`, `<format>`) the plugin turns into a real
133150
`Generator` wrapping the cross-port `render/TemplateGenerator` + the named walk. **This
134151
is where Kotlin gains a template generator** — it is JVM and reuses the shared engine;
135152
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.
153+
- **C#** (`dotnet meta`) and **Python** (`metaobjects gen`): the same declarative
154+
**JSON spec file** surface — `--template-spec <path>`, with a conventional default the
155+
port auto-discovers and the flag overriding (mirrors `.config/dotnet-ef.json` and
156+
`datamodel-code-generator`'s auto-discovery-with-flag-override). These are CLI-only
157+
ports, so the spec is a file, not a closure. The file is a JSON object:
158+
```json
159+
{ "generators": [
160+
{ "name": "service", "template": "service/entity-service",
161+
"scope": "perEntity", "outputPattern": "{package}/{Name}Service.cs", "format": "text" }
162+
] }
163+
```
164+
Turns C#'s no-op registry primitive (and Python's programmatic-only path) into a real
165+
consumer-usable generator. JSON, not YAML/TOML, is the deliberate choice: it is the
166+
**only** format both runtimes parse with their standard library (System.Text.Json /
167+
`json`), so one identical file works byte-for-byte on both ports — best parity, zero
168+
added deps, and consistent with the canonical-JSON interchange (ADR-0006). It matches
169+
the validated shape for "a list of generator specs" (Smithy `smithy-build.json`,
170+
OpenAPI's `files` node, Buf's `plugins` list). Inline flags are rejected (protoc's
171+
cautionary tale); a `pyproject.toml`/`.csproj` section is rejected (TOML-vs-XML kills
172+
cross-port parity).
173+
174+
A **JSON Schema** for the spec ships with the feature; both `gen` and `verify` validate
175+
authored specs against it (guardrails for agent authors + a drift check, consistent with
176+
the `verify` philosophy). TS keeps its executable config; the JSON spec is the
177+
CLI-port equivalent, expressing the same neutral `{ name, template, scope, outputPattern,
178+
format? }` records. If human authoring ergonomics later matter, YAML may be added as a
179+
*desugar-to-this-JSON* front-end (ADR-0006 pattern), never as the interchange.
143180

144181
The walk + data dict + pattern expansion is shared-by-contract (gated); the registration
145182
surface is per-port.
@@ -149,8 +186,9 @@ surface is per-port.
149186
Mirrors how cross-port features land in this repo — TS reference first, then fan out,
150187
flipping the corpus on per port as it lands.
151188

152-
1. **SP-1a — TS reference.** Named scope walks (`perEntity`/`perPackage`/`wholeModel`)
153-
+ the `perPackage` engine helper + `outputPattern` + data-dict builder, wired into
189+
1. **SP-1a — TS reference.** Named scope walks (`perEntity`/`perPackage`/`perModel`)
190+
+ the `perPackage` engine helper + the `oncePerRun``perModel` rename (alias kept)
191+
+ `outputPattern` + data-dict builder + the JSON-spec schema, wired into
154192
`templateGenerator`. Author `fixtures/template-codegen-conformance/` and gate TS
155193
against it. (Lands the concepts-guide §10 `perPackage` gap.)
156194
2. **SP-1b — JVM (Java + Kotlin).** Wire `<templateGenerator>` into the Maven plugin
@@ -178,15 +216,23 @@ merge — admin-merge after local green, per the repo flow).
178216
display labels) — a fast-follow once the v1 dict is gated.
179217
- **Groovy** — dropped; Mustache covers the need.
180218

181-
## 7. Risks / open points
219+
## 7. Decisions + remaining risks
220+
221+
**Decided** (researched against prior art, 2026-06-28):
222+
223+
- **Scope names: `perEntity` / `perPackage` / `perModel`** (one vocabulary for the code
224+
helpers and the declarative `scope` value); `oncePerRun` → soft-deprecated alias of
225+
`perModel`. Grounded in Telosys / EF Core T4 / Prisma / OpenAPI Generator scope
226+
vocabulary; "run" rejected as ambiguous under multi-target output. (§3.1)
227+
- **CLI-port declarative surface: a JSON spec file** (`--template-spec <path>` + auto-
228+
discovered default + JSON Schema), identical on C# and Python. Grounded in Smithy /
229+
OpenAPI Generator `files` / Buf / `.config/dotnet-ef.json`; YAML/TOML/inline-flags
230+
rejected (stdlib-parity, protoc cautionary tale, cross-port divergence). (§4)
231+
232+
**Remaining risks:**
182233

183234
- **Data-dict scope creep.** v1 is intentionally thin. The gate makes additions cheap to
184235
verify but every field added is a cross-port obligation — add only on demonstrated need.
185236
- **Reusing the docs data builder.** If the docs builder's shape is awkward for codegen,
186237
the codegen dict may need its own builder that shares helpers rather than the exact
187238
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)