Skip to content

feat(metadata): add @collectionName and naming.collections pluralizer across all five ports - #558

Merged
dmealing merged 4 commits into
mainfrom
fm/mo-rc19-pr1-loaders
Nov 12, 2026
Merged

dmealing merged 4 commits into
mainfrom
fm/mo-rc19-pr1-loaders

Conversation

@dmealing

@dmealing dmealing commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • Rename the port-scoped C#-only @dbSetName attr to @collectionName on object.entity, object.projection and object.report, with no alias (it was never published). Update spec/metamodel/object.json, the registry manifest and canonical, the per-port embedded copies, the generated metamodel docs and the TS/C#/Java/Python constants. @objectAdapter stays the one port-scoped attr (["java", "kotlin"]). Conformance fixtures: object-port-scoped-attr-dbsetname becomes object-collection-name, and a new object-port-scoped-attr-object-adapter fixture covers the port-scoped case.
  • Add the naming.collections pluralizer (standard default, none, or an application-registered one). It is read in the TS sdk config and the C#, Java and Python NeutralConfig loaders, which refuse an unknown pluralizer at config read. Registration entry points are registerPluralizer (TS, Java), RegisterPluralizer (C#) and register_pluralizer (Python), alongside the existing column/table naming rules. The C# generated context uses @collectionName for the DbSet property name in place of the derived plural (CSharpNaming, DbContextGenerator).
  • Add per-port tests for the attr and pluralizer: collection-naming-config.test.ts, CollectionNameVocabularyTests.cs, CollectionNamingTest.java and test_collection_naming.py. Update CHANGELOG.md, docs/features/migrations/physical-naming.md, docs/CONFORMANCE.md and the generated site-reference pages to match.
Validation

Risk Assessment

✅ Low: The fix round closed all three round-1 warnings: the TS config reads and carries naming.collections, the C# and Python probe candidates keep the pluralizer, and Java, TS and C# now have tests checked against the shared naming-conformance corpus. The remaining gap, TS cannot register a custom pluralizer from metaobjects.config.ts, is PR 2 scope.

Testing

Drove the TS and Python CLIs and loaders live against disposable local projects. Confirmed valid naming.collections and @collectionName load. Confirmed bad pluralizer names and misplaced attrs are refused with clear errors. Java and C# were verified by unit and conformance suites only, with no live run; those scenarios are recorded as untested. The full TS server suite did not complete, and Python integration tests cannot collect without pg8000. Those gaps are reported as findings.

  • Live validation: ✅ go - 5 of 8 scenarios driven live against the product
Scenario Result Live Evidence
TS: project with naming.collections=none and @collectionName on an entity loads and verifies (live TS CLI, disposable project) ✅ pass live naming-collections-live.txt; node dist/bin/meta.js verify --templates exit 0 in a disposable project
TS: naming.collections=latin is refused with a clear message (adversarial, live TS CLI) ✅ pass live naming-collections-live.txt; exit 1 with "naming.collections" names "latin" message
TS: @collectionName on a field is refused as unknown attr (adversarial boundary, live TS CLI) ✅ pass live naming-collections-live.txt; ERR_UNKNOWN_ATTR, exit 1
Python: loader strict-loads valid @collectionName and naming config, refuses field-level misuse (live Python loader against local synthetic project) ✅ pass live naming-collections-live.txt; ok: no errors, objects Stats/Address; adv: ERR_UNKNOWN_ATTR
Python: naming.collections=latin refused by the CLI (adversarial, live Python CLI) ✅ pass live Python CLI transcript in the session; error "naming.collections" names "latin" with exit 2
Java: naming.collections read, unknown pluralizer refused, registering none throws, standardPluralize conformance cases (Category/Address/Stats) ⏸️ untested no Downgraded: no live Java CLI or loader run in this session. Only mvn -o -pl metadata test unit results exist (CollectionNamingTest 7/7, NeutralConfigTest 16/16, metadata module 3337/3337), which do no…
C#: CollectionNameVocabularyTests and NamingStrategiesTests load and validate the new attr and pluralizer ⏸️ untested no Downgraded: no live C# loader or CLI run in this session. Only dotnet test unit and conformance results exist (Conformance 2403 pass, Codegen Naming 127 pass), which do not count as live.
Full regression suites for changed ports ⏸️ untested no Downgraded: regression suites are unit and test runs, not live product drives. TS metadata 3970, sdk 443, cli 2006 pass; Python unit+loader 746 pass; Java metadata 3337 pass; C# Conformance 2403 pass.…
Evidence: Live CLI and Python loader transcripts for naming.collections and @collectionName
## TS meta, naming.collections=latin
meta: <project>/.metaobjects/config.json is not a valid MetaObjects config:
  naming.collections: "naming.collections" names "latin", which no collection-naming (pluralizer) strategy registered in the TypeScript port answers (built in: standard, none; registered by the application: none). Register it before .metaobjects/config.json is read, or correct the name.
exit=1
## TS meta, field-level @collectionName
meta: failed to load metadata: ERR_UNKNOWN_ATTR: Unknown attribute '@collectionName' on field.string 'label' — not declared by any registered provider for field.string
## Python loader strict, valid vs field-level
ok errors: [] objects: ['Stats', 'Address']
adv errors: ['MetaError(ERR_UNKNOWN_ATTR: "Unknown attribute \'@collectionName\' on field.string \'label\' — not declared by any registere'] objects: ['Stats']
  • Outcome: ⚠️ 3 issues (1 warning, 2 infos) across 1 run (1h6m7s)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 3 infos
  • ⚠️ server/typescript/packages/sdk/src/config.ts:238 - The TypeScript config reader rejects naming.collections. The sdk naming zod schema allows only columns and tables and is .strict(). A project that sets &#34;naming&#34;: { &#34;collections&#34;: &#34;none&#34; } therefore fails every Node meta command (gen, migrate, verify) with an unknown-key error. This includes a C# adopter, who must run Node meta migrate for schema (ADR-0015). physicalNamingFromConfig (config.ts:315-320) also never copies collections onto the root's PhysicalNaming, so the new collectionNameOf (metadata/src/naming.ts:447) always falls back to standard. Three places claim the key is read in every port: CHANGELOG.md:110 ("Every port registers the attr and reads the key, and refuses an unknown pluralizer when it reads the config"), the naming table in docs/features/migrations/physical-naming.md:30, and the registerPluralizer docstring in naming.ts:215. Fix: add collections: z.string().superRefine(n =&gt; isRegisteredPluralizer(n) || issue(unknownNamingStrategyMessage(&#34;collections&#34;, n))).optional() to the schema, carry collections through physicalNamingFromConfig, and add an sdk test that reads the config (accepts none, refuses an unknown name). This may have been planned for PR 2, but PR 1's CHANGELOG and docs already state it as shipped.
  • ⚠️ server/csharp/MetaObjects.Codegen/CodegenCli.cs:818 - The verify --codegen naming probe drops the project's pluralizer and miscompares candidates. PhysicalNaming (C# record) and NamingRules (Python dataclass) now include collections in equality, but NamingCandidates still builds new PhysicalNaming(c, t) with Collections = null. Take a project with &#34;naming&#34;: {&#34;collections&#34;: &#34;none&#34;} (or an explicit &#34;standard&#34;). The candidate with the same columns and tables as used now compares unequal and is probed: one wasted regeneration today. Every probe also replaces root.PhysicalNaming with a candidate that has lost the configured pluralizer. Once C# codegen reads Collections (PR 3), a probe can match on the pluralizer alone, and NamingDiffers then prints two identical naming blocks instead of saying "regenerate". Same defect in Python at server/python/src/metaobjects/cli.py:2323 (NamingRules(columns=c, tables=t) takes the default collections=&#34;standard&#34;). Fix: carry the used pluralizer into each candidate: new PhysicalNaming(c, t) { Collections = used.Collections } and NamingRules(columns=c, tables=t, collections=used.collections).
  • ⚠️ server/java/metadata/src/main/java/com/metaobjects/config/NeutralConfig.java:341 - The new Java work has no tests. The intent requires that "Python and Java accept naming.collections", but nothing executes Java's naming.collections read, the unknown-pluralizer refusal in PhysicalNaming.of(columns, tables, collections), NamingStrategies.registerPluralizer and its built-in protection, or standardPluralize (NamingStrategies.java:103). Python has test_collection_naming.py for the same behavior. Add a Java test like the Python one. Write a temp .metaobjects/config.json and read it through NeutralConfig. Assert that collections: &#34;none&#34; reads back and that an absent key gives null. Assert that &#34;latin&#34; fails with the &#34;naming.collections&#34; names &#34;latin&#34; message. Assert that registering none throws. Check standardPluralize against the shared naming-conformance cases (Category→Categories, Address→Addresses, Stats→Stats). Example wrong behavior this would catch: the suia already-plural check is inverted and Address becomes Address. TS registerPluralizer/pluralizer/collectionNameOf (naming.ts:219-458) and C# CollectionNames.StandardPluralize/Of (CollectionNames.cs:18-45) are also new and have no direct tests.
  • ℹ️ server/csharp/MetaObjects.Codegen/CSharpNaming.cs:374 - naming.collections does nothing yet in any generator. C# DbSetName still falls back to Pluralize(Pascal(entity.Name)) and ignores the configured pluralizer, and CollectionNames.Of / collectionNameOf have no callers. The CHANGELOG and the new attr description ("Absent, the project's pluralizer derives it") describe the finished behavior. This matches the four-PR plan (PR 3 wires C# codegen), and 1.3.0 is held until all four land, so no action is needed in PR 1. Make sure PR 3 switches DbSetName to CollectionNames.Of.
  • ℹ️ server/typescript/packages/metadata/test/object-definition-completeness.test.ts:89 - Corrected the comment "@collectionName (implementedIn C# + TypeScript)" to "(implementedIn C#)". The registry marks it implementedIn: [&#34;csharp&#34;] only.

🔧 Fix applied.
3 infos still open:

  • ℹ️ server/csharp/MetaObjects.Codegen/CSharpNaming.cs:374 - naming.collections does nothing yet in any generator. C# DbSetName still falls back to Pluralize(Pascal(entity.Name)) and ignores the configured pluralizer, and CollectionNames.Of / collectionNameOf have no callers. The CHANGELOG and the new attr description ("Absent, the project's pluralizer derives it") describe the finished behavior. This matches the four-PR plan (PR 3 wires C# codegen), and 1.3.0 is held until all four land, so no action is needed in PR 1. Make sure PR 3 switches DbSetName to CollectionNames.Of.
  • ℹ️ server/typescript/packages/metadata/test/object-definition-completeness.test.ts:89 - Corrected the comment "@collectionName (implementedIn C# + TypeScript)" to "(implementedIn C#)". The registry marks it implementedIn: [&#34;csharp&#34;] only.
  • ℹ️ server/typescript/packages/cli/src/lib/naming-preload.ts:48 - A TypeScript project cannot select an application-registered pluralizer through the CLI. namingStrategies in metaobjects.config.ts (codegen-ts/src/metaobjects-config.ts:172) has only columns and tables, so registerNamingStrategies (cli/src/lib/load-metaobjects-config.ts:577) never registers a pluralizer. registerProjectNamingStrategies also reads only columns and tables. A config with naming.collections: &#34;my-plural&#34; is refused by the reader on every meta command, and only a program that calls registerPluralizer before loading can satisfy it. The built-ins (standard, none) work, and the fix round's TS config read is correct. docs/features/migrations/physical-naming.md says pluralizers register "the same way" as table rules, which is not true for the TS config file. This fits PR 2 (TS codegen): add collections to NamingStrategies, registerNamingStrategies and the preload check, or narrow the doc sentence for TS.
⚠️ **Test** - 3 issues (1 warning, 2 infos)
  • ⚠️ Full TS server suite (bun test in server/typescript) was killed at the 10-minute background limit with no output, so it was not verified. Touched packages metadata, sdk and cli pass in full. Re-run the full TS server suite in a longer background run, or in per-package chunks.
  • ℹ️ Python integration tests (tests/integration) fail at collection with ModuleNotFoundError: pg8000. This is a missing dev dependency in this environment, not a code failure. Unit and loader suites pass (746).
  • ℹ️ Java and C# naming behavior was not driven live in this run. Coverage is unit and conformance tests only (Java CollectionNamingTest 7/7, metadata module 3337 pass; C# Conformance 2403 pass, Codegen Naming 127 pass).
  • Live validation: ✅ go - 5 of 8 scenarios driven live against the product
Scenario Result Live Evidence
TS: project with naming.collections=none and @collectionName on an entity loads and verifies (live TS CLI, disposable project) ✅ pass live naming-collections-live.txt; node dist/bin/meta.js verify --templates exit 0 in a disposable project
TS: naming.collections=latin is refused with a clear message (adversarial, live TS CLI) ✅ pass live naming-collections-live.txt; exit 1 with "naming.collections" names "latin" message
TS: @collectionName on a field is refused as unknown attr (adversarial boundary, live TS CLI) ✅ pass live naming-collections-live.txt; ERR_UNKNOWN_ATTR, exit 1
Python: loader strict-loads valid @collectionName and naming config, refuses field-level misuse (live Python loader against local synthetic project) ✅ pass live naming-collections-live.txt; ok: no errors, objects Stats/Address; adv: ERR_UNKNOWN_ATTR
Python: naming.collections=latin refused by the CLI (adversarial, live Python CLI) ✅ pass live Python CLI transcript in the session; error "naming.collections" names "latin" with exit 2
Java: naming.collections read, unknown pluralizer refused, registering none throws, standardPluralize conformance cases (Category/Address/Stats) ⏸️ untested no Downgraded: no live Java CLI or loader run in this session. Only mvn -o -pl metadata test unit results exist (CollectionNamingTest 7/7, NeutralConfigTest 16/16, metadata module 3337/3337), which do no…
C#: CollectionNameVocabularyTests and NamingStrategiesTests load and validate the new attr and pluralizer ⏸️ untested no Downgraded: no live C# loader or CLI run in this session. Only dotnet test unit and conformance results exist (Conformance 2403 pass, Codegen Naming 127 pass), which do not count as live.
Full regression suites for changed ports ⏸️ untested no Downgraded: regression suites are unit and test runs, not live product drives. TS metadata 3970, sdk 443, cli 2006 pass; Python unit+loader 746 pass; Java metadata 3337 pass; C# Conformance 2403 pass.…
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • Baseline: scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains (given)
  • bun test in server/typescript/packages/{sdk,metadata}/test/collection-naming-config.test.ts, object-definition-completeness.test.ts, port-scoped-attr.test.ts
  • python3 -m pytest tests/unit/test_collection_naming.py tests/loader/test_port_scoped_attr.py
  • mvn -o -pl metadata test -Dtest=CollectionNamingTest,PortScopedAttrTest (Java surefire reports read: 7 and 4 tests, 0 fail)
  • dotnet test MetaObjects.Codegen.Tests --filter Naming (127 pass)
  • dotnet test MetaObjects.Conformance.Tests (2403 pass, includes CollectionNameVocabularyTests and NamingStrategiesTests)
  • Rebuilt TS CLI: cd server/typescript/packages/cli &amp;&amp; bun run build
  • Live TS CLI node dist/bin/meta.js verify --templates on a disposable project with .metaobjects/config.json naming.collections=none, @collectionName on an entity: exit 0
  • Live TS CLI on a disposable project with naming.collections=latin: refused with &#34;naming.collections&#34; names &#34;latin&#34;, exit 1
  • Live TS CLI on a disposable project with @collectionName on field.string: refused with ERR_UNKNOWN_ATTR, exit 1
  • Live Python loader MetaDataLoader.from_directory(..., strict=True) on ok project: loads Stats and Address, no errors; on adv project: ERR_UNKNOWN_ATTR
  • Live Python CLI metaobjects main on bad project: naming.collections latin refused with same message, exit 2
  • Full regression: bun test in server/typescript/packages/metadata (3970 pass), sdk (443 pass), cli (2006 pass, 2017 ran)
  • Full regression: python3 -m pytest tests/unit tests/loader (746 pass)
  • Full regression: mvn -o -pl metadata test (3337 pass, BUILD SUCCESS)
  • Full bun test in server/typescript was killed at the 10-minute background limit with no output, so the whole TS server suite is NOT verified
  • Full Python pytest on tests/integration errors at collection: ModuleNotFoundError pg8000 (environment, not installed)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…ng.collections pluralizer

Renames the unreleased @dbSetName to @collectionName (no alias) and registers it in
every port on object.entity, object.projection and object.report, marked
implementedIn csharp: the generated C# context takes it as the DbSet property name.
A project chooses the pluralizer an object without one takes its name from with
naming.collections in .metaobjects/config.json (standard by default, none, or one
the application registers); every port reads the key and refuses an unknown name.

The shared fixture object-port-scoped-attr-dbsetname becomes object-collection-name,
and object-port-scoped-attr-object-adapter keeps the port-scoped marker covered.
@dmealing
dmealing merged commit 976427a into main Nov 12, 2026
9 checks passed
@dmealing
dmealing deleted the fm/mo-rc19-pr1-loaders branch November 12, 2026 07:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant