@@ -50,7 +50,7 @@ code** for the common cases:
50501 . Drop a Mustache template in the project templates dir, e.g.
5151 ` templates/service/entity-service.mustache ` .
52522 . 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).
55553 . 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
104120Placeholders, 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
107123placeholder → 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
144181The walk + data dict + pattern expansion is shared-by-contract (gated); the registration
145182surface is per-port.
@@ -149,8 +186,9 @@ surface is per-port.
149186Mirrors how cross-port features land in this repo — TS reference first, then fan out,
150187flipping 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.)
1561942 . ** 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