diff --git a/CHANGELOG.md b/CHANGELOG.md index 9028d6d76..ed02e04c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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? 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`._ diff --git a/agent-context/skills/metaobjects-codegen/SKILL.md b/agent-context/skills/metaobjects-codegen/SKILL.md index 0b3aa831f..7c6a2ff82 100644 --- a/agent-context/skills/metaobjects-codegen/SKILL.md +++ b/agent-context/skills/metaobjects-codegen/SKILL.md @@ -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 @@ -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 diff --git a/agent-context/skills/metaobjects-codegen/references/csharp.md b/agent-context/skills/metaobjects-codegen/references/csharp.md index f95895c95..81def21b4 100644 --- a/agent-context/skills/metaobjects-codegen/references/csharp.md +++ b/agent-context/skills/metaobjects-codegen/references/csharp.md @@ -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 @@ -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: @@ -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? 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 `): 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`, diff --git a/agent-context/skills/metaobjects-codegen/references/owning-generators.md b/agent-context/skills/metaobjects-codegen/references/owning-generators.md index 30845017e..c78c09789 100644 --- a/agent-context/skills/metaobjects-codegen/references/owning-generators.md +++ b/agent-context/skills/metaobjects-codegen/references/owning-generators.md @@ -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 `` 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. diff --git a/docs/features/migrations/0.x-to-1.0.md b/docs/features/migrations/0.x-to-1.0.md index fffdc2cc5..164ad0000 100644 --- a/docs/features/migrations/0.x-to-1.0.md +++ b/docs/features/migrations/0.x-to-1.0.md @@ -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 diff --git a/docs/features/migrations/upgrading-1.0-to-1.1.md b/docs/features/migrations/upgrading-1.0-to-1.1.md index e1b98e6e1..1a7c342a7 100644 --- a/docs/features/migrations/upgrading-1.0-to-1.1.md +++ b/docs/features/migrations/upgrading-1.0-to-1.1.md @@ -13,7 +13,7 @@ skip them, so read the whole list before you regenerate. ## Before you start - **Upgrade every port together, because the failure is silent.** All five ports read - `.metaobjects/config.json`. Once you add `naming` (step 2), a 1.0.x TypeScript tool refuses + `.metaobjects/config.json`. Once you add `naming` (step 3), a 1.0.x TypeScript tool refuses it ("Unrecognized key(s) in object: 'naming'"), but a 1.0.x C#, Java or Python tool ignores the unknown key without a word and keeps its old column names. A mixed estate therefore does not fail: the old port reads and writes columns named like the fields while the new ones use @@ -27,15 +27,23 @@ skip them, so read the whole list before you regenerate. ## The steps -1. **Bump every MetaObjects package together**: all `@metaobjectsdev/*` at `1.1.0`, +1. **Refresh the agent context first**: `meta init --docs-only --refresh-docs` (or `npx meta + agent-docs`). A coding agent doing this upgrade reads the `metaobjects-*` skills in + `.claude/skills/`, and until this step they are the OLD version's, which describe the old + behaviour (the 1.0.x skill told C# and Python adopters they had no generator seam, which + was never true). Do it before anything below, so the rest of the upgrade is done on the + new skills. C#, Java and Python projects run this one command through Node `meta` + (`dotnet meta agent-docs` only prints a redirect). A file you edited is left alone and the + new copy written beside it as `.new`; merge it by hand and delete the `.new`. +2. **Bump every MetaObjects package together**: all `@metaobjectsdev/*` at `1.1.0`, `metaobjects` `1.1.0` (PyPI), `MetaObjects*` `1.1.0` (NuGet), `com.metaobjects:*` `8.1.0` (Maven, including the `metaobjects-maven-plugin` version in each `pom.xml`). Reinstall cleanly and read the resolved versions back. -2. **Set the physical naming rule** if your database's columns are named like the fields (C#, +3. **Set the physical naming rule** if your database's columns are named like the fields (C#, Java, the JVM runtime and Python; see "Column and table names"). TypeScript: move `columnNamingStrategy` to the same setting. This is the step that fails nothing when skipped. -3. **Give every port a generator list, and move documentation into it.** TypeScript: remove +4. **Give every port a generator list, and move documentation into it.** TypeScript: remove the `docs` block from `metaobjects.config.ts` and declare each docs generator. C#: run `dotnet meta init` once to write `codegen/Generators.cs`; without it `dotnet meta gen` and `verify --codegen` exit 2. Python: write the target's `generators` in @@ -43,13 +51,13 @@ skip them, so read the whole list before you regenerate. --out` form is gone. Java and Kotlin: the `` list in the pom, and the `metaobjects:docs` goal is removed. Do this whether or not you ever ran a docs command (see "Documentation is generated"). -4. **`meta upgrade`**, then `meta upgrade --apply` if it reports anything. Then load the model +5. **`meta upgrade`**, then `meta upgrade --apply` if it reports anything. Then load the model (`meta verify`), which refuses a leftover `columnNamingStrategy` or `docs` block until steps - 2 and 3 are done: a bare cross-package `extends` now fails with `ERR_UNRESOLVED_SUPER`, and + 3 and 4 are done: a bare cross-package `extends` now fails with `ERR_UNRESOLVED_SUPER`, and the message names the spelling to write (see "References and `extends`"). -5. **Java and Kotlin: let `mvn metaobjects:generate` write the hash manifest, then commit +6. **Java and Kotlin: let `mvn metaobjects:generate` write the hash manifest, then commit `.metaobjects/.gen-state/.hashes.json`** in every module (see "JVM hash manifest"). -6. **Resync the generators you own.** `meta eject --list` (TypeScript; `metaobjects gen --list` +7. **Resync the generators you own.** `meta eject --list` (TypeScript; `metaobjects gen --list` in Python, which has no `eject --list`; `dotnet meta gen --list`, `mvn metaobjects:eject -Dlist=true` in the other ports) reports each owned copy, and every file it owns beside it (in TypeScript the renderers @@ -57,14 +65,10 @@ skip them, so read the whole list before you regenerate. report output and none of the 1.1 fixes until you resync it. For a copy you never edited, `meta eject --force` (Java: `mvn metaobjects:eject -Dnames= -Dforce=true`) takes the new reference; for one you edited, three-way merge it. -7. **Regenerate** (`meta gen`, `dotnet meta gen`, `metaobjects gen`, `mvn metaobjects:generate`), +8. **Regenerate** (`meta gen`, `dotnet meta gen`, `metaobjects gen`, `mvn metaobjects:generate`), then build and run your tests. Read the generated diff against "Diffs to expect". -8. **Run the drift gates**: `meta verify --codegen` (and your port's `verify`). They should be +9. **Run the drift gates**: `meta verify --codegen` (and your port's `verify`). They should be clean; a column-name mismatch is reported naming the `naming` setting. -9. **Refresh the agent context**: `meta init --docs-only --refresh-docs` (or `npx meta - agent-docs`). A file you edited is left alone and the new copy written beside it as - `.new`; merge it by hand and delete the `.new`. - ## Changes that need action ### Column and table names (C#, Java, the JVM runtime, Python) @@ -151,7 +155,7 @@ digits than `@scale` or more integer digits than `@precision - @scale` is `400 v rather than rounded. A decimal filter operand is compared exactly; a bad one is `400 invalid_filter_value`. On SQLite and D1 a decimal keeps 15 significant digits, and `meta migrate` warns for one declared with `@precision` above 15. An owned routes generator -keeps its 1.0 decimal handling until you resync it (step 6). +keeps its 1.0 decimal handling until you resync it (step 7). ### Fractional validator bounds (TypeScript, C#, Python) @@ -196,6 +200,29 @@ whole number written `1.0` still loads, as `1`. `mvn metaobjects:eject -Dnames= -Dforce=true` and merge your edits back from `git diff`. +## Changes to generated text (for subclassed or post-processed generators) + +"Diffs to expect" is for someone who regenerates and reads the diff. If you subclass a +packaged generator (the sanctioned extension path, ADR-0002) or rewrite its output afterwards, +a rule keyed to the old text silently stops matching, so this is the list of emit-shape +changes, with the `protected virtual` seam that replaces a text rewrite. Prefer the seam: it +survives the next change to the text. The list is C#, where the request that produced it came +from; the Java/Kotlin and Python generators have their own extension seams and the changes +that reach them are in the CHANGELOG. + +| Changed in | Old text | New text | Seam instead of a rewrite | +|---|---|---|---| +| 1.0.5 and after | `HasForeignKey(nameof(Owner.Prop))` | `HasForeignKey(e => e.Prop)`, or `e => new { e.A, e.B }` for a composite (the `M:N` `UsingEntity` sides too) | `DbContextGenerator.EmitOnModelCreatingBody(sb, modelLines, ctx)` receives the lines as a list before they are written. There is no seam for the key expression itself, so match the lambda form | +| 1.1.0 | scalar and enum arrays `ICollection` | `IList`; a non-required value-object array is nullable and has no initializer | `EntityGenerator.ScalarProperty(...)` / `EnumProperty(...)` return the property text; override the one you changed | +| 1.1.0 | no `JsonIgnore` on value-object members | on a nullable member, `[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]` beside `[JsonPropertyName("…")]` | `EntityGenerator.EmitPropertyAttributes(sb, owner, field, ctx)` adds attributes; to change the POCO itself, override `EmitValueObjectPoco(vo, ctx)` | +| 1.1.0 | `decimal` written and read as a JSON number | a `[DecimalWire…]` converter attribute on each decimal member and one `DecimalWire.g.cs` per namespace | `EntityGenerator.ScalarProperty(...)` for the member; `EmitFileUsings(sb, entity, ctx)` for the usings the attribute needs | +| 1.0.13 | `DbSet` and navigation names from the 1.0 pluralizer (`CallScriptses`) | an already-plural entity name stays as it is (`CallScripts`) | `DbContextGenerator.EmitDbSetDeclarations(sb, objects, ctx)`; the name itself is `CSharpNaming`'s, so a rewrite keyed to the old plural has to be re-keyed | +| 1.1.0 | a reference written bare in canonical output | `pkg::Name` (a root-level target is `::Name`), so `MetaReferenceIdentity.TargetEntity` is a qualified reference | none needed: resolve a reference through the model (`ValueObjectNames.ResolveFieldRef(field, ctx.Root)` for an `@objectRef`) instead of comparing the string | + +Per-entry overrides that need no text rewrite at all: `new EntityGenerator { Include = o => … }` +scopes a packaged generator to some objects (1.1.2; see `references/csharp.md` in the +`metaobjects-codegen` skill), and `PropertyName(field)` renames a property. + ## CLI behaviour that changed - `meta gen` on a model that does not load exits 1 (1.0.13 exited 2). diff --git a/docs/features/migrations/upgrading-within-1.0.md b/docs/features/migrations/upgrading-within-1.0.md index 28c4dc05e..d72f37bf7 100644 --- a/docs/features/migrations/upgrading-within-1.0.md +++ b/docs/features/migrations/upgrading-within-1.0.md @@ -13,6 +13,11 @@ cross. ## The steps +0. **Refresh the agent context first**: `meta init --docs-only --refresh-docs` (or `npx meta + agent-docs`). It updates `.metaobjects/` and `.claude/skills/metaobjects-*`; an agent doing + this upgrade reads those skills, and until they are refreshed they describe the OLD version. + A file you edited is left alone and the new copy is written beside it as `.new`; + merge it by hand and delete the `.new`. 1. **Bump every MetaObjects package together**: all `@metaobjectsdev/*` at the same version (npm), `metaobjects` (PyPI), `MetaObjects*` (NuGet), `com.metaobjects:*` at npm major + 7 (Maven). Reinstall cleanly and read the resolved versions back. @@ -26,9 +31,6 @@ cross. learn what changed. 5. **Schema.** If `meta gen` or `meta migrate --dry-run` reports a schema change, write the migration your usual way and read it before applying it (see "Views" below). -6. **Agent context.** `meta init --docs-only --refresh-docs` (or `npx meta agent-docs`) updates - `.metaobjects/` and `.claude/skills/metaobjects-*`. A file you edited is left alone and the - new copy is written beside it as `.new`; merge it by hand and delete the `.new`. ## What to look for after `meta gen` diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index 0b3aa831f..7c6a2ff82 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -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 @@ -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 diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 30845017e..c78c09789 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -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 `` 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. diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md index 0b3aa831f..7c6a2ff82 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -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 @@ -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 diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 30845017e..c78c09789 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -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 `` 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. diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md index 0b3aa831f..7c6a2ff82 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -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 @@ -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 diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 30845017e..c78c09789 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -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 `` 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. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index 0b3aa831f..7c6a2ff82 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -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 @@ -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 diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 30845017e..c78c09789 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -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 `` 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. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md index 0b3aa831f..7c6a2ff82 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -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 @@ -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 diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 30845017e..c78c09789 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -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 `` 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. diff --git a/server/csharp/MetaObjects.Codegen.Tests/GeneratorIncludeTests.cs b/server/csharp/MetaObjects.Codegen.Tests/GeneratorIncludeTests.cs new file mode 100644 index 000000000..c1103f4ae --- /dev/null +++ b/server/csharp/MetaObjects.Codegen.Tests/GeneratorIncludeTests.cs @@ -0,0 +1,92 @@ +// The packaged generators' `Include` property (#455): scope an entry to some objects the way +// a TypeScript `filter` does, without dropping emitted files by name after the fact. + +using MetaObjects.Codegen; +using MetaObjects.Codegen.Generators; +using MetaObjects.Loader; +using MetaObjects.Meta; +using Xunit; + +namespace MetaObjects.Codegen.Tests; + +public class GeneratorIncludeTests +{ + private const string Model = """ + { "metadata.root": { "package": "acme", "children": [ + { "object.entity": { "name": "Order", "children": [ + { "source.rdb": { "@table": "orders" } }, + { "field.long": { "name": "id" } }, + { "field.object": { "name": "ship", "@objectRef": "ShipTo" } }, + { "identity.primary": { "@fields": "id" } } + ]}}, + { "object.entity": { "name": "Customer", "children": [ + { "source.rdb": { "@table": "customers" } }, + { "field.long": { "name": "id" } }, + { "identity.primary": { "@fields": "id" } } + ]}}, + { "object.value": { "name": "ShipTo", "children": [ { "field.string": { "name": "street" } } ]}}, + { "object.value": { "name": "CatalogEntry", "children": [ { "field.string": { "name": "sku" } } ]}} + ]}} + """; + + private static GenContext Ctx() + { + var r = new MetaDataLoader().Load([new InMemoryStringSource(Model, id: "gen.json")]); + Assert.Empty(r.Errors); + return new GenContext + { + Entities = r.Root.Objects(), Root = r.Root, + Config = new GenConfig { OutDir = "/tmp", Namespace = "Acme.Generated" }, + }; + } + + private static string Paths(IGenerator g) => string.Join("|", g.Generate(Ctx()).Select(f => f.Path).OrderBy(p => p)); + + [Fact] + public void Entity_include_scopes_entities_and_value_objects() + { + var all = Paths(new EntityGenerator()); + Assert.Contains("Customer.g.cs", all); + Assert.Contains("CatalogEntry.g.cs", all); + + var scoped = Paths(new EntityGenerator { Include = o => o.Name is "Order" }); + Assert.Contains("Order.g.cs", scoped); + Assert.DoesNotContain("Customer.g.cs", scoped); + // Not accepted, so no POCO of its own... + Assert.DoesNotContain("CatalogEntry.g.cs", scoped); + // ...but a value object an accepted entity points at still resolves through the root. + Assert.Contains("ShipTo.g.cs", scoped); + } + + [Fact] + public void DbContext_include_maps_only_the_accepted_objects() + { + var src = Assert.Single(new DbContextGenerator { Include = o => o.Name is "Customer" }.Generate(Ctx())).Content; + Assert.Contains("DbSet", src); + Assert.DoesNotContain("DbSet", src); + } + + [Fact] + public void Per_entity_generators_honour_include() + { + foreach (IGenerator g in new IGenerator[] + { new RoutesGenerator { Include = o => o.Name is "Order" }, + new FilterAllowlistGenerator { Include = o => o.Name is "Order" }, + new NamesGenerator { Include = o => o.Name is "Order" } }) + { + var paths = Paths(g); + Assert.Contains("Order", paths); + Assert.DoesNotContain("Customer", paths); + } + } + + [Fact] + public void No_include_is_unchanged_and_narrow_keeps_the_root() + { + var ctx = Ctx(); + Assert.Same(ctx, ctx.Narrow(null)); + var narrowed = ctx.Narrow(o => o.Name == "Order"); + Assert.Single(narrowed.Entities); + Assert.Same(ctx.Root, narrowed.Root); + } +} diff --git a/server/csharp/MetaObjects.Codegen/Generator.cs b/server/csharp/MetaObjects.Codegen/Generator.cs index fda57f6a3..032fe9890 100644 --- a/server/csharp/MetaObjects.Codegen/Generator.cs +++ b/server/csharp/MetaObjects.Codegen/Generator.cs @@ -295,6 +295,31 @@ public sealed class GenContext /// ): the packaged generators' answers. /// public GeneratorApplicability Applicability { get; init; } = GeneratorApplicability.Packaged; + + /// + /// The predicate applied, so a view-backed report's row model (which + /// joins a generator's set from , not from ) is + /// held to it too. null on a context nobody narrowed. + /// + public Func? Scope { get; init; } + + /// + /// The same context with reduced to the objects + /// accepts (and every report row model held to it too). + /// is untouched, so a generator that resolves a reference through it + /// (a value object an emitted entity points at) still finds the target. null returns + /// this context. This is what the packaged generators' Include property does. + /// + public GenContext Narrow(Func? include) => + include is null ? this : new GenContext + { + Entities = Entities.Where(include).ToList(), + Root = Root, + Config = Config, + Warn = Warn, + Applicability = Applicability, + Scope = Scope is null ? include : o => Scope(o) && include(o), + }; } /// A codegen unit. Kebab-case surfaces in diagnostics. @@ -376,5 +401,13 @@ public abstract class PerEntityGenerator : IGenerator protected abstract EmittedFile GenerateOne(MetaObject entity, GenContext ctx); public virtual IEnumerable Generate(GenContext ctx) => - ctx.Entities.Where(Filter).Select(e => GenerateOne(e, ctx)); + ctx.Narrow(Include).Entities.Where(Filter).Select(e => GenerateOne(e, ctx)); + + /// + /// Scope this entry to some objects: only those it accepts are generated for. Set on the + /// entry in codegen/Generators.cs (new RoutesGenerator { Include = o => ... }). + /// null, the default, accepts every object. Separate from , + /// which is the generator's OWN applicability rule and stays the subclass seam. + /// + public Func? Include { get; init; } } diff --git a/server/csharp/MetaObjects.Codegen/Generators/DbContextGenerator.cs b/server/csharp/MetaObjects.Codegen/Generators/DbContextGenerator.cs index bbb2ef233..ab4b56154 100644 --- a/server/csharp/MetaObjects.Codegen/Generators/DbContextGenerator.cs +++ b/server/csharp/MetaObjects.Codegen/Generators/DbContextGenerator.cs @@ -68,8 +68,13 @@ public static bool AppliesTo(MetaObject obj, MetaRoot root) => && InstanceArtifacts.EmitsInstanceArtifacts(obj) && !TphPlanBuilder.IsTphSubtype(obj, root); + /// Scope this entry to some objects: only those it accepts get a DbSet + /// and mapping. null, the default, accepts every object. See . + public Func? Include { get; init; } + public virtual IEnumerable Generate(GenContext ctx) { + ctx = ctx.Narrow(Include); // FR-017 TPH: a concrete subtype shares the base's single table — it gets NO // DbSet and no per-subtype model config; the hierarchy is reached via the base // DbSet (`.OfType()`). Filter subtypes out of the emitted set entirely. diff --git a/server/csharp/MetaObjects.Codegen/Generators/EntityGenerator.cs b/server/csharp/MetaObjects.Codegen/Generators/EntityGenerator.cs index 846c73818..b7a37aa5f 100644 --- a/server/csharp/MetaObjects.Codegen/Generators/EntityGenerator.cs +++ b/server/csharp/MetaObjects.Codegen/Generators/EntityGenerator.cs @@ -43,8 +43,15 @@ public class EntityGenerator : IGenerator /// like NPI/PARequired that a plain PascalCase of the field name would mangle). protected virtual string PropertyName(MetaField field) => CSharpNaming.Pascal(field.Name); + /// Scope this entry to some objects: only those it accepts are emitted (entities + /// and value objects alike). A value object an emitted entity points at is still emitted, + /// because it is resolved through the model root. null, the default, accepts every + /// object. See . + public Func? Include { get; init; } + public virtual IEnumerable Generate(GenContext ctx) { + ctx = ctx.Narrow(Include); // FR-044 — a view-backed report joins the set as its ROW MODEL (a keyless, // projection-shaped object carrying its derived fields); every other report node // is dropped. See ReportRows. diff --git a/server/csharp/MetaObjects.Codegen/Generators/FilterAllowlistGenerator.cs b/server/csharp/MetaObjects.Codegen/Generators/FilterAllowlistGenerator.cs index dad246683..77f90d6d7 100644 --- a/server/csharp/MetaObjects.Codegen/Generators/FilterAllowlistGenerator.cs +++ b/server/csharp/MetaObjects.Codegen/Generators/FilterAllowlistGenerator.cs @@ -65,7 +65,7 @@ public static bool AppliesTo(MetaObject entity) => // FR-044 — a served report joins the set as its ROW MODEL (ReportRows), whose derived // fields are what the allowlist lists; a raw report node in the entity set is dropped. public override IEnumerable Generate(GenContext ctx) => - ReportRows.WithReportRows(ctx).Where(Filter).Select(e => GenerateOne(e, ctx)); + ReportRows.WithReportRows(ctx.Narrow(Include)).Where(Filter).Select(e => GenerateOne(e, ctx)); protected override EmittedFile GenerateOne(MetaObject entity, GenContext ctx) { diff --git a/server/csharp/MetaObjects.Codegen/Generators/NamesGenerator.cs b/server/csharp/MetaObjects.Codegen/Generators/NamesGenerator.cs index 7001a2069..74c9186a4 100644 --- a/server/csharp/MetaObjects.Codegen/Generators/NamesGenerator.cs +++ b/server/csharp/MetaObjects.Codegen/Generators/NamesGenerator.cs @@ -47,7 +47,7 @@ public override IEnumerable Generate(GenContext ctx) var emitted = new HashSet(StringComparer.Ordinal); var files = new List(); - var participants = ctx.Entities.Where(Filter).ToList(); + var participants = ctx.Narrow(Include).Entities.Where(Filter).ToList(); foreach (var entity in participants) { emitted.Add(entity.Name); diff --git a/server/csharp/MetaObjects.Codegen/Generators/RoutesGenerator.cs b/server/csharp/MetaObjects.Codegen/Generators/RoutesGenerator.cs index 7ec86477a..3587cf869 100644 --- a/server/csharp/MetaObjects.Codegen/Generators/RoutesGenerator.cs +++ b/server/csharp/MetaObjects.Codegen/Generators/RoutesGenerator.cs @@ -79,7 +79,7 @@ public static bool AppliesTo(MetaObject entity, MetaRoot root) => // projection-shaped object, so GenerateStandardRoutes emits the list GET and the POST // refusal and nothing else. A raw report node in the entity set is dropped. public override IEnumerable Generate(GenContext ctx) => - ReportRows.WithReportRows(ctx) + ReportRows.WithReportRows(ctx.Narrow(Include)) .Where(e => AppliesTo(e, ctx.Root)) .Select(e => GenerateOne(e, ctx)); diff --git a/server/csharp/MetaObjects.Codegen/ReportRows.cs b/server/csharp/MetaObjects.Codegen/ReportRows.cs index d735c997e..f2ee3b67e 100644 --- a/server/csharp/MetaObjects.Codegen/ReportRows.cs +++ b/server/csharp/MetaObjects.Codegen/ReportRows.cs @@ -216,5 +216,5 @@ public static IReadOnlyList For(MetaRoot root) => /// /// public static IReadOnlyList WithReportRows(GenContext ctx) => - [.. ctx.Entities.Where(o => !o.IsReport()), .. For(ctx.Root)]; + [.. ctx.Entities.Where(o => !o.IsReport()), .. For(ctx.Root).Where(o => ctx.Scope?.Invoke(o) ?? true)]; } diff --git a/server/csharp/MetaObjects.Conformance.Tests/MetadataLocationTests.cs b/server/csharp/MetaObjects.Conformance.Tests/MetadataLocationTests.cs index cb3706f92..5a7807289 100644 --- a/server/csharp/MetaObjects.Conformance.Tests/MetadataLocationTests.cs +++ b/server/csharp/MetaObjects.Conformance.Tests/MetadataLocationTests.cs @@ -108,4 +108,24 @@ public void Several_declared_sources_are_refused_in_words() var ex = Assert.Throws(() => MetadataLocation.Resolve(null, cwd: _root)); Assert.Contains("2 metadata sources", ex.Message); } + + [Fact] + public void FromDirectory_with_a_registry_applies_the_projects_naming() + { + WriteConfig("""{ "schema_version": 1, "naming": { "columns": "literal", "tables": "literal" } }"""); + + var result = MetaObjects.Loader.MetaDataLoader.FromDirectory( + MetaDir, MetaObjects.Provider.ComposeRegistry(MetaObjects.CoreTypes.LibraryProviders), libraries: ["iam"], strict: true); + + Assert.Equal(new PhysicalNaming(ColumnNamingStrategy.Literal, TableNamingRule.Literal), result.Root.PhysicalNaming); + } + + [Fact] + public void FromDirectory_without_a_project_config_keeps_the_default() + { + var result = MetaObjects.Loader.MetaDataLoader.FromDirectory( + MetaDir, MetaObjects.Provider.ComposeRegistry(MetaObjects.CoreTypes.LibraryProviders), libraries: ["iam"]); + + Assert.Equal(PhysicalNaming.Default, result.Root.PhysicalNaming); + } } diff --git a/server/csharp/MetaObjects/Config/MetadataLocation.cs b/server/csharp/MetaObjects/Config/MetadataLocation.cs index 2b8a65e32..4c7fa89a4 100644 --- a/server/csharp/MetaObjects/Config/MetadataLocation.cs +++ b/server/csharp/MetaObjects/Config/MetadataLocation.cs @@ -60,7 +60,7 @@ public static string ProjectRootFor(string metadataDir) => /// dotnet meta gen /repo/model/catalog reads /repo/.metaobjects/config.json, /// as the Maven plugin does. Null when no such ancestor exists. /// - private static NeutralConfig? ConfigFor(string metadataDir) + internal static NeutralConfig? ConfigFor(string metadataDir) { for (var dir = new DirectoryInfo(ProjectRootFor(metadataDir)); dir is not null; dir = dir.Parent) { diff --git a/server/csharp/MetaObjects/Loader/MetaDataLoader.cs b/server/csharp/MetaObjects/Loader/MetaDataLoader.cs index c9d9e4c58..d47461014 100644 --- a/server/csharp/MetaObjects/Loader/MetaDataLoader.cs +++ b/server/csharp/MetaObjects/Loader/MetaDataLoader.cs @@ -151,7 +151,12 @@ public static LoadResult FromDirectory(string directory, TypeRegistry registry, } var libSources = Library.LibrarySources.Resolve(libraries); if (libSources.Count > 0) sources.InsertRange(0, libSources); - return new MetaDataLoader(registry, strict: strict).Load(sources); + var result = new MetaDataLoader(registry, strict: strict).Load(sources); + // ADR-0058: the project's `naming` applies to a directory load too, as the JVM + // fromDirectory does. Without this, a host that composes its own registry (the + // documented way to add a provider) silently got snake_case whatever the project says. + if (Config.MetadataLocation.ConfigFor(directory) is { } cfg) result.Root.PhysicalNaming = cfg.Naming; + return result; } /// diff --git a/server/csharp/MetaObjects/Meta/MetaRoot.cs b/server/csharp/MetaObjects/Meta/MetaRoot.cs index d2cab0667..169620b9d 100644 --- a/server/csharp/MetaObjects/Meta/MetaRoot.cs +++ b/server/csharp/MetaObjects/Meta/MetaRoot.cs @@ -14,8 +14,8 @@ public class MetaRoot(TypeId typeId, string name) : MetaData(typeId, name) /// The project's physical-naming rules (naming in .metaobjects/config.json, /// ADR-0058). Set by the project-aware load (), /// read by every name resolver through . A tree loaded with - /// no project — or built directly via MetaDataLoader.FromDirectory/FromUris - /// with no project-aware wrapper — keeps the default. + /// no project — or built via MetaDataLoader.FromUris with no project-aware + /// wrapper — keeps the default. FromDirectory reads the project's config itself. /// public PhysicalNaming PhysicalNaming { get; set; } = PhysicalNaming.Default; diff --git a/server/typescript/packages/cli/src/commands/upgrade.ts b/server/typescript/packages/cli/src/commands/upgrade.ts index 6b6a3c261..4e1bcbf95 100644 --- a/server/typescript/packages/cli/src/commands/upgrade.ts +++ b/server/typescript/packages/cli/src/commands/upgrade.ts @@ -83,6 +83,14 @@ export async function upgradeCommand(args: string[], cwd: string): Promise