Skip to content

feat: load project provider classes in every port - #529

Merged
dmealing merged 5 commits into
mainfrom
fm/mo-declarative-vocabulary
Nov 4, 2026
Merged

dmealing merged 5 commits into
mainfrom
fm/mo-declarative-vocabulary

Conversation

@dmealing

@dmealing dmealing commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • Every port loads project-defined provider classes named in its own config: TypeScript and Python through the config loader, C# through the new csharp.providers key ({assembly, type}) in .metaobjects/config.json, and Java/Kotlin through the Maven plugin <providers> list (also -Dmeta.providers), Spring MetaDataTypeProvider beans, LoaderOptions.setProviders, and a providers parameter on the metadata-ktx loaders. Metadata-loading commands in TypeScript (export, deps, fmt, types, upgrade, verify, prompt-snapshot) and Python (fmt, which gains --config; verify --provider now merges with the configured list) apply them. There is no ambient discovery: a class listed in META-INF/services is not loaded.
  • Adds shared conformance fixtures provider-custom-vocabulary-success and provider-custom-vocabulary-missing-provider-fails. The Python loader now keeps a file whose only parse errors are unknown subtype or type, so the unknown-attr pass still runs. The Java loader defers ERR_UNKNOWN_ATTR until after ERR_UNKNOWN_SUBTYPE, matching TypeScript and C#. A load that failed still fails; only the error list order differs.
  • Updates the agent-context skills (metaobjects-authoring with per-language provider references, metaobjects-codegen), regenerates their conformance fixtures, rewrites docs/features/extending-with-providers.md with a worked example per language, and adds a CHANGELOG entry under Unreleased. Fixture count moves from 423 to 425.
Validation

Risk Assessment

⚠️ Medium: The fix-round edits look correct and I found no new defects in them, but the overall change is a large cross-port loader and CLI change, and I only re-read the fix-round files in this pass.

Testing

Drove two TS scenarios live through the built-from-source meta CLI in disposable projects: the forge-provider duplicate fix and a downstream-style custom vocabulary (pass with provider, refuse without). Drove the C# bad-assembly refusal live through dotnet meta verify. Python, Java/Kotlin and C# provider-loading checks ran only as unit, conformance or in-process tests, so they are recorded as untested for live behavior. Did not run the full repository regression suite (full ci-local, full Java reactor, full dotnet solution); only the baseline TS fast/unit lanes and the targeted suites above ran.

  • Live validation: ✅ go - 4 of 10 scenarios driven live against the product
