Skip to content

Commit 15c85f8

Browse files
dmealingclaude
andauthored
feat(codegen-ts): declarative Mustache template-codegen scopes (#88)
* 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 * 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 * docs(codegen): SP-1a implementation plan — TS declarative template-codegen 8-task TDD plan for the TS reference increment: perPackage helper + perModel rename; neutral structural data-dict builder; output-pattern expander; scope walks wired into templateGenerator; JSON template-spec + schema (the CLI-port contract); the template-codegen-conformance corpus + TS gate. Grounded in the real metadata accessors; self-reviewed against the spec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky * feat(codegen-ts): add perPackage scope helper + perModel (alias oncePerRun) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky * feat(codegen-ts): output-pattern expander for template codegen Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky * feat(codegen-ts): neutral structural template data-dict builder Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky * feat(codegen-ts): built-in scope walks (perEntity/perPackage/perModel) in templateGenerator Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky * feat(codegen-ts): JSON template-spec shape + parser + schema (CLI-port contract) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky * feat(codegen-ts): export template-codegen public API Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky * fix(codegen-ts): resolve effective package in template data dict + scope walks entity.package is undefined for objects (object fqn() stays bare); read the effective package via effectivePackage(resolutionKey()) so {package} and the per-package grouping reflect the declared file package. Strengthen the data-dict test to assert the real package (the prior assertion was tautological). * test(codegen-ts): template-codegen conformance corpus + TS gate Cross-port corpus: spec.json (perEntity/perPackage/perModel) over metadata/ resolving templates/ → byte-identical expected/. The TS gate runs it in a hermetic tmp projectRoot (copies templates/) so runGen's .gen-state merge base never lands in the committed corpus; a corpus .gitignore guards .metaobjects/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky * no-mistakes(review): fix perPackage effective-package grouping and validate template-spec format * no-mistakes(document): sync docs for declarative template-codegen scopes (SP-1a) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 47d3e47 commit 15c85f8

31 files changed

Lines changed: 2149 additions & 13 deletions

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
88
## [Unreleased]
99

1010
### Added
11+
- **codegen-ts — declarative Mustache template-codegen (SP-1a):** a
12+
`templateGenerator` can now take its walk **declaratively** via `scope`
13+
(`"perEntity" | "perPackage" | "perModel"`) + `outputPattern` instead of a
14+
hand-written `walk` (the two are mutually exclusive — provide exactly one). The
15+
generator derives a **neutral, structural** template data dict per unit
16+
(`buildEntityTemplateData` / `buildPackageTemplateData` / `buildModelTemplateData`,
17+
with types `FieldTemplateData` / `EntityTemplateData` / `IdentityTemplateData` /
18+
`RelationshipTemplateData` / `PackageTemplateData` / `ModelTemplateData`) — raw
19+
structural facts only, distinct from the Markdown-flavored `EntityDocData`, and
20+
byte-gated as a cross-port contract by `fixtures/template-codegen-conformance/`.
21+
`outputPattern` supports `{name}` / `{Name}` / `{package}` (`::``/`; unknown
22+
placeholder throws), expandable via the exported `expandOutputPattern`. A JSON
23+
**template-spec** (`parseTemplateSpec` / `templateSpecToGenerators`, types
24+
`TemplateSpecEntry` / `TemplateSpecFile`, JSON Schema beside the source) is the
25+
surface the C#/Python CLI ports will reuse. New package-scope engine helper
26+
`perPackage(fn)` joins `perEntity` / `perModel`. All exported from the package
27+
main entry `@metaobjectsdev/codegen-ts`.
1128
- **cli — `meta init` scaffolds owned codegen generators (ADR-0034 scaffold-and-own, step 2):**
1229
`meta init` now copies the four codegen reference templates (step 1) into the
1330
consumer repo at `codegen/generators/{entity,queries,routes,barrel}.ts` and
@@ -34,6 +51,10 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
3451
generator-export deprecation, and the guidance rewrite are later steps.)
3552

3653
### Deprecated
54+
- **codegen-ts — `oncePerRun` scope helper (SP-1a):** renamed to `perModel`
55+
"run" is ambiguous under multi-target output (it reads as "per target"), while
56+
`perModel` names the data scope (the whole model). `oncePerRun` is kept as a
57+
soft-deprecated alias and still works.
3758
- **codegen-ts — `@metaobjectsdev/codegen-ts/generators` factory re-exports
3859
(ADR-0034 scaffold-and-own, step 2):** importing `entityFile` / `queriesFile` /
3960
`routesFile` / `barrel` from the package `/generators` export is deprecated in

docs/features/codegen-concepts.md

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,34 @@ whole-model are just different walks.
150150
**The model to expose.** Three scope helpers, so the scope is declared, not buried in
151151
custom iteration:
152152

153-
- **`perEntity(fn)`** — object scope. *(exists)*
154-
- **`perPackage(fn)`** — package scope: group matching objects by package, run `fn`
155-
once per package. *(the gap — Java never had this cleanly either, so this is an
156-
improvement on the predecessor, not just parity.)*
157-
- **`oncePerRun(fn)`** — app scope: run once over all matching objects. *(exists; an
158-
`appLevel` alias can make the intent explicit.)*
153+
- **`perEntity(fn)`** — object scope. Runs `fn` once per matched object.
154+
- **`perPackage(fn)`** — package scope: group matching objects by package (packages
155+
ascending, objects keeping `ctx.entities` order), run `fn` once per package. Groups
156+
by `effectivePackage` so objects with a bare FQN (FR5d) still bucket correctly.
157+
- **`perModel(fn)`** — app scope: run once over all matching objects. (`oncePerRun`
158+
is kept as a soft-deprecated alias — "run" is ambiguous under multi-target output;
159+
`perModel` names the data scope.)
159160

160161
A generator picks its scope by which helper it composes — the same way it picks
161-
object vs run today. Reference templates and the guidance call out the scope each
162+
object vs model today. Reference templates and the guidance call out the scope each
162163
starting point is for, so "I need a per-package service layer" maps to a clear
163164
pattern instead of hand-rolled grouping.
165+
166+
### Declarative template scopes
167+
168+
A `templateGenerator` can take the scope walk declaratively instead of a hand-written
169+
`walk` — pass `scope` (`"perEntity" | "perPackage" | "perModel"`) plus an
170+
`outputPattern`, and the generator derives the neutral template data dict per unit and
171+
names each file via the pattern. `walk` and `scope` are mutually exclusive — provide
172+
exactly one. The `outputPattern` placeholders are `{name}`, `{Name}`, and `{package}`
173+
(an unknown placeholder throws); `{package}` resolves through `effectivePackage` and
174+
renders `::`-separated segments as nested path directories.
175+
176+
```ts
177+
templateGenerator({
178+
name: "entity-doc",
179+
scope: "perEntity",
180+
outputPattern: "{package}/{Name}.md",
181+
template: "entity-doc",
182+
});
183+
```

docs/features/codegen-data-shapes.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,44 @@ trivial and cross-port walkers don't have to re-derive the rules.
6868
file into your project; the project's `templates/` directory takes precedence
6969
over the framework defaults. Same for any partial under `templates/docs/`.
7070

71+
### Neutral structural shapes → `EntityTemplateData` / `PackageTemplateData` / `ModelTemplateData`
72+
73+
The declarative template scopes (`scope: "perEntity" | "perPackage" | "perModel"`
74+
on `templateGenerator`) feed a **neutral, structural** data dict — deliberately
75+
distinct from the Markdown-flavored `EntityDocData` above. It carries raw
76+
structural facts only, so a consumer's template can emit any language's code from
77+
it. The field names are a byte-gated **cross-port contract** (the
78+
`fixtures/template-codegen-conformance/` corpus locks them) — change them only via
79+
the spec.
80+
81+
```ts
82+
interface FieldTemplateData {
83+
name: string;
84+
type: string; // neutral subtype: "string" | "int" | "currency" | "enum" | ...
85+
required: boolean;
86+
isArray: boolean; // arrayness is carried here, NOT appended to `type`
87+
maxLength?: number;
88+
enumValues?: string[]; // present only for field.enum
89+
}
90+
interface IdentityTemplateData { kind: string; fields: string[]; }
91+
interface RelationshipTemplateData { name: string; cardinality: string; targetRef: string; }
92+
interface EntityTemplateData {
93+
name: string;
94+
package: string; // effective package (`::`-separated), "" when none
95+
fields: FieldTemplateData[];
96+
identities: IdentityTemplateData[];
97+
relationships: RelationshipTemplateData[];
98+
}
99+
interface PackageTemplateData { package: string; entities: EntityTemplateData[]; }
100+
interface ModelTemplateData { packages: PackageTemplateData[]; } // packages ascending
101+
```
102+
103+
These types and their builders (`buildEntityTemplateData`,
104+
`buildPackageTemplateData`, `buildModelTemplateData`) are exported from the package
105+
**main entry** (`@metaobjectsdev/codegen-ts`), alongside `expandOutputPattern` and
106+
the JSON template-spec parser (`parseTemplateSpec`, `templateSpecToGenerators`).
107+
Abstract objects are excluded from every scope (they emit no instance artifact).
108+
71109
### Section-presence flags (`hasX`)
72110

73111
Mustache cleanly iterates an array (`{{#identities}}...{{/identities}}`) but

0 commit comments

Comments
 (0)