Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,47 @@ here.**

## [Unreleased]

## [1.1.2] — unreleased

_Fixes #455. A bug-fix release: no vocabulary or `metamodelVersion` change._

### Fixed

- **C#: `MetaDataLoader.FromDirectory` (every overload, the registry overload included) now applies
the project's `naming`**, as the JVM `fromDirectory` does. A host that composed its own registry
and loaded with `FromDirectory(dir, registry, strict: true)` got `PhysicalNaming.Default`
(snake_case) whatever `.metaobjects/config.json` said, and every `[Column]` and `[Table]`
changed without a word. The config is found the way the CLI finds it (the nearest ancestor
`.metaobjects/config.json`, no further than the repository root); a malformed one throws. A
hosted runner that builds its own `GenConfig` still sets `ColumnNamingStrategy` from the loaded
root: `ColumnNamingStrategy = load.Root.PhysicalNaming.Columns`.

### Added

- **C#: `Include` on the packaged generators.** `EntityGenerator`, `DbContextGenerator`,
`RoutesGenerator`, `FilterAllowlistGenerator`, `NamesGenerator` and `CallableGenerator` take an
optional `Func<MetaObject, bool>? Include` init property that scopes the entry to some objects
(`new EntityGenerator { Include = o => … }`), and `GenContext.Narrow(predicate)` hands any
other generator a narrowed context. C# had no equivalent of the TypeScript `filter`, so an
adopter whose value objects belong to another port (ADR-0056 emits a POCO for every concrete
`object.value`) dropped emitted files by name afterwards. The property is `Include`, not
`Filter`, because `PerEntityGenerator.Filter(MetaObject)` is the generators' own applicability
method and stays the subclass seam. Nothing changes for an entry that sets none.
- **`meta upgrade` prints the agent-context refresh as its first line.**

### Documentation

- **Every upgrade guide starts with the agent-context refresh**, and says why: the agent doing the
upgrade reads the `metaobjects-*` skills, and refreshing them last (as the 1.0-to-1.1 guide did,
at step 9) meant the whole upgrade ran on the old version's skill.
- **`upgrading-1.0-to-1.1.md` gains "Changes to generated text (for subclassed or post-processed
generators)"**: the C# emit-shape changes since 1.0.5, each with the `protected virtual` seam that
replaces rewriting the text.
- **The codegen skill says how to narrow a generator in each language's own idiom** (TypeScript
`filter`, C# `Include`, Python `filter()`, the JVM's args or eject), and `references/csharp.md`
gains "Scoping a generator to some objects", which names dropping files by name as the
anti-pattern. Its claim that every port reads `naming` now says what a hosted runner must do.

## [1.1.1] — 2026-10-24

_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`._
Expand Down
6 changes: 4 additions & 2 deletions agent-context/skills/metaobjects-codegen/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ import { isAbstract, perEntity, type Generator } from "@metaobjectsdev/codegen-t
export function fieldListGenerator(): Generator {
return {
name: "field-list", // shows in diagnostics
filter: (obj) => !isAbstract(obj), // ctx.entities has EVERY object, abstract ones too
filter: (obj) => !isAbstract(obj), // TypeScript's own seam; ctx.entities has EVERY object, abstract ones too
generate: perEntity((obj) => ({ // perPackage / perModel for other scopes
path: `field-list/${obj.name}.txt`, // relative to outDir
content: obj.fields() // fields() includes inherited fields
Expand Down Expand Up @@ -124,7 +124,9 @@ without typechecking, so a wrong accessor runs silently.

- **Which objects.** The runner hands you every object — entities, value objects,
projections and abstract bases. Filter: abstract bases only contribute fields to what
`extends` them.
`extends` them. To scope a generator that ships with the tool to some objects, use the
port's own idiom (a TypeScript `filter`, a C# `Include`, a Python `filter=` argument; the
table in `references/owning-generators.md`), never emit everything and delete files by name.
- **Resolving accessors, always (ADR-0039).** `fields()`, `attr()`, `isRequired`,
`resolvedIsArray()` see what a field or object inherits through `extends`; the `own*()`
forms and the raw `isArray` flag do not, and the output is silently wrong on any model
Expand Down
50 changes: 50 additions & 0 deletions agent-context/skills/metaobjects-codegen/references/csharp.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ is **schema-migrations only** on the C# side (ADR-0015): `meta migrate` /
- Docs — one more entry in `codegen/Generators.cs`
- Persistence + routes are the deployed artifact
- Write your own generator
- Scoping a generator to some objects
- Extending the generators (open-for-extension, ADR-0002)
- Re-scaffold this context

Expand Down Expand Up @@ -53,6 +54,8 @@ public static class Generators

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

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

## Scoping a generator to some objects

C# has no TypeScript-style `filter`: `IGenerator` is `Name` + `Generate(GenContext)` + `OutDir`.
Since ADR-0056 `EntityGenerator` emits a POCO for every concrete `object.value` in
`ctx.Entities`, so a project whose value objects belong to another port (catalog or config
VOs) has to say which objects this generator is for. In order of preference:

1. **`Include` on the packaged generator.** `EntityGenerator`, `DbContextGenerator`,
`RoutesGenerator`, `FilterAllowlistGenerator`, `NamesGenerator` and `CallableGenerator` take
an optional `Func<MetaObject, bool>? Include` init property (it is `Include`, not `Filter`:
`Filter(MetaObject)` is the per-entity generators' own applicability method, which stays
the subclass seam). `null`, the default, accepts every object.
```csharp
new MetaObjects.Codegen.Generators.EntityGenerator { Include = o => NamingRefs.EffectivePackageOf(o) == "acme::orders" },
new MetaObjects.Codegen.Generators.DbContextGenerator { Include = o => NamingRefs.EffectivePackageOf(o) == "acme::orders" },
```
Give the same predicate to every entry that should agree: an `Include` on `entity` alone
leaves `db-context` mapping classes that were never emitted.
2. **Hand a generator a narrowed context.** For any `IGenerator` without `Include` (your own,
or a subclass), wrap it: `inner.Generate(ctx.Narrow(o => ...))`. `Narrow` returns the same
context with `Entities` reduced and everything else (`Root`, `Config`, `Warn`,
`Applicability`) kept. Build the `GenContext` by hand only if you must, and copy all of
those members.
3. **Eject and edit** (`dotnet meta eject <name>`): the owned copy's object selection is yours.

What narrowing reaches. Narrowing reduces `ctx.Entities`, and that is the set `entity`,
`db-context`, `routes`, `filter-allowlist`, `names` and `callable` iterate. A generator that
walks `ctx.Root` instead (the prompt tier: `output-parser`, `extractor`, `output-prompt`,
`render-helper`) is not narrowed, and neither is the resolution of a reference:
`EntityGenerator` finds the value objects an emitted entity points at through `ctx.Root`, so an
anchored value object is still emitted when `Include` rejects it by name. To drop a value
object nothing anchors, reject it in `Include`; to drop one an entity references, change the
model.

**The anti-pattern: letting a generator emit everything and dropping files by name
afterwards** (a `Where(f => !f.Path.StartsWith(...))` over `Generate`'s result). The emitted
set and the committed set then disagree, `verify --codegen` regenerates the unfiltered set and
reports drift, and a renamed file silently stops matching. Narrow the input, or own the
generator.

**A hosted runner** (your own `CodegenRunner.Run` call, not `dotnet meta gen`) must apply the
project's `naming` itself. `MetaDataLoader.FromDirectory(dir, registry, ...)` reads
`.metaobjects/config.json` above the directory and sets `root.PhysicalNaming` from it (since
1.1.2; through 1.1.1 a host got snake_case whatever the project said). `GenConfig` is built by
hand, so set its strategy from the root: `ColumnNamingStrategy = load.Root.PhysicalNaming.Columns`.
`MetadataLocation.Resolve(...).Load()` and `dotnet meta gen` do both for you.

## Extending the generators (open-for-extension, ADR-0002)

The generators are subclassable: the per-class emit methods are `protected virtual`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,19 @@ mechanical: if changing a generator — or a file eject copied beside it — fix
yours.

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

**How you narrow one is each language's own idiom, not one shared flag:**

| Port | Narrow a generator to some objects |
|---|---|
| **TypeScript** | the generator's `filter: (obj) => boolean` |
| **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 |
| **Python** | the `filter=` argument on the generator factory (a callable of the object), e.g. `trace_helper_generator(filter=...)` |
| **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 |

Every port gets a reference generator's source the same way — its `eject` command — and
writes a new one against the same small interface (the top of the skill). Your language
reference has the per-port mechanics.
Expand Down
5 changes: 5 additions & 0 deletions docs/features/migrations/0.x-to-1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ already shipped incrementally across `0.14`–`0.15`; the 1.0 cut is the moment
on all of them. The stability promise that begins at 1.0 is the
[compatibility policy](../../compatibility-policy.md).

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

## 0. Update the dependency version

Package versions **re-baseline** at the cut (both forward — see
Expand Down
Loading
Loading