Scenario Result Live Evidence
TS: config providers include forge types; meta export does not throw duplicate provider ✅ pass live meta export in /tmp disposable project, exit 0
TS: project custom provider adds layout.picker and field attrs; meta export loads them ✅ pass live meta export exit 0, canonical JSON contains layout.picker and @searchable/@valueLabels
TS: same metadata without the provider is refused ✅ pass live meta export exit 1, Unknown type "layout.picker" — not registered
C#: bad csharp.providers assembly refused with named config error, no crash ✅ pass live dotnet run MetaObjects.Cli verify exit 2, ERR_BAD_ATTR_VALUE naming csharp.providers
C#: provider assembly loads and registers vocabulary (CLI provider tests) ⏸️ untested no Prior payload ran this only through the CLI test harness, not a live CLI run, so no live result is established.
Python: fmt honors configured providers and --config; verify --provider merges ⏸️ untested no Prior payload called the CLI main() in-process, not as a separate live process, so no live result is established.
Python: cross-port conformance corpus incl. missing-provider fixture ⏸️ untested no Prior payload ran the conformance runner, not a live product run, so no live result is established.
Java: provider loading and conformance (strict loader, Maven plugin, Spring, Kotlin facade) ⏸️ untested no Prior payload ran unit and integration tests only, not a live CLI or Maven goal run, so no live result is established.
Kotlin: ProviderVocabularyTest via metadata-ktx ⏸️ untested no Prior payload ran test-driven checks only, so no live result is established.
Full repository regression suite ⏸️ untested no Full ci-local, full Java reactor and full dotnet solution were not run; only the harness baseline (ts-fast, ts-unit) and targeted suites ran.

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ⚠️ server/typescript/packages/cli/src/commands/export.ts:47 - meta export now composes [...defaultLoadMemoryProviders, forgeTypesProvider, ...providers], where providers is the config's list as written. The documented way to opt into forge vocabulary is providers: [forgeTypesProvider] (sdk memory.ts:133-134 and docs/features/extending-with-providers.md:483). A project that does this now has the forge provider twice, so composeRegistry throws ERR_PROVIDER_DUPLICATE_ID. The throw happens outside the try/catch at export.ts:47, so meta export fails for the projects that hold descriptive records. Before this change export ignored config providers and worked. Fix: drop config providers whose id is already in the base set (or add forge only when the config does not list it), and add a test that uses a config with providers: [forgeTypesProvider].
  • ⚠️ server/csharp/MetaObjects/Config/ProviderLoader.cs:62 - ProviderLoader.LoadAssembly calls AssemblyName.GetAssemblyName and AssemblyLoadContext.LoadFromAssemblyPath without handling BadImageFormatException or FileLoadException. Example: csharp.providers[0].assembly points at a native dll, a .pdb, or a dll built for an incompatible runtime. The exception then escapes MetadataLocation.Resolve. ResolveMetadataDirOrExit (Program.cs:323-337) and CodegenCli.ResolveOrReport catch only MetaModelException/MetadataLocationException, so dotnet meta gen/verify/fmt crash with a stack trace. The user does not get the Refuse(...) message that names csharp.providers, as the other bad-entry cases do. Fix: wrap the GetAssemblyName/LoadFromAssemblyPath/GetType calls and convert BadImageFormatException, FileLoadException and ReflectionTypeLoadException to Refuse(spec, ...).
  • ℹ️ server/typescript/packages/cli/src/commands/verify.ts:331 - Simplification: the new early return at verify.ts:315-321 (forgeConfig === undefined &amp;&amp; existsSync(config) returns 2) makes the old else if (existsSync(join(genConfigDir, &#34;metaobjects.config.ts&#34;))) branch at verify.ts:331-336 unreachable. In that branch forgeConfig is undefined, so the config file cannot exist there. Remove the dead branch and leave else say(...).
  • ℹ️ server/python/tests/codegen/test_cli_config_providers.py:105 - The new Python fmt tests use weak oracles. test_fmt_rewrites_a_file_using_the_config_vocabulary asserts &#34;@searchable&#34; in text or &#34;searchable&#34; in text: the second clause makes the first redundant, and a formatter that drops the @ sigil still passes. test_fmt_honors_config_providers accepts rc in (0, 1). test_fmt_without_config_providers_still_refuses_the_vocabulary accepts any non-zero exit, including a crash. test_fmt_merges_config_and_flag_providers uses metadata that does not need the flag provider, so it does not prove the merge. Stronger oracles: compare the formatted file to the shared fixture fixtures/conformance/provider-custom-vocabulary-success/expected.json, assert the specific ERR_UNKNOWN_SUBTYPE in stderr for the no-config case, and make the merge test's metadata need both providers (the verify test already does this with validator.geocheck).
  • ℹ️ server/typescript/packages/cli/src/commands/upgrade.ts:136 - meta upgrade &lt;projectRoot&gt; looks up providers with loadProjectProviders(cwd, projectRoot), so the nearest-ancestor walk starts from cwd and not from the project named on the command line. Running meta upgrade sub/proj from a directory that has its own metaobjects.config.ts uses that other project's providers. fmt-engine starts its walk at opts.projectRoot, which is consistent with the argument. Use loadProjectProviders(projectRoot, projectRoot) here. The impact is small: upgrade is best-effort, and the providers only add abstract-anchor types.
  • ℹ️ server/csharp/MetaObjects/Config/MetadataLocation.cs:85 - Inline fix: the new LoadProviders method had been inserted between ProjectRootFor's &lt;summary&gt; and ProjectRootFor, so the summary was attached to the wrong member. I gave LoadProviders its own one-line summary and put the original summary back on ProjectRootFor. No code changed.
  • ℹ️ server/java/metadata-ktx/src/main/kotlin/com/metaobjects/metadata/ktx/Loader.kt:28 - Inline fix: the loadDirectory KDoc said providers are composed "sealed, strict". MetaDataLoader.fromDirectory builds its loader with LoaderOptions.create(false, false, false), which is lax. I changed the comment to say the registry is sealed and the directory load stays lax, as it is without providers.

🔧 Fix applied.
2 infos still open:

  • ℹ️ server/csharp/MetaObjects/Config/MetadataLocation.cs:85 - Inline fix: the new LoadProviders method had been inserted between ProjectRootFor's &lt;summary&gt; and ProjectRootFor, so the summary was attached to the wrong member. I gave LoadProviders its own one-line summary and put the original summary back on ProjectRootFor. No code changed.
  • ℹ️ server/java/metadata-ktx/src/main/kotlin/com/metaobjects/metadata/ktx/Loader.kt:28 - Inline fix: the loadDirectory KDoc said providers are composed "sealed, strict". MetaDataLoader.fromDirectory builds its loader with LoaderOptions.create(false, false, false), which is lax. I changed the comment to say the registry is sealed and the directory load stays lax, as it is without providers.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 4 of 10 scenarios driven live against the product
Scenario Result Live Evidence
TS: config providers include forge types; meta export does not throw duplicate provider ✅ pass live meta export in /tmp disposable project, exit 0
TS: project custom provider adds layout.picker and field attrs; meta export loads them ✅ pass live meta export exit 0, canonical JSON contains layout.picker and @searchable/@valueLabels
TS: same metadata without the provider is refused ✅ pass live meta export exit 1, Unknown type "layout.picker" — not registered
C#: bad csharp.providers assembly refused with named config error, no crash ✅ pass live dotnet run MetaObjects.Cli verify exit 2, ERR_BAD_ATTR_VALUE naming csharp.providers
C#: provider assembly loads and registers vocabulary (CLI provider tests) ⏸️ untested no Prior payload ran this only through the CLI test harness, not a live CLI run, so no live result is established.
Python: fmt honors configured providers and --config; verify --provider merges ⏸️ untested no Prior payload called the CLI main() in-process, not as a separate live process, so no live result is established.
Python: cross-port conformance corpus incl. missing-provider fixture ⏸️ untested no Prior payload ran the conformance runner, not a live product run, so no live result is established.
Java: provider loading and conformance (strict loader, Maven plugin, Spring, Kotlin facade) ⏸️ untested no Prior payload ran unit and integration tests only, not a live CLI or Maven goal run, so no live result is established.
Kotlin: ProviderVocabularyTest via metadata-ktx ⏸️ untested no Prior payload ran test-driven checks only, so no live result is established.
Full repository regression suite ⏸️ untested no Full ci-local, full Java reactor and full dotnet solution were not run; only the harness baseline (ts-fast, ts-unit) and targeted suites ran.
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • Live TS: meta export in a disposable project with providers: [forgeTypesProvider] in metaobjects.config.ts → exit 0, no ERR_PROVIDER_DUPLICATE_ID (export duplicate fix)
  • Live TS: meta export with a project provider registering layout.picker + field attrs @searchable/@valueLabels → exit 0, attrs round-trip into canonical JSON
  • Live TS: same metadata without the provider in config → exit 1 Unknown type &#34;layout.picker&#34; — not registered (refusal path)
  • Live C#: dotnet run --project server/csharp/MetaObjects.Cli -- verify with csharp.providers pointing at a non-assembly file → exit 2, message names csharp.providers and BadImageFormatException (no stack trace)
  • dotnet test MetaObjects.Cli.Tests --filter CsharpProvidersTests → 19 passed (test harness, not live)
  • python3 -m pytest tests/codegen/test_cli_config_providers.py → 6 passed (in-process, not live)
  • python3 -m pytest tests/conformance → 1417 passed (conformance runner, not live)
  • mvn -o test -pl metadata -am -Dtest=LoaderProvidersTest,ConformanceTest,YamlTwinConformanceTest → BUILD SUCCESS (not live)
  • mvn -o test -pl metadata-ktx,maven-plugin,core-spring -am -Dtest=ProviderVocabularyTest,MojoProvidersTest,MetaDataLoaderProvidersTest → 3+7+2 passed, BUILD SUCCESS (not live)
  • bun test packages/cli/test/config-providers-everywhere.test.ts packages/sdk/test/config.test.ts (server/typescript) → 45 pass, 0 fail (not live)
  • Baseline scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains (ran by harness, green)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

no-mistakes gate: 1 run. Review raised 5 fixable findings (a meta export duplicate-provider failure, an unhandled bad C# provider assembly, a meta upgrade config-walk start, a dead meta verify branch, weak Python fmt test oracles), all fixed in that run.

…by every command

A project that declares its own vocabulary (a new subtype, or attributes on a
core type) writes a provider class in each language whose tools load the
metadata and names it once in that port's config. Every metadata-loading
entry point now loads it.

- TypeScript: export, deps, fmt, types and upgrade now load the providers
  in metaobjects.config.ts; verify and prompt-snapshot report a config that
  fails to load instead of continuing without its providers.
- Python: fmt reads configured providers (and gains --config); verify's
  --provider adds to the configured list instead of replacing it. The loader
  keeps a file whose only parse errors are unknown subtypes, so its unknown
  attributes are reported too.
- C#: new csharp.providers [{assembly, type}] list in .metaobjects/config.json,
  loaded by gen, verify and fmt (ProviderLoader).
- Java/Kotlin: Maven plugin <providers> list for generate/verify/fmt/editor,
  Spring MetaDataTypeProvider beans composed into the auto-configured loader,
  LoaderOptions.setProviders, and a providers parameter on metadata-ktx.
  ERR_UNKNOWN_ATTR is reported after ERR_UNKNOWN_SUBTYPE, as in the other ports.
- Conformance: provider-custom-vocabulary-success and
  provider-custom-vocabulary-missing-provider-fails, run by every port.
- Skills: metaobjects-authoring teaches it with a worked reference per
  language; metaobjects-codegen notes how a generator reads the attribute.
- Docs: extending-with-providers.md has a worked example per language and
  drops the false SPI auto-discovery claim; port pages and recipe updated.
…our-codegen.md provider table (Java SPI claim, C#, TypeScript command list), server/java/README.md (ServiceLoader wording), and the recipe's per-language section now points to extending-with-providers.md instead of duplicating wiring facts. No code or tests touched; nothing unresolved.
Brings in #528 (read-only @SQL view column-collision exemption) and #530
(cross-field validators in generated create models). Conflicts resolved by
keeping both sides: the conformance fixture count is 428 (main's 426 plus this
branch's two provider-custom-vocabulary fixtures) in AGENTS.md,
docs/CONFORMANCE.md and the site payload, and CHANGELOG keeps both entries.

The metaobjects-authoring SKILL.md is held under its 500-line cap: the
provider section's per-language wiring table moves into the per-language
provider references it already pointed at, with a one-line pointer left in
place, and the agent-context conformance fixtures are regenerated.
@dmealing
dmealing merged commit 61b2fa8 into main Nov 4, 2026
9 checks passed
@dmealing
dmealing deleted the fm/mo-declarative-vocabulary branch November 4, 2026 08:39
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