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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken.
- **Kotlin** — `codegen-kotlin` (KotlinPoet on JVM): entity + Exposed table + Spring controller + payload + relations + filter allowlist + validator + stored-proc + output-parser generators. `integration-tests-kotlin` runs the persistence-conformance corpus through Exposed against Testcontainers Postgres.

**Cross-port conformance corpora** (every port runs the shared corpus):
- Metamodel: `fixtures/conformance/` (426 fixtures; 35 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
- Metamodel: `fixtures/conformance/` (428 fixtures; 35 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
- Render: `fixtures/render-conformance/`. TS / C# / Java / Kotlin / Python byte-identical.
- Persistence: `fixtures/persistence-conformance/`. **Query** scenarios run on every port (TS / C# / Java / Kotlin / Python), each provisioning its test DB by executing the committed, TS-produced `canonical/schema.postgres.sql` (Postgres only — Derby dropped for the cross-port query corpus, ADR-0015; the FR-044 `report-*` scenarios also run on SQLite, MySQL and D1 where a port has the engine, see [docs/CONFORMANCE.md](docs/CONFORMANCE.md)). The **migration** scenarios are exercised by **TS only** (TS owns schema migrations). **The corpus now gates WRITES, not just reads (SP-H):** an `op: roundtrip` scenario type INSERTs through each port's runtime/ORM write codec (NOT raw SQL), reads the row back, and asserts the wire-normalized value. The `AllTypes` entity (`roundtrip-all-types.yaml`) carries one field of **every** persistable `field.*` subtype — string/int/long/double/float/decimal/boolean/date/time/timestamp(+tz)/currency/enum/uuid/object — plus an **array-of-VO** `field.object @isArray @storage:jsonb` column (`labels`, written as 2-element / empty-`[]` / single-element arrays across the three rows) — so every subtype write+read (incl. the array-of-value-object jsonb codec) round-trips through every port against Testcontainers PG. (`field.byte`/`field.short`/`field.class` were cut as non-functional registration-only stubs — the matrix tracks only genuinely-supported subtypes; see `fixtures/registry-conformance/README.md` → "Per-subtype write-round-trip matrix".)
- API-contract: `fixtures/api-contract-conformance/`. TS / C# / Java / Kotlin / Python all green — each port runs **two lanes**: a hand-rolled reference server AND its **generated** API artifact booted over HTTP (the deployed controller/routes; TS+C# full-stack vs Testcontainers PG, Java/Kotlin/Python generated controller + in-memory repo behind the consumer seam). The generated fan-out found 10 real deployment bugs golden snapshots missed. Three sub-corpora run the **generated lane only, on all five ports** — `write-through/`, `projection/` (F22: a view-only `object.projection` serves GET list + GET by id and answers every write verb with `405 {"error": "method_not_allowed"}`) and `report/` (FR-044: a view-backed `object.report` serves GET list, answers `POST` with that same `405`, and mounts no `/{id}` route). That is deliberate, not a gap: what is under test is whether a port's GENERATOR emits those routes, and a hand-rolled reference server would answer every scenario by construction. The `m2m/` sub-corpus also gates **TPH x M:N together** (base-declared, subtype-declared, abstract-mid-declared, a non-subtype source onto a subtype TARGET, and the cross-subtype source id answering `200 []`) — the two corpora were originally built disjoint (`tph/` had no relationships, `m2m/` no discriminators), which is precisely how that defect class survived.
Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ here.**

### Fixed

- **TypeScript: every `meta` command now loads the providers in `metaobjects.config.ts`.**
`meta export`, `meta deps`, `meta fmt`, `meta types` and `meta upgrade` loaded the core
vocabulary only, so a project's own subtype or attribute failed there while `meta gen` accepted
it. A config that exists but fails to load is now reported as its own error by `meta verify`
(exit 2, in every mode) and `meta prompt-snapshot`, instead of continuing without its providers
and reporting an unknown subtype.
- **Python: `metaobjects fmt` reads the configured providers, and `--provider` adds to them.**
`fmt` ignored `providers:` in `metaobjects.config.yaml` (it also gains `--config`), and
`metaobjects verify --provider` replaced the configured list instead of adding to it.
- **`ERR_COLUMN_NAME_COLLISION` no longer rejects a read-only view whose columns come from `@sql`.**
The 1.2.0 check treated every field of a sourced object as a column MetaObjects creates. For a
`source.rdb @kind: view` with an `@sql` body, the author's SQL defines the columns and the fields
Expand Down Expand Up @@ -75,8 +84,30 @@ here.**
new command. `examples/brownfield/` holds a runnable project for each, and the recipe ships in
the `metaobjects-authoring` skill.

- **A project's own provider class, wired in every port's config, loaded 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:
`defineConfig({ providers })` in TypeScript, `providers: ["module:symbol"]` in
`metaobjects.config.yaml` for Python, the new `csharp.providers: [{ "assembly", "type" }]` list
in `.metaobjects/config.json` for C#, and for Java and Kotlin the new Maven plugin
`<providers><provider>FQCN</provider></providers>` list, `MetaDataTypeProvider` beans composed by
the Spring auto-configured loader, `LoaderOptions.setProviders(...)`, and a `providers`
parameter on the `metadata-ktx` loader. C#, Java and Kotlin had no project-level way to do this
before; `META-INF/services` still does not add a provider to the JVM loader. Two new conformance
fixtures, `provider-custom-vocabulary-success` and
`provider-custom-vocabulary-missing-provider-fails`, hold every port to loading a project
subtype plus attributes with its provider and refusing the same file without it.
`docs/features/extending-with-providers.md` now has a worked example per language, and the
`metaobjects-authoring` skill teaches it, with a reference file per language.

### Changed

- **Python and Java report a missing provider's errors in the same order as the other ports.**
When a file holds both an unknown subtype and unknown attributes, Python used to drop the whole
file at the unknown subtype and never report the attributes; it now reports
`ERR_UNKNOWN_SUBTYPE` and then each `ERR_UNKNOWN_ATTR`. Java reported the attribute errors
before the subtype error; it now reports them after it, as TypeScript and C# do. A load that
failed still fails; only the list of errors differs.
- **A prompt or output template that names a variable not on its payload now raises at render time.**
Behaviour change: a generated render helper used to render `Hi {{nmae}}` as `Hi ` with no error
when the payload declares `name`; it now raises `ERR_VAR_NOT_ON_PAYLOAD (nmae)` (`RenderError` in
Expand Down
6 changes: 6 additions & 0 deletions agent-context/skills/metaobjects-authoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,12 @@ Several variants of one thing sharing **one table**: the base `object.entity` de
`@discriminatorValue`. Codegen emits per-subtype routes with the discriminator injected and
immutable. Supported in all five ports; the worked example is in `references/inheritance-tph.md`.

## Your own vocabulary — a provider class, in every language

A subtype or attribute the core does not ship (a `layout.picker`, `@searchable` on fields) is registered by a **provider class** in the project's own language — never a side file, never a loosened `strict`. Decide the shape first (`references/choosing-a-shape.md`, which also has the register/extend rules). List the provider once in the port's project configuration and every command that loads metadata — gen and its docs generators, verify, fmt — composes it. Where each port lists it, plus a worked example: `references/typescript-providers.md`, `references/python-providers.md`, `references/csharp-providers.md`, `references/java-providers.md`, `references/kotlin-providers.md` (only this stack's are installed).

**Every port whose tools load the metadata needs its own copy**: a TypeScript-only provider leaves the Java, Python or C# side, and its docs build, failing with `ERR_UNKNOWN_SUBTYPE` / `ERR_UNKNOWN_ATTR`.

## Requirements — capability ledger (opt-in)

**This capability exists whether or not the project uses it yet.** `requirement.functional` and `requirement.architectural` are registered metadata types, declared in the metadata directory beside the entities they describe and loaded by the same loader — no side file, no bespoke parser. They record *why* each part of the model exists, so a field with no reason to exist becomes visible as one.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@ or behavior is a **subtype**, and structural variants *within* such a subtype ar
(including any `@kind` value-set) carry `allowedValues` in the gate (ADR-0036).
ADR-0037 is the authority.

**What a provider does.** It names an `id` and the provider ids it depends on, then does one
or both of two things. It **registers** a new `(type, subType)` with its own attributes, which
may be required, and a node class (reuse the type's existing one unless the node has new
behaviour). It **extends** an existing `(type, subType)` with attributes, which must be
optional: a required one is `ERR_EXTEND_REQUIRED_ATTR` (ADR-0050), because removing your
provider must never weaken a type someone else owns. A new type allowed at the document root
also needs `extend` on `metadata.root` to admit it, or it fails with `ERR_CHILD_NOT_ALLOWED`.
An attribute no provider declares is `ERR_UNKNOWN_ATTR` under a strict load, and a subtype
nobody registered is `ERR_UNKNOWN_SUBTYPE`; that refusal is what keeps a typo from loading.
The per-language example and wiring is in the skill's `references/<language>-providers.md`.

**Converge before inventing.** Search the shipped vocabulary first (`meta types
<term>`) and check the roadmap — if core already models (or plans) the concept, model
yours in a fold-in-friendly shape. The type names `api`, `operation`, `surface`, and
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Your own vocabulary in C# — a provider

> Part of the `metaobjects-authoring` skill, "Your own vocabulary". Read it when the project
> needs a subtype or an attribute the core does not ship, and its C# tools load the metadata.

A provider is a public class implementing `IMetaDataTypeProvider`, with a public parameterless
constructor: an `Id`, the provider ids it depends on, and a `RegisterTypes(registry)` body that
calls `Register` (a new subtype) and `Extend` (attributes on an existing one). Put it in its
own class library that references the `MetaObjects` package at the version your `dotnet meta`
tool runs. This one registers `layout.picker` and adds `@searchable` and `@valueLabels` to
`field.string`:

```csharp
// vocabulary/AcmeVocabularyProvider.cs (vocabulary/Acme.Vocabulary.csproj: net8.0 class
// library with <PackageReference Include="MetaObjects" Version="<your tool's version>" />)
using MetaObjects;
using MetaObjects.Meta;
using MetaObjects.Shared;

namespace Acme.Vocabulary;

public sealed class AcmeVocabularyProvider : IMetaDataTypeProvider
{
public string Id => "acme-vocabulary";
public IReadOnlyList<string> Dependencies => ["metaobjects-core-types"];

public void RegisterTypes(TypeRegistry registry)
{
// A new subtype, with its own attribute (an own attribute may be required).
registry.Register(new TypeDefinition(
typeId: new TypeId("layout", "picker"),
description: "A picker over the owning object.",
childRules: [new ChildRule(BaseTypes.TYPE_ATTR, Structural.CHILD_RULE_WILDCARD, Structural.CHILD_RULE_WILDCARD)],
factory: (typeId, name) => new MetaLayout(typeId, name),
attributes: [new AttrSchema("displayField", "string", Required: false)]));

// Attributes on a type another provider owns: always optional.
registry.Extend("field", "string", attributes:
[
new AttrSchema("searchable", "boolean", Required: false),
new AttrSchema("valueLabels", "string", Required: false, IsArray: true),
]);
}
}
```

Wire it once, in `.metaobjects/config.json`. `assembly` is the BUILT dll, relative to the
directory holding `.metaobjects/`; `type` is the provider's full type name:

```json
{
"schema_version": 1,
"csharp": {
"providers": [
{ "assembly": "vocabulary/bin/Debug/net8.0/Acme.Vocabulary.dll", "type": "Acme.Vocabulary.AcmeVocabularyProvider" }
]
}
}
```

Build the vocabulary first (`dotnet build vocabulary`); a missing dll is refused with the path
and that instruction. Then `dotnet meta gen` and `verify --codegen` (through the project's
`codegen/` runner), `dotnet meta verify` (templates, requirements, field lint) and
`dotnet meta fmt` all compose the core providers plus this one. The `codegen/` project may
also reference the vocabulary project directly; the already-loaded assembly is reused.

An application host that loads the model itself composes the registry in code:

```csharp
var registry = Provider.ComposeRegistry([.. CoreTypes.LibraryProviders, new AcmeVocabularyProvider()]);
var load = MetaDataLoader.FromDirectory("metaobjects", registry, strict: true);
```

The metadata uses it like core vocabulary:

<!-- meta-example: external-provider -->
```json
{ "field.string": { "name": "name", "@searchable": true } }
{ "layout.picker": { "name": "contactPicker", "@displayField": "name" } }
```

Without the provider the same file fails `dotnet meta verify` with `ERR_UNKNOWN_SUBTYPE` on
`layout.picker` and `ERR_UNKNOWN_ATTR` on `@searchable`. If another port's tools load this
metadata too, that port needs its own copy of the provider (same id, same vocabulary). Each port's `verify` loads strict, so a clean run there proves that port's copy is complete.
Full contract: `docs/features/extending-with-providers.md`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Your own vocabulary in Java — a provider

> Part of the `metaobjects-authoring` skill, "Your own vocabulary". Read it when the project
> needs a subtype or an attribute the core does not ship, and its Java tools load the metadata.

A provider is a public class implementing `com.metaobjects.registry.MetaDataTypeProvider`, with
a public no-arg constructor: a provider id, the provider ids it depends on (the JVM ids:
`field-types` registers the field types, `layout-types` the layouts), and a `registerTypes`
body that calls `registerType` (a new subtype, backed by its own node class) and `extendType`
(attributes on an existing one). This one registers `layout.picker` and adds `@searchable` and
`@valueLabels` to `field.string`:

```java
package com.acme.vocab;

import com.metaobjects.attr.BooleanAttribute;
import com.metaobjects.attr.StringAttribute;
import com.metaobjects.field.StringField;
import com.metaobjects.layout.MetaLayout;
import com.metaobjects.registry.MetaDataRegistry;
import com.metaobjects.registry.MetaDataTypeProvider;

public final class AcmeVocabulary implements MetaDataTypeProvider {
/** The node class behind layout.picker. */
public static final class PickerLayout extends MetaLayout {
public PickerLayout(String name) { super("picker", name); }
}

@Override public String getProviderId() { return "acme-vocabulary"; }
@Override public String[] getDependencies() { return new String[] {"field-types", "layout-types"}; }

@Override public void registerTypes(MetaDataRegistry registry) {
// A new subtype, with its own attribute (an own attribute may be required).
registry.registerType(PickerLayout.class, def -> {
def.type("layout").subType("picker").inheritsFrom("layout", "base");
def.optionalAttributeWithConstraints("displayField").ofType(StringAttribute.SUBTYPE_STRING).asSingle();
});
// Attributes on a type another provider owns: always optional.
registry.extendType(StringField.class, def -> {
def.optionalAttributeWithConstraints("searchable").ofType(BooleanAttribute.SUBTYPE_BOOLEAN).asSingle();
def.optionalAttributeWithConstraints("valueLabels").ofType(StringAttribute.SUBTYPE_STRING).asArray();
});
}
}
```

Wire it once, in the `metaobjects-maven-plugin` configuration. The class must be on the
plugin's classpath: put the provider in its own module and add that module as a project
dependency or under the plugin's `<dependencies>`. A provider in the same module is not
compiled yet when `generate` runs at `generate-sources`.

```xml
<plugin>
<groupId>com.metaobjects</groupId>
<artifactId>metaobjects-maven-plugin</artifactId>
<configuration>
<providers>
<provider>com.acme.vocab.AcmeVocabulary</provider>
</providers>
</configuration>
<dependencies>
<dependency><groupId>com.acme</groupId><artifactId>acme-vocabulary</artifactId><version>1.0</version></dependency>
</dependencies>
</plugin>
```

`metaobjects:generate` (with its docs generators), `metaobjects:verify`, `metaobjects:fmt` and
`metaobjects:editor` all compose the metamodel providers plus these. Listing a class in
`META-INF/services` does NOT add it to the loader: the strict loader composes a fixed
metamodel set plus the providers you name, so a jar on the classpath cannot change the
vocabulary by accident.

In a Spring Boot app, declare the provider as a bean and the auto-configured loader composes
it: `@Bean MetaDataTypeProvider acmeVocabulary() { return new AcmeVocabulary(); }`. Code that
loads the model itself passes it on the options:

```java
MetaDataLoader.fromUris("app", uris,
LoaderOptions.create(false, false, true).setProviders(List.of(new AcmeVocabulary())));
```

The metadata uses it like core vocabulary:

<!-- meta-example: external-provider -->
```json
{ "field.string": { "name": "name", "@searchable": true } }
{ "layout.picker": { "name": "contactPicker", "@displayField": "name" } }
```

Without the provider the same file fails `metaobjects:verify` with `ERR_UNKNOWN_SUBTYPE` on
`layout.picker` and `ERR_UNKNOWN_ATTR` on `@searchable`. If another port's tools load this
metadata too, that port needs its own copy of the provider (the same vocabulary). Each port's `verify` loads strict, so a clean run there proves that port's copy is complete.
Full contract: `docs/features/extending-with-providers.md`.
Loading
Loading