Skip to content

Commit fbcae70

Browse files
authored
fix(csharp): scope generators with Include and apply project naming on directory load (#456)
* fix(csharp,docs): scope a generator with Include, apply naming in FromDirectory, refresh agent context first (#455) * no-mistakes(review): apply reviewer inline fixes * no-mistakes(document): Docs for 1.1.2 agent-context and C# scoping updated; Python seam corrected
1 parent e6c4167 commit fbcae70

30 files changed

Lines changed: 406 additions & 45 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,47 @@ here.**
1010

1111
## [Unreleased]
1212

13+
## [1.1.2] — unreleased
14+
15+
_Fixes #455. A bug-fix release: no vocabulary or `metamodelVersion` change._
16+
17+
### Fixed
18+
19+
- **C#: `MetaDataLoader.FromDirectory` (every overload, the registry overload included) now applies
20+
the project's `naming`**, as the JVM `fromDirectory` does. A host that composed its own registry
21+
and loaded with `FromDirectory(dir, registry, strict: true)` got `PhysicalNaming.Default`
22+
(snake_case) whatever `.metaobjects/config.json` said, and every `[Column]` and `[Table]`
23+
changed without a word. The config is found the way the CLI finds it (the nearest ancestor
24+
`.metaobjects/config.json`, no further than the repository root); a malformed one throws. A
25+
hosted runner that builds its own `GenConfig` still sets `ColumnNamingStrategy` from the loaded
26+
root: `ColumnNamingStrategy = load.Root.PhysicalNaming.Columns`.
27+
28+
### Added
29+
30+
- **C#: `Include` on the packaged generators.** `EntityGenerator`, `DbContextGenerator`,
31+
`RoutesGenerator`, `FilterAllowlistGenerator`, `NamesGenerator` and `CallableGenerator` take an
32+
optional `Func<MetaObject, bool>? Include` init property that scopes the entry to some objects
33+
(`new EntityGenerator { Include = o => … }`), and `GenContext.Narrow(predicate)` hands any
34+
other generator a narrowed context. C# had no equivalent of the TypeScript `filter`, so an
35+
adopter whose value objects belong to another port (ADR-0056 emits a POCO for every concrete
36+
`object.value`) dropped emitted files by name afterwards. The property is `Include`, not
37+
`Filter`, because `PerEntityGenerator.Filter(MetaObject)` is the generators' own applicability
38+
method and stays the subclass seam. Nothing changes for an entry that sets none.
39+
- **`meta upgrade` prints the agent-context refresh as its first line.**
40+
41+
### Documentation
42+
43+
- **Every upgrade guide starts with the agent-context refresh**, and says why: the agent doing the
44+
upgrade reads the `metaobjects-*` skills, and refreshing them last (as the 1.0-to-1.1 guide did,
45+
at step 9) meant the whole upgrade ran on the old version's skill.
46+
- **`upgrading-1.0-to-1.1.md` gains "Changes to generated text (for subclassed or post-processed
47+
generators)"**: the C# emit-shape changes since 1.0.5, each with the `protected virtual` seam that
48+
replaces rewriting the text.
49+
- **The codegen skill says how to narrow a generator in each language's own idiom** (TypeScript
50+
`filter`, C# `Include`, Python `filter()`, the JVM's args or eject), and `references/csharp.md`
51+
gains "Scoping a generator to some objects", which names dropping files by name as the
52+
anti-pattern. Its claim that every port reads `naming` now says what a hosted runner must do.
53+
1354
## [1.1.1] — 2026-10-24
1455

1556
_npm `1.1.1` (full lockstep across all 14 `@metaobjectsdev/*` publish candidates), PyPI `1.1.1`, NuGet `1.1.1` and Maven Central `8.1.1`._

‎agent-context/skills/metaobjects-codegen/SKILL.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ import { isAbstract, perEntity, type Generator } from "@metaobjectsdev/codegen-t
9090
export function fieldListGenerator(): Generator {
9191
return {
9292
name: "field-list", // shows in diagnostics
93-
filter: (obj) => !isAbstract(obj), // ctx.entities has EVERY object, abstract ones too
93+
filter: (obj) => !isAbstract(obj), // TypeScript's own seam; ctx.entities has EVERY object, abstract ones too
9494
generate: perEntity((obj) => ({ // perPackage / perModel for other scopes
9595
path: `field-list/${obj.name}.txt`, // relative to outDir
9696
content: obj.fields() // fields() includes inherited fields
@@ -124,7 +124,9 @@ without typechecking, so a wrong accessor runs silently.
124124

125125
- **Which objects.** The runner hands you every object — entities, value objects,
126126
projections and abstract bases. Filter: abstract bases only contribute fields to what
127-
`extends` them.
127+
`extends` them. To scope a generator that ships with the tool to some objects, use the
128+
port's own idiom (a TypeScript `filter`, a C# `Include`, a Python `filter=` argument; the
129+
table in `references/owning-generators.md`), never emit everything and delete files by name.
128130
- **Resolving accessors, always (ADR-0039).** `fields()`, `attr()`, `isRequired`,
129131
`resolvedIsArray()` see what a field or object inherits through `extends`; the `own*()`
130132
forms and the raw `isArray` flag do not, and the output is silently wrong on any model

‎agent-context/skills/metaobjects-codegen/references/csharp.md‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ is **schema-migrations only** on the C# side (ADR-0015): `meta migrate` /
1212
- Docs — one more entry in `codegen/Generators.cs`
1313
- Persistence + routes are the deployed artifact
1414
- Write your own generator
15+
- Scoping a generator to some objects
1516
- Extending the generators (open-for-extension, ADR-0002)
1617
- Re-scaffold this context
1718

@@ -53,6 +54,8 @@ public static class Generators
5354

5455
**Column names come from `naming` in `.metaobjects/config.json`, the one setting every port
5556
reads** (ADR-0058): `meta migrate`, every port's codegen and every runtime `ObjectManager`.
57+
A hosted runner that builds its own `GenConfig` sets `ColumnNamingStrategy` from the loaded
58+
root (see "Scoping a generator to some objects", last paragraph).
5659
The default is `snake_case`, what `meta migrate` creates. A database whose columns are named
5760
exactly like the fields declares it there, once, for every port:
5861

@@ -236,6 +239,53 @@ entry runs, and `--generators` only narrows. Model reads: `Fields()`, `Attr(n)`,
236239
`using static MetaObjects.Core.Field.FieldConstants;`. A generator that throws is reported
237240
as the generator, with its message.
238241

242+
## Scoping a generator to some objects
243+
244+
C# has no TypeScript-style `filter`: `IGenerator` is `Name` + `Generate(GenContext)` + `OutDir`.
245+
Since ADR-0056 `EntityGenerator` emits a POCO for every concrete `object.value` in
246+
`ctx.Entities`, so a project whose value objects belong to another port (catalog or config
247+
VOs) has to say which objects this generator is for. In order of preference:
248+
249+
1. **`Include` on the packaged generator.** `EntityGenerator`, `DbContextGenerator`,
250+
`RoutesGenerator`, `FilterAllowlistGenerator`, `NamesGenerator` and `CallableGenerator` take
251+
an optional `Func<MetaObject, bool>? Include` init property (it is `Include`, not `Filter`:
252+
`Filter(MetaObject)` is the per-entity generators' own applicability method, which stays
253+
the subclass seam). `null`, the default, accepts every object.
254+
```csharp
255+
new MetaObjects.Codegen.Generators.EntityGenerator { Include = o => NamingRefs.EffectivePackageOf(o) == "acme::orders" },
256+
new MetaObjects.Codegen.Generators.DbContextGenerator { Include = o => NamingRefs.EffectivePackageOf(o) == "acme::orders" },
257+
```
258+
Give the same predicate to every entry that should agree: an `Include` on `entity` alone
259+
leaves `db-context` mapping classes that were never emitted.
260+
2. **Hand a generator a narrowed context.** For any `IGenerator` without `Include` (your own,
261+
or a subclass), wrap it: `inner.Generate(ctx.Narrow(o => ...))`. `Narrow` returns the same
262+
context with `Entities` reduced and everything else (`Root`, `Config`, `Warn`,
263+
`Applicability`) kept. Build the `GenContext` by hand only if you must, and copy all of
264+
those members.
265+
3. **Eject and edit** (`dotnet meta eject <name>`): the owned copy's object selection is yours.
266+
267+
What narrowing reaches. Narrowing reduces `ctx.Entities`, and that is the set `entity`,
268+
`db-context`, `routes`, `filter-allowlist`, `names` and `callable` iterate. A generator that
269+
walks `ctx.Root` instead (the prompt tier: `output-parser`, `extractor`, `output-prompt`,
270+
`render-helper`) is not narrowed, and neither is the resolution of a reference:
271+
`EntityGenerator` finds the value objects an emitted entity points at through `ctx.Root`, so an
272+
anchored value object is still emitted when `Include` rejects it by name. To drop a value
273+
object nothing anchors, reject it in `Include`; to drop one an entity references, change the
274+
model.
275+
276+
**The anti-pattern: letting a generator emit everything and dropping files by name
277+
afterwards** (a `Where(f => !f.Path.StartsWith(...))` over `Generate`'s result). The emitted
278+
set and the committed set then disagree, `verify --codegen` regenerates the unfiltered set and
279+
reports drift, and a renamed file silently stops matching. Narrow the input, or own the
280+
generator.
281+
282+
**A hosted runner** (your own `CodegenRunner.Run` call, not `dotnet meta gen`) must apply the
283+
project's `naming` itself. `MetaDataLoader.FromDirectory(dir, registry, ...)` reads
284+
`.metaobjects/config.json` above the directory and sets `root.PhysicalNaming` from it (since
285+
1.1.2; through 1.1.1 a host got snake_case whatever the project said). `GenConfig` is built by
286+
hand, so set its strategy from the root: `ColumnNamingStrategy = load.Root.PhysicalNaming.Columns`.
287+
`MetadataLocation.Resolve(...).Load()` and `dotnet meta gen` do both for you.
288+
239289
## Extending the generators (open-for-extension, ADR-0002)
240290

241291
The generators are subclassable: the per-class emit methods are `protected virtual`,

‎agent-context/skills/metaobjects-codegen/references/owning-generators.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,19 @@ mechanical: if changing a generator — or a file eject copied beside it — fix
4141
yours.
4242

4343
**The converse, so ownership does not become sprawl:** wire a generator only for output
44-
you will actually consume. Decide per generator, narrow one with its own `filter`, and
44+
you will actually consume. Decide per generator, narrow one to the objects you consume, and
4545
own the ones you keep — an emitted file nobody imports still reads as an invitation to
4646
adopt the surface you decided against.
4747

48+
**How you narrow one is each language's own idiom, not one shared flag:**
49+
50+
| Port | Narrow a generator to some objects |
51+
|---|---|
52+
| **TypeScript** | the generator's `filter: (obj) => boolean` |
53+
| **C#** | `new EntityGenerator { Include = o => ... }` on the entry in `codegen/Generators.cs` (also on `db-context`, `routes`, `filter-allowlist`, `names`, `callable`); for any other generator, wrap it and hand it a narrowed `GenContext` (`ctx.Narrow(...)`), or eject and edit. `references/csharp.md`, "Scoping a generator to some objects". Never emit everything and delete files by name afterwards |
54+
| **Python** | the `filter=` argument on the generator factory (a callable of the object), e.g. `trace_helper_generator(filter=...)` |
55+
| **Java / Kotlin** | the entry's `<args>` in the pom where the generator offers one; otherwise `mvn metaobjects:eject` the generator and edit its object selection |
56+
4857
Every port gets a reference generator's source the same way — its `eject` command — and
4958
writes a new one against the same small interface (the top of the skill). Your language
5059
reference has the per-port mechanics.

‎docs/features/migrations/0.x-to-1.0.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ already shipped incrementally across `0.14`–`0.15`; the 1.0 cut is the moment
66
on all of them. The stability promise that begins at 1.0 is the
77
[compatibility policy](../../compatibility-policy.md).
88

9+
**Refresh the agent context right after step 0, before the rest.** A coding agent doing this
10+
migration reads the `metaobjects-*` skills in `.claude/skills/`, which describe the version
11+
that scaffolded them. Run `meta init --docs-only --refresh-docs` once the new version is
12+
installed, so the remaining steps are done on the skills for the version you are moving to.
13+
914
## 0. Update the dependency version
1015

1116
Package versions **re-baseline** at the cut (both forward — see

0 commit comments

Comments
 (0)