From 0323a1e7a583a18b0e486bc8818e167a4cfc21fa Mon Sep 17 00:00:00 2001
From: Doug Mealing
Date: Wed, 11 Nov 2026 12:27:45 -0500
Subject: [PATCH 1/4] feat(registry): vocabulary only some ports implement is
cross-port, marked implementedIn
The 13 generic view.* controls (with view.textarea @rows and the view.image
attrs) and object.* @object used to be carved out of the registry manifest as
presentation-only or native-binding vocabulary. They are now in the shared
contract in every port, marked rather than removed:
- implementedIn names the ports that act on an entry so far (["typescript"]
for the view controls, ["java"] for @object). Every other port registers it,
loads it strictly and ignores it; an unregistered attr on it still fails.
- ports names the ports a language-specific attr belongs to: @dbSetName
["csharp"] and @objectAdapter ["java", "kotlin"].
All five ports register the metaobjects-ui-web provider and @object /
@objectAdapter, emit implementedIn in their manifests, and byte-match
expected-registry.json. Java and Python gain the ui-web provider and the
spec-reader support for implementedIn; Java's extends-value mapping accepts
a double-valued attr. Three conformance fixtures gate the loading behaviour
(view-kind1-controls, object-kind1-jvm-binding-attrs,
error-kind1-view-unregistered-attr).
meta types marks these entries in its text output and carries implementedIn
and ports in --format json. The json tsOnly field is kept, derived from
implementedIn, and deprecated; it is removed in 1.4.
The metamodel docs, site reference pages and agent-context fixtures are
regenerated, and docs/CONFORMANCE.md and the fixture counts are updated.
---
AGENTS.md | 2 +-
CHANGELOG.md | 33 +-
agent-context/templates/always-on.md.mustache | 2 +-
docs/CONFORMANCE.md | 30 +-
docs/features/image-upload.md | 12 +-
.../base-subtypes-are-not-authorable.md | 5 +-
examples/showcase/site-payload.json | 2 +-
.../expected/.metaobjects/AGENTS.md | 2 +-
.../expected/.metaobjects/CLAUDE.md | 2 +-
.../expected/.metaobjects/AGENTS.md | 2 +-
.../expected/.metaobjects/CLAUDE.md | 2 +-
.../python/expected/.metaobjects/AGENTS.md | 2 +-
.../python/expected/.metaobjects/CLAUDE.md | 2 +-
.../expected/.metaobjects/AGENTS.md | 2 +-
.../expected/.metaobjects/CLAUDE.md | 2 +-
.../expected/.metaobjects/AGENTS.md | 2 +-
.../expected/.metaobjects/CLAUDE.md | 2 +-
.../expected-errors.json | 15 +
.../input/meta.catalog.json | 40 +++
.../expected.json | 110 +++++++
.../input/meta.catalog.json | 108 +++++++
.../view-kind1-controls/expected.json | 193 ++++++++++++
.../input/meta.catalog.json | 193 ++++++++++++
fixtures/metamodel-docs/expected/INDEX.md | 13 +
fixtures/metamodel-docs/expected/providers.md | 5 +-
.../metamodel-docs/expected/types/object.md | 13 +-
.../metamodel-docs/expected/types/view.md | 216 ++++++++++++++
fixtures/registry-conformance/README.md | 60 ++--
.../registry-conformance/coverage-report.json | 4 +-
.../expected-registry.json | 282 +++++++++++++++++-
scripts/check-requirements-vocabulary.ts | 6 +-
scripts/lib/requirement-vocabulary.ts | 5 +-
.../GeneratorGoldenTests.cs | 7 +-
.../ConformanceAdapter.cs | 2 +
.../FixtureDiscovery.cs | 2 +
.../FullCoreRegistry.cs | 1 +
.../KindOneVocabularyTests.cs | 70 +++++
.../RegistryManifestConformanceTests.cs | 14 +-
.../Core/Object/ObjectConstants.cs | 12 +
.../MetaObjects/Core/Object/ObjectSchema.cs | 15 +
server/csharp/MetaObjects/CoreTypes.cs | 3 +
.../Presentation/Ui/UiWebMetaDataProvider.cs | 44 +++
.../Presentation/View/ViewConstants.cs | 9 +-
server/csharp/MetaObjects/Registry.cs | 17 +-
.../Registry/Spec/SpecMetamodelReader.cs | 20 +-
server/csharp/MetaObjects/RegistryManifest.cs | 68 +++--
.../MetaObjects/SpecMetamodel/object.json | 4 +-
.../MetaObjects/SpecMetamodel/view.json | 14 +-
.../ui/UiWebTypesMetaDataProvider.java | 62 ++++
.../registry/MetaDataRegistry.java | 1 +
.../registry/RegistryManifest.java | 75 +++--
.../registry/spec/SpecMetamodelReader.java | 14 +-
.../metaobjects/view/PresentationView.java | 8 +-
....metaobjects.registry.MetaDataTypeProvider | 1 +
.../conformance/ConformanceTest.java | 3 +
.../RegistryManifestConformanceTest.java | 8 +-
server/python/src/metaobjects/core_types.py | 10 +
.../meta/core/object/object_constants.py | 5 +
.../meta/presentation/ui/ui_web_provider.py | 33 ++
.../meta/presentation/view/view_constants.py | 9 +-
server/python/src/metaobjects/registry.py | 5 +
.../src/metaobjects/registry_manifest.py | 29 +-
.../metaobjects/spec_metamodel/__init__.py | 16 +-
.../metaobjects/spec_metamodel/object.json | 4 +-
.../src/metaobjects/spec_metamodel/view.json | 14 +-
.../tests/conformance/conformance_adapter.py | 3 +
.../tests/conformance/fixture_discovery.py | 2 +
.../conformance/test_registry_conformance.py | 8 +-
.../packages/cli/src/commands/types.ts | 66 +++-
.../packages/cli/test/types-command.test.ts | 37 ++-
.../packages/conformance/src/fixture.ts | 2 +
.../core/object/object-definition.embedded.ts | 23 ++
.../metadata/src/metamodel-docs/render.ts | 9 +-
.../presentation/ui-web/ui-web-provider.ts | 17 +-
.../view/view-definition.embedded.ts | 39 +++
.../packages/metadata/src/provider-data.ts | 10 +
.../src/registry-manifest-exclusions.ts | 40 +--
.../metadata/src/registry-manifest.ts | 23 +-
.../packages/metadata/src/registry.ts | 12 +
.../metadata/src/vocabulary-catalog.ts | 32 +-
.../metadata/test/conformance/adapter.ts | 2 +
.../metadata/test/metamodel-docs.test.ts | 37 +--
.../object-definition-completeness.test.ts | 21 +-
.../metadata/test/port-scoped-attr.test.ts | 33 ++
.../test/registry-conformance.test.ts | 41 ++-
site-reference/index.html | 65 ++++
site-reference/providers.html | 5 +-
site-reference/types/object.html | 104 ++++++-
site-reference/types/view.html | 182 +++++++++++
spec/metamodel/object.json | 4 +-
spec/metamodel/view.json | 14 +-
91 files changed, 2470 insertions(+), 329 deletions(-)
create mode 100644 fixtures/conformance/error-kind1-view-unregistered-attr/expected-errors.json
create mode 100644 fixtures/conformance/error-kind1-view-unregistered-attr/input/meta.catalog.json
create mode 100644 fixtures/conformance/object-kind1-jvm-binding-attrs/expected.json
create mode 100644 fixtures/conformance/object-kind1-jvm-binding-attrs/input/meta.catalog.json
create mode 100644 fixtures/conformance/view-kind1-controls/expected.json
create mode 100644 fixtures/conformance/view-kind1-controls/input/meta.catalog.json
create mode 100644 server/csharp/MetaObjects.Conformance.Tests/KindOneVocabularyTests.cs
create mode 100644 server/csharp/MetaObjects/Presentation/Ui/UiWebMetaDataProvider.cs
create mode 100644 server/java/metadata/src/main/java/com/metaobjects/presentation/ui/UiWebTypesMetaDataProvider.java
create mode 100644 server/python/src/metaobjects/meta/presentation/ui/ui_web_provider.py
diff --git a/AGENTS.md b/AGENTS.md
index 5d0c29e98b..bde4ffec93 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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/` (451 fixtures; 38 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/` (454 fixtures; 38 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.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 473b230f94..b40d03c06f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,13 +13,32 @@ here.**
**`metamodelVersion` moves to `1.3`.** This release adds vocabulary (`field.duration`,
`validator.exactlyOne`, `@constraintName`, `@condition`, `@maxLength` on `field.enum`,
`@sequenceStart`, `@sequenceName`, `@flattenedColumnPrefix`, the `pascal` column rule, and
-`@dbSetName`, a C#-only attr) and nothing that loaded before stops loading. A registry attr entry
-gains `ports`, carried into the registry manifest: the ports that act on a language-specific attr
-(`@dbSetName` names `["csharp"]`). A port that reports `1.2` does not know that vocabulary; upgrade
-every port together.
+`@dbSetName`, a C#-only attr) and nothing that loaded before stops loading. The registry gains two
+fields on a type or attr entry, carried into the registry manifest: `implementedIn` (cross-port
+vocabulary only the named ports implement so far, e.g. the `view.*` controls, `["typescript"]`) and
+`ports` (vocabulary only the named ports ever act on, e.g. `@dbSetName`, `["csharp"]`, and
+`@objectAdapter`, `["java", "kotlin"]`: the JVM runtime both share acts on it). The manifest's row
+set grows by the `implementedIn` rows that used to be carved out. A port that reports `1.2` does not
+know that vocabulary; upgrade every port together.
### Added
+- **Cross-port vocabulary that only some ports implement so far (`implementedIn`).** A registry
+ type or attr may name the ports that implement it so far (`"implementedIn": ["typescript"]` on
+ its `spec/metamodel` entry, carried into the registry manifest). It is still cross-port
+ vocabulary: every port registers it and loads it strictly, and a port that has not implemented
+ it ignores it. The 13 generic `view.*` controls, `view.textarea @rows` and the five `view.image`
+ attrs are `["typescript"]`; `object.* @object` is `["java"]`. They used to be carved out of the
+ manifest (`PRESENTATION_ONLY`, `NATIVE_BINDING`); both carve-outs are gone, and the registry
+ canonical grows by those rows. `meta types` marks them `[ts; pending elsewhere]` / `[java;
+ pending elsewhere]` (it used to say `[ts-only]`, "the cross-port contract does not carry it"; its
+ JSON change is under Changed), and the metamodel docs now cover the view controls.
+ `@objectAdapter` joins `@dbSetName` as port-scoped (`"ports": ["java", "kotlin"]`). C#, Java,
+ Kotlin and Python now register the `metaobjects-ui-web` attrs (`view.textarea @rows` and the
+ `view.image` attrs), and C# and Python register `@object` / `@objectAdapter`, so a model that
+ uses any of them loads strictly in every port. Gated by the conformance fixtures
+ `view-kind1-controls`, `object-kind1-jvm-binding-attrs` and
+ `error-kind1-view-unregistered-attr`, and by the registry canonical.
- **Port-scoped vocabulary, and `@dbSetName`.** A registry attr may name the ports
that act on it (`"ports": ["csharp"]` on its `spec/metamodel` entry, carried into the registry
manifest). Every port registers and type-checks it, so a model that carries it loads under every
@@ -510,6 +529,12 @@ every port together.
Python, `Verify` in C#, the `verify` field of `RenderRequest` in Java and Kotlin). If a template
already named a missing variable, regenerate and fix the name; the generated helper text changes
(TypeScript `prompts.ts` and Python helpers gain a `verify` argument).
+- **Deprecated, CLI: `tsOnly` in `meta types --format json`; read `implementedIn`.** Each row
+ and attr now names the ports that implement it so far (`"implementedIn": ["typescript"]`), and a
+ port-scoped attr carries `ports`. `tsOnly` is still emitted through 1.3.x, derived from
+ `implementedIn` (`true` exactly when it is `["typescript"]`), and is removed in 1.4. The text
+ output's `[ts-only]` marker is now `[ts; pending elsewhere]`, and a port-scoped attr shows
+ `[-only]` (`[java+kotlin]` for `@objectAdapter`, which names two).
- **C#, upgrade note: the next EF migration scaffolds your indexes and comments.** The generated
context now declares every `@unique`, `identity.secondary` and `index.lookup` as an EF index
diff --git a/agent-context/templates/always-on.md.mustache b/agent-context/templates/always-on.md.mustache
index 73d371ac6c..9eef331f35 100644
--- a/agent-context/templates/always-on.md.mustache
+++ b/agent-context/templates/always-on.md.mustache
@@ -72,7 +72,7 @@ from.
- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope.
## Authoring rules you must not violate
-- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts-only]` are TypeScript-only vocabulary the cross-port contract does not carry. `[-only]` (such as `[csharp-only]`) marks vocabulary only that port ever acts on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
+- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
- Nodes are fused-key maps: `{".": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
diff --git a/docs/CONFORMANCE.md b/docs/CONFORMANCE.md
index 9b4817e5ef..84f5b60641 100644
--- a/docs/CONFORMANCE.md
+++ b/docs/CONFORMANCE.md
@@ -26,7 +26,7 @@ regenerate with `ls -d fixtures//*/ | wc -l` for directory-shaped corpor
| Corpus | Fixtures | TS | Java | Kotlin | C# | Python |
|---|---|---|---|---|---|---|
-| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 451 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
+| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 454 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
| [`fixtures/yaml-conformance/`](../fixtures/yaml-conformance/) | 16 | 16 / 16 | 15 / 16 (1 ledgered: `yaml-quoted-leading-zero` — Java pipeline strips quotes off `"007"`) | inherits via Java | 15 / 16 (1 ledgered: `error-yaml-coerced-hex-in-string` — YamlDotNet doesn't coerce `0xFF`) | 16 / 16 |
| [`fixtures/verify-conformance/`](../fixtures/verify-conformance/) | 45 | ✓ | ✓ | inherits via Java | ✓ | ✓ |
| [`fixtures/verify-strict-conformance/`](../fixtures/verify-strict-conformance/) | 1 | ✓ | — | — | — | ✓ |
@@ -132,6 +132,30 @@ are tracked in [#544](https://github.com/metaobjectsdev/metaobjects/issues/544).
- *TODO, Python:* the vocabulary loads; the runtime has no DDL path. If it gains one, the same
semantic and pin apply.
+**Cross-port vocabulary implemented in some ports so far (`implementedIn`).** Each entry
+below is in the shared registry and the registry manifest, marked `implementedIn`. A port that
+has not implemented it must still register it and load it strictly (and then ignore it); an
+unregistered attr on it still fails. Gated by the conformance fixtures `view-kind1-controls`,
+`object-kind1-jvm-binding-attrs` and `error-kind1-view-unregistered-attr`, and by the registry
+canonical (`fixtures/registry-conformance/expected-registry.json`).
+
+- *Implemented in TypeScript only* (`implementedIn: ["typescript"]`). The TS web client and TS
+ form codegen act on them; no other port does yet.
+ - `view.checkbox`, `view.date`, `view.dropdown`, `view.hidden`, `view.hotlink`, `view.month`,
+ `view.number`, `view.password`, `view.radio`, `view.text`, `view.web` (types, no attrs)
+ - `view.textarea`, and its `@rows`
+ - `view.image`, and its `@accept`, `@aspectRatio`, `@maxBytes`, `@maxEdge`, `@store`
+- *Implemented in Java only* (`implementedIn: ["java"]`): `object.* @object` (ADR-0001), the
+ native class an object binds to. Java's runtime acts on it; Java's own behaviour is unchanged.
+- *Loads strictly in every port.* Each port registers the `metaobjects-ui-web` provider
+ (`view.textarea @rows`, the `view.image` attrs) and `@object` / `@objectAdapter` on
+ `object.*`, loads all three fixtures strictly, and emits `implementedIn` in its manifest, which
+ byte-matches the canonical in all five ports (`KindOneVocabularyTests` in C#;
+ `registry-conformance.test.ts` in TypeScript; `RegistryManifestConformanceTest` in Java and
+ Kotlin; `test_registry_conformance.py` in Python).
+- *Port-scoped, for comparison* (`ports`, language-specific by nature, never "pending"):
+ `@dbSetName` (`["csharp"]`) and `@objectAdapter` (`["java", "kotlin"]`).
+
**Native index vocabulary and `@description` in the ORM models.** `meta migrate` creates
every index (`identity.secondary`, `index.lookup`, a field's `@unique`, with `@where`, `@using`,
`@orders`, `@expr` and the resolved name) and writes `@description` as `COMMENT ON`. What each
@@ -326,7 +350,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker.
## Fixture-to-doc mapping
-### `fixtures/conformance/` — metamodel loader + canonical serializer (451)
+### `fixtures/conformance/` — metamodel loader + canonical serializer (454)
| Fixture prefix | Feature doc |
|---|---|
@@ -687,7 +711,7 @@ JSON and verify payload feeds it unchanged.
## Orphaned fixtures (tested but not yet documented)
-The fixtures in the nine corpora mapped above (metamodel 451 + yaml 16 + verify 45
+The fixtures in the nine corpora mapped above (metamodel 454 + yaml 16 + verify 45
+ render 18 + persistence 62 + api-contract 123 + source-resolution 25 + scope 10 +
dependency 47) each map to a feature doc, and so do the two requirement corpora, `shared-model-conformance`, the
`cube-model` corpus and `explorer-conformance`, whose case lists live in their own READMEs. None are orphaned today. The remaining
diff --git a/docs/features/image-upload.md b/docs/features/image-upload.md
index 9d33ead5e0..14a32fd3e5 100644
--- a/docs/features/image-upload.md
+++ b/docs/features/image-upload.md
@@ -9,12 +9,12 @@ generated Zod schema, and the REST payload all carry a plain string.
This is a **TS-web-only** feature — the upload widget, crop UI and adapter
contract exist only in the TypeScript web client, and no other port ships one.
-The `view.image` subtype itself LOADS in every port (all the generic `view.*`
-controls register for loading everywhere; they stay manifest-excluded as
-PRESENTATION_ONLY — `fixtures/registry-conformance/README.md`), but its five
-attrs (`@aspectRatio` … `@maxBytes`) are registered only by the TypeScript
-`metaobjects-ui-web` provider, so a `view.image` carrying them is authorable in
-TypeScript alone. A backend port reads the field as the plain string it is.
+`view.image` and its five attrs (`@aspectRatio` … `@maxBytes`) are cross-port
+vocabulary that only TypeScript implements so far
+(`implementedIn: ["typescript"]` in the registry manifest —
+`fixtures/registry-conformance/README.md`). Every port registers the attrs and
+loads them strictly, and none but TypeScript acts on them. A backend port reads
+the field as the plain string it is.
## Authoring: `field.string` + `view.image`
diff --git a/docs/features/migrations/base-subtypes-are-not-authorable.md b/docs/features/migrations/base-subtypes-are-not-authorable.md
index 840c535766..288f42d3a6 100644
--- a/docs/features/migrations/base-subtypes-are-not-authorable.md
+++ b/docs/features/migrations/base-subtypes-are-not-authorable.md
@@ -71,9 +71,8 @@ the owning field's subtype, so a boolean default stays a boolean rather than bei
concrete view subtypes while the non-TS ports registered only `view.base` and `view.currency` — so
a backend port rejected `view.text` with `ERR_UNKNOWN_SUBTYPE`. That asymmetry is since gone: every
port now registers the generic `view.*` controls (`view.text`, `view.textarea`, `view.checkbox`,
-`view.radio`, `view.image`, …) for LOADING, still excluded from the cross-port manifest as
-PRESENTATION_ONLY (`fixtures/registry-conformance/README.md`), while the attrs ON those controls
-remain registered only by the TypeScript UI provider. The removal stands unchanged: `view.base`
+`view.radio`, `view.image`, …), which are cross-port vocabulary only TypeScript implements so far
+(`fixtures/registry-conformance/README.md`). The removal stands unchanged: `view.base`
itself is refused in every port.
Nothing is lost that carried information — a `view.base` node declared no kind and no attrs, and
diff --git a/examples/showcase/site-payload.json b/examples/showcase/site-payload.json
index 8124e84e1b..40de19a0d8 100644
--- a/examples/showcase/site-payload.json
+++ b/examples/showcase/site-payload.json
@@ -7,7 +7,7 @@
"metamodel": "1.3"
},
"counts": {
- "fixtures": 451,
+ "fixtures": 454,
"corpora": 38,
"baseTypes": 17
},
diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md
index 0e7f2570c9..48c0a14f43 100644
--- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md
+++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md
@@ -50,7 +50,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has
- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope.
## Authoring rules you must not violate
-- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts-only]` are TypeScript-only vocabulary the cross-port contract does not carry. `[-only]` (such as `[csharp-only]`) marks vocabulary only that port ever acts on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
+- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
- Nodes are fused-key maps: `{".": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md
index 0e7f2570c9..48c0a14f43 100644
--- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md
+++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md
@@ -50,7 +50,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has
- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope.
## Authoring rules you must not violate
-- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts-only]` are TypeScript-only vocabulary the cross-port contract does not carry. `[-only]` (such as `[csharp-only]`) marks vocabulary only that port ever acts on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
+- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
- Nodes are fused-key maps: `{".": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md
index 484509de51..44eb9f1644 100644
--- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md
+++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md
@@ -50,7 +50,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has
- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope.
## Authoring rules you must not violate
-- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts-only]` are TypeScript-only vocabulary the cross-port contract does not carry. `[-only]` (such as `[csharp-only]`) marks vocabulary only that port ever acts on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
+- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
- Nodes are fused-key maps: `{".": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md
index 484509de51..44eb9f1644 100644
--- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md
+++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md
@@ -50,7 +50,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has
- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope.
## Authoring rules you must not violate
-- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts-only]` are TypeScript-only vocabulary the cross-port contract does not carry. `[-only]` (such as `[csharp-only]`) marks vocabulary only that port ever acts on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
+- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
- Nodes are fused-key maps: `{".": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md
index 7bd3174d19..ece01bdac0 100644
--- a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md
+++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md
@@ -47,7 +47,7 @@ from.
- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope.
## Authoring rules you must not violate
-- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts-only]` are TypeScript-only vocabulary the cross-port contract does not carry. `[-only]` (such as `[csharp-only]`) marks vocabulary only that port ever acts on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
+- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
- Nodes are fused-key maps: `{".": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md
index 7bd3174d19..ece01bdac0 100644
--- a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md
+++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md
@@ -47,7 +47,7 @@ from.
- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope.
## Authoring rules you must not violate
-- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts-only]` are TypeScript-only vocabulary the cross-port contract does not carry. `[-only]` (such as `[csharp-only]`) marks vocabulary only that port ever acts on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
+- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
- Nodes are fused-key maps: `{".": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md
index ba41481275..2b67e4ec0b 100644
--- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md
+++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md
@@ -50,7 +50,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has
- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope.
## Authoring rules you must not violate
-- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts-only]` are TypeScript-only vocabulary the cross-port contract does not carry. `[-only]` (such as `[csharp-only]`) marks vocabulary only that port ever acts on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
+- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
- Nodes are fused-key maps: `{".": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md
index ba41481275..2b67e4ec0b 100644
--- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md
+++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md
@@ -50,7 +50,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has
- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope.
## Authoring rules you must not violate
-- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts-only]` are TypeScript-only vocabulary the cross-port contract does not carry. `[-only]` (such as `[csharp-only]`) marks vocabulary only that port ever acts on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
+- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
- Nodes are fused-key maps: `{".": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md
index 72b81c75ed..2fbe964b28 100644
--- a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md
+++ b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md
@@ -50,7 +50,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has
- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope.
## Authoring rules you must not violate
-- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts-only]` are TypeScript-only vocabulary the cross-port contract does not carry. `[-only]` (such as `[csharp-only]`) marks vocabulary only that port ever acts on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
+- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
- Nodes are fused-key maps: `{".": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md
index 72b81c75ed..2fbe964b28 100644
--- a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md
+++ b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md
@@ -50,7 +50,7 @@ Not there? List the missing generator — and a `docs` target, if `targets` has
- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope.
## Authoring rules you must not violate
-- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts-only]` are TypeScript-only vocabulary the cross-port contract does not carry. `[-only]` (such as `[csharp-only]`) marks vocabulary only that port ever acts on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
+- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
- Nodes are fused-key maps: `{".": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
diff --git a/fixtures/conformance/error-kind1-view-unregistered-attr/expected-errors.json b/fixtures/conformance/error-kind1-view-unregistered-attr/expected-errors.json
new file mode 100644
index 0000000000..3a8893091b
--- /dev/null
+++ b/fixtures/conformance/error-kind1-view-unregistered-attr/expected-errors.json
@@ -0,0 +1,15 @@
+{
+ "errors": [
+ {
+ "code": "ERR_UNKNOWN_ATTR",
+ "source": {
+ "format": "json",
+ "files": [
+ "meta.catalog.json"
+ ],
+ "jsonPath": "$['metadata.root'].children[0]['object.entity'].children[1]['field.string'].children[0]['view.image']"
+ }
+ }
+ ],
+ "warnings": []
+}
diff --git a/fixtures/conformance/error-kind1-view-unregistered-attr/input/meta.catalog.json b/fixtures/conformance/error-kind1-view-unregistered-attr/input/meta.catalog.json
new file mode 100644
index 0000000000..3b4cf58f39
--- /dev/null
+++ b/fixtures/conformance/error-kind1-view-unregistered-attr/input/meta.catalog.json
@@ -0,0 +1,40 @@
+{
+ "metadata.root": {
+ "package": "acme::catalog",
+ "children": [
+ {
+ "object.entity": {
+ "name": "Product",
+ "children": [
+ {
+ "field.long": {
+ "name": "id"
+ }
+ },
+ {
+ "field.string": {
+ "name": "coverKey",
+ "children": [
+ {
+ "view.image": {
+ "name": "form",
+ "@cropShape": "circle"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "identity.primary": {
+ "name": "pk",
+ "@fields": [
+ "id"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+}
diff --git a/fixtures/conformance/object-kind1-jvm-binding-attrs/expected.json b/fixtures/conformance/object-kind1-jvm-binding-attrs/expected.json
new file mode 100644
index 0000000000..b22884d21e
--- /dev/null
+++ b/fixtures/conformance/object-kind1-jvm-binding-attrs/expected.json
@@ -0,0 +1,110 @@
+{
+ "metadata.root": {
+ "package": "acme::catalog",
+ "children": [
+ {
+ "object.entity": {
+ "name": "Product",
+ "@object": "com.acme.catalog.Product",
+ "@objectAdapter": "com.acme.catalog.ProductAdapter",
+ "children": [
+ {
+ "source.rdb": {
+ "@table": "products"
+ }
+ },
+ {
+ "field.long": {
+ "name": "id"
+ }
+ },
+ {
+ "field.string": {
+ "name": "sku"
+ }
+ },
+ {
+ "identity.primary": {
+ "name": "pk",
+ "@fields": [
+ "id"
+ ]
+ }
+ },
+ {
+ "dimension.attribute": {
+ "name": "sku",
+ "@of": "acme::catalog::Product.sku"
+ }
+ },
+ {
+ "measure.aggregate": {
+ "name": "products",
+ "@agg": "count",
+ "@of": [
+ "acme::catalog::Product.id"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "object.value": {
+ "name": "Price",
+ "@object": "com.acme.catalog.Price",
+ "@objectAdapter": "com.acme.catalog.PriceAdapter",
+ "children": [
+ {
+ "field.string": {
+ "name": "currency"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "object.projection": {
+ "name": "ProductSummary",
+ "@object": "com.acme.catalog.ProductSummary",
+ "@objectAdapter": "com.acme.catalog.ProductSummaryAdapter",
+ "children": [
+ {
+ "source.rdb": {
+ "@kind": "view",
+ "@view": "v_product_summary"
+ }
+ },
+ {
+ "field.string": {
+ "name": "sku"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "object.report": {
+ "name": "ProductCounts",
+ "@dimensions": [
+ "sku"
+ ],
+ "@from": "acme::catalog::Product",
+ "@measures": [
+ "products"
+ ],
+ "@object": "com.acme.catalog.ProductCounts",
+ "@objectAdapter": "com.acme.catalog.ProductCountsAdapter",
+ "children": [
+ {
+ "source.rdb": {
+ "@kind": "view",
+ "@view": "v_product_counts"
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+}
diff --git a/fixtures/conformance/object-kind1-jvm-binding-attrs/input/meta.catalog.json b/fixtures/conformance/object-kind1-jvm-binding-attrs/input/meta.catalog.json
new file mode 100644
index 0000000000..150783e49b
--- /dev/null
+++ b/fixtures/conformance/object-kind1-jvm-binding-attrs/input/meta.catalog.json
@@ -0,0 +1,108 @@
+{
+ "metadata.root": {
+ "package": "acme::catalog",
+ "children": [
+ {
+ "object.entity": {
+ "name": "Product",
+ "@object": "com.acme.catalog.Product",
+ "@objectAdapter": "com.acme.catalog.ProductAdapter",
+ "children": [
+ {
+ "source.rdb": {
+ "@table": "products"
+ }
+ },
+ {
+ "field.long": {
+ "name": "id"
+ }
+ },
+ {
+ "field.string": {
+ "name": "sku"
+ }
+ },
+ {
+ "identity.primary": {
+ "name": "pk",
+ "@fields": [
+ "id"
+ ]
+ }
+ },
+ {
+ "dimension.attribute": {
+ "name": "sku",
+ "@of": "Product.sku"
+ }
+ },
+ {
+ "measure.aggregate": {
+ "name": "products",
+ "@agg": "count",
+ "@of": "Product.id"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "object.value": {
+ "name": "Price",
+ "@object": "com.acme.catalog.Price",
+ "@objectAdapter": "com.acme.catalog.PriceAdapter",
+ "children": [
+ {
+ "field.string": {
+ "name": "currency"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "object.projection": {
+ "name": "ProductSummary",
+ "@object": "com.acme.catalog.ProductSummary",
+ "@objectAdapter": "com.acme.catalog.ProductSummaryAdapter",
+ "children": [
+ {
+ "source.rdb": {
+ "@kind": "view",
+ "@view": "v_product_summary"
+ }
+ },
+ {
+ "field.string": {
+ "name": "sku"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "object.report": {
+ "name": "ProductCounts",
+ "@object": "com.acme.catalog.ProductCounts",
+ "@objectAdapter": "com.acme.catalog.ProductCountsAdapter",
+ "@from": "Product",
+ "@dimensions": [
+ "sku"
+ ],
+ "@measures": [
+ "products"
+ ],
+ "children": [
+ {
+ "source.rdb": {
+ "@kind": "view",
+ "@view": "v_product_counts"
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+}
diff --git a/fixtures/conformance/view-kind1-controls/expected.json b/fixtures/conformance/view-kind1-controls/expected.json
new file mode 100644
index 0000000000..6da2060ace
--- /dev/null
+++ b/fixtures/conformance/view-kind1-controls/expected.json
@@ -0,0 +1,193 @@
+{
+ "metadata.root": {
+ "package": "acme::catalog",
+ "children": [
+ {
+ "object.entity": {
+ "name": "Product",
+ "children": [
+ {
+ "field.long": {
+ "name": "id"
+ }
+ },
+ {
+ "field.string": {
+ "name": "checkboxValue",
+ "children": [
+ {
+ "view.checkbox": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "dateValue",
+ "children": [
+ {
+ "view.date": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "dropdownValue",
+ "children": [
+ {
+ "view.dropdown": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "hiddenValue",
+ "children": [
+ {
+ "view.hidden": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "hotlinkValue",
+ "children": [
+ {
+ "view.hotlink": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "monthValue",
+ "children": [
+ {
+ "view.month": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "numberValue",
+ "children": [
+ {
+ "view.number": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "passwordValue",
+ "children": [
+ {
+ "view.password": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "radioValue",
+ "children": [
+ {
+ "view.radio": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "textValue",
+ "children": [
+ {
+ "view.text": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "webValue",
+ "children": [
+ {
+ "view.web": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "notes",
+ "children": [
+ {
+ "view.textarea": {
+ "name": "form",
+ "@rows": 6
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "coverKey",
+ "@maxLength": 80,
+ "children": [
+ {
+ "view.image": {
+ "name": "form",
+ "@accept": [
+ "image/jpeg",
+ "image/png"
+ ],
+ "@aspectRatio": 1.5,
+ "@maxBytes": 10485760,
+ "@maxEdge": 2000,
+ "@store": "photos"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "identity.primary": {
+ "name": "pk",
+ "@fields": [
+ "id"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+}
diff --git a/fixtures/conformance/view-kind1-controls/input/meta.catalog.json b/fixtures/conformance/view-kind1-controls/input/meta.catalog.json
new file mode 100644
index 0000000000..7e5b217bae
--- /dev/null
+++ b/fixtures/conformance/view-kind1-controls/input/meta.catalog.json
@@ -0,0 +1,193 @@
+{
+ "metadata.root": {
+ "package": "acme::catalog",
+ "children": [
+ {
+ "object.entity": {
+ "name": "Product",
+ "children": [
+ {
+ "field.long": {
+ "name": "id"
+ }
+ },
+ {
+ "field.string": {
+ "name": "checkboxValue",
+ "children": [
+ {
+ "view.checkbox": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "dateValue",
+ "children": [
+ {
+ "view.date": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "dropdownValue",
+ "children": [
+ {
+ "view.dropdown": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "hiddenValue",
+ "children": [
+ {
+ "view.hidden": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "hotlinkValue",
+ "children": [
+ {
+ "view.hotlink": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "monthValue",
+ "children": [
+ {
+ "view.month": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "numberValue",
+ "children": [
+ {
+ "view.number": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "passwordValue",
+ "children": [
+ {
+ "view.password": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "radioValue",
+ "children": [
+ {
+ "view.radio": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "textValue",
+ "children": [
+ {
+ "view.text": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "webValue",
+ "children": [
+ {
+ "view.web": {
+ "name": "form"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "notes",
+ "children": [
+ {
+ "view.textarea": {
+ "name": "form",
+ "@rows": 6
+ }
+ }
+ ]
+ }
+ },
+ {
+ "field.string": {
+ "name": "coverKey",
+ "@maxLength": 80,
+ "children": [
+ {
+ "view.image": {
+ "name": "form",
+ "@aspectRatio": 1.5,
+ "@maxEdge": 2000,
+ "@store": "photos",
+ "@accept": [
+ "image/jpeg",
+ "image/png"
+ ],
+ "@maxBytes": 10485760
+ }
+ }
+ ]
+ }
+ },
+ {
+ "identity.primary": {
+ "name": "pk",
+ "@fields": [
+ "id"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+}
diff --git a/fixtures/metamodel-docs/expected/INDEX.md b/fixtures/metamodel-docs/expected/INDEX.md
index 1d5bb0f58f..1a81c10049 100644
--- a/fixtures/metamodel-docs/expected/INDEX.md
+++ b/fixtures/metamodel-docs/expected/INDEX.md
@@ -91,4 +91,17 @@ children and the cardinality of that subtype. Documentation attributes
| `validator.required` | Fails when the value is null (NOT NULL). Equivalent to @required on the owning field: on a non-array string, generated wire-tier input validation also rejects the empty string by default unless an explicit validator.length @min: 0 opts back to presence-only. | [types/validator.md#validatorrequired](types/validator.md#validatorrequired) |
| `validator.requiredWhen` | One-directional conditional presence: when the gating field (@when) equals @equals, the target field (@field) must be present (NOT NULL); otherwise @field is unconstrained. Mirrors JSON Schema dependentRequired / Rails validates_presence_of :x, if:. Entity-scoped; references fields by name. | [types/validator.md#validatorrequiredwhen](types/validator.md#validatorrequiredwhen) |
| `view.base` | Abstract view base — the shared root subtype for field-level UI/render hints. A view declares how a field's value is rendered or edited; the base carries no attrs of its own. Not authored directly: a `view.base` node fails to load (ERR_ABSTRACT_SUBTYPE_AUTHORED) — this subtype is a registry anchor concrete subtypes inherit from, never a node in a document. | [types/view.md#viewbase](types/view.md#viewbase) |
+| `view.checkbox` | Checkbox selection control. | [types/view.md#viewcheckbox](types/view.md#viewcheckbox) |
| `view.currency` | Currency display formatting (locale-aware via @locale). | [types/view.md#viewcurrency](types/view.md#viewcurrency) |
+| `view.date` | Date picker / date display. | [types/view.md#viewdate](types/view.md#viewdate) |
+| `view.dropdown` | Single-select dropdown control. | [types/view.md#viewdropdown](types/view.md#viewdropdown) |
+| `view.hidden` | Not rendered; carried but not shown. | [types/view.md#viewhidden](types/view.md#viewhidden) |
+| `view.hotlink` | Renders the value as a clickable link. | [types/view.md#viewhotlink](types/view.md#viewhotlink) |
+| `view.image` | Image upload/display control; the field stores an opaque storage key (field.string). | [types/view.md#viewimage](types/view.md#viewimage) |
+| `view.month` | Month picker / month display. | [types/view.md#viewmonth](types/view.md#viewmonth) |
+| `view.number` | Numeric input control. | [types/view.md#viewnumber](types/view.md#viewnumber) |
+| `view.password` | Masked input for sensitive values. | [types/view.md#viewpassword](types/view.md#viewpassword) |
+| `view.radio` | Radio-button selection control. | [types/view.md#viewradio](types/view.md#viewradio) |
+| `view.text` | Single-line text input/display. | [types/view.md#viewtext](types/view.md#viewtext) |
+| `view.textarea` | Multi-line text area. | [types/view.md#viewtextarea](types/view.md#viewtextarea) |
+| `view.web` | Abstract base for web-rendered views. | [types/view.md#viewweb](types/view.md#viewweb) |
diff --git a/fixtures/metamodel-docs/expected/providers.md b/fixtures/metamodel-docs/expected/providers.md
index a4d72cca64..e2a4b48491 100644
--- a/fixtures/metamodel-docs/expected/providers.md
+++ b/fixtures/metamodel-docs/expected/providers.md
@@ -13,7 +13,7 @@ provider owns. This is the ownership lens over the same vocabulary
Core metaobjects metamodel types and subtypes.
-**Owns (registers):** `attr.base`, `attr.boolean`, `attr.class`, `attr.double`, `attr.expression`, `attr.filter`, `attr.int`, `attr.intMap`, `attr.long`, `attr.properties`, `attr.string`, `field.base`, `field.boolean`, `field.currency`, `field.date`, `field.decimal`, `field.double`, `field.duration`, `field.enum`, `field.float`, `field.inet`, `field.int`, `field.long`, `field.map`, `field.object`, `field.string`, `field.time`, `field.timestamp`, `field.uri`, `field.uuid`, `identity.primary`, `identity.reference`, `identity.secondary`, `layout.base`, `layout.dataGrid`, `object.base`, `object.entity`, `object.projection`, `object.report`, `object.value`, `origin.aggregate`, `origin.base`, `origin.computed`, `origin.first`, `origin.passthrough`, `relationship.aggregation`, `relationship.association`, `relationship.base`, `relationship.composition`, `source.base`, `source.rdb`, `template.base`, `template.output`, `template.prompt`, `template.toolcall`, `validator.array`, `validator.atLeastOne`, `validator.base`, `validator.comparison`, `validator.exactlyOne`, `validator.length`, `validator.numeric`, `validator.presentIff`, `validator.regex`, `validator.required`, `validator.requiredWhen`, `view.base`, `view.currency`
+**Owns (registers):** `attr.base`, `attr.boolean`, `attr.class`, `attr.double`, `attr.expression`, `attr.filter`, `attr.int`, `attr.intMap`, `attr.long`, `attr.properties`, `attr.string`, `field.base`, `field.boolean`, `field.currency`, `field.date`, `field.decimal`, `field.double`, `field.duration`, `field.enum`, `field.float`, `field.inet`, `field.int`, `field.long`, `field.map`, `field.object`, `field.string`, `field.time`, `field.timestamp`, `field.uri`, `field.uuid`, `identity.primary`, `identity.reference`, `identity.secondary`, `layout.base`, `layout.dataGrid`, `object.base`, `object.entity`, `object.projection`, `object.report`, `object.value`, `origin.aggregate`, `origin.base`, `origin.computed`, `origin.first`, `origin.passthrough`, `relationship.aggregation`, `relationship.association`, `relationship.base`, `relationship.composition`, `source.base`, `source.rdb`, `template.base`, `template.output`, `template.prompt`, `template.toolcall`, `validator.array`, `validator.atLeastOne`, `validator.base`, `validator.comparison`, `validator.exactlyOne`, `validator.length`, `validator.numeric`, `validator.presentIff`, `validator.regex`, `validator.required`, `validator.requiredWhen`, `view.base`, `view.checkbox`, `view.currency`, `view.date`, `view.dropdown`, `view.hidden`, `view.hotlink`, `view.image`, `view.month`, `view.number`, `view.password`, `view.radio`, `view.text`, `view.textarea`, `view.web`
**Contributes attributes:**
@@ -29,6 +29,7 @@ Core metaobjects metamodel types and subtypes.
- `identity.primary`: `@fields`, `@generation`
- `identity.reference`: `@enforce`, `@fields`, `@references`
- `identity.secondary`: `@fields`
+- `object.base`: `@object`, `@objectAdapter`
- `object.entity`: `@dbSetName`, `@discriminator`, `@discriminatorValue`
- `object.projection`: `@dbSetName`, `@filter`
- `object.report`: `@dbSetName`, `@dimensions`, `@filter`, `@from`, `@measures`, `@segment`, `@spine`
@@ -156,7 +157,7 @@ UI/query-surface domain — @filterable / @sortable / @sortableDefaultOrder fiel
## metaobjects-ui-web
-TS-web presentation view attrs — @rows (view.textarea) and the view.image control attrs. Applied only in TypeScript; the non-TS ports mirror the spec file but never apply this provider (the view subtypes are TS-web presentation-only).
+Web presentation view attrs — @rows (view.textarea) and the view.image control attrs. Every port applies this provider and loads them strictly; only TypeScript acts on them so far.
**Contributes attributes:**
diff --git a/fixtures/metamodel-docs/expected/types/object.md b/fixtures/metamodel-docs/expected/types/object.md
index e02f41c316..cb69c9ca09 100644
--- a/fixtures/metamodel-docs/expected/types/object.md
+++ b/fixtures/metamodel-docs/expected/types/object.md
@@ -18,7 +18,10 @@ Abstract object base — the shared root subtype that concrete object subtypes (
**Attributes**
-_No subtype-specific attributes._
+| Attribute | Type | Required | Default | Allowed values | Provider | Description |
+| --- | --- | --- | --- | --- | --- | --- |
+| `@object` | string | no | | | metaobjects-core-types | The native class an object binds to (ADR-0001), in place of its default runtime class: on the JVM, a fully-qualified class name. Cross-port vocabulary only Java implements so far; every port loads and type-checks it, and the others do not act on it yet. |
+| `@objectAdapter` | string | no | | | metaobjects-core-types | JVM-only (ADR-0005): the fully-qualified name of an ObjectAdapter that takes over value access for this object's instances. Every port loads and type-checks it; only Java and Kotlin act on it. |
**Allowed children**
@@ -46,6 +49,8 @@ An object that owns its data: own identity, writable sources, and lifecycle. The
| `@dbSetName` | string | no | | | metaobjects-core-types | C#-only (EF Core): the DbSet property name the generated context gives this object, in place of the plural derived from its name. Set it when two objects in one generated context would otherwise claim the same property. Every port loads and type-checks it; only C# acts on it. |
| `@discriminator` | string | no | | | metaobjects-core-types | FR-014: names the field on this entity (resolvable via extends:) that holds the subtype-discriminator value. Subtypes of this entity declare @discriminatorValue to bind their rows to a discriminator value. The discriminator field itself is an ordinary field declaration (typically field.enum or field.int / field.string). |
| `@discriminatorValue` | string | no | | | metaobjects-core-types | FR-014: on a subtype of an entity with @discriminator — the value that identifies rows of this subtype in the shared discriminator field. Wire form is always a string; the underlying field's subtype (enum / int / string) controls codegen + storage coercion. Required on every concrete subtype of a discriminated entity. |
+| `@object` | string | no | | | metaobjects-core-types | The native class an object binds to (ADR-0001), in place of its default runtime class: on the JVM, a fully-qualified class name. Cross-port vocabulary only Java implements so far; every port loads and type-checks it, and the others do not act on it yet. |
+| `@objectAdapter` | string | no | | | metaobjects-core-types | JVM-only (ADR-0005): the fully-qualified name of an ObjectAdapter that takes over value access for this object's instances. Every port loads and type-checks it; only Java and Kotlin act on it. |
**Allowed children**
@@ -77,6 +82,8 @@ A derived read-only representation of entities. Its fields are extends-bound / o
| --- | --- | --- | --- | --- | --- | --- |
| `@dbSetName` | string | no | | | metaobjects-core-types | C#-only (EF Core): the DbSet property name the generated context gives this object, in place of the plural derived from its name. Set it when two objects in one generated context would otherwise claim the same property. Every port loads and type-checks it; only C# acts on it. |
| `@filter` | filter | no | | | metaobjects-core-types | Optional row-scope predicate (a portable attr.filter object: eq/ne/gt/gte/lt/lte/like/in/isNull with and/or, desugared to { field: { op: value } } at parse time) selecting which rows the view returns — lowered to an outer SQL WHERE. Resolves against the projection's own declared fields; an aggregate-derived field is not addressable (fail-closed). |
+| `@object` | string | no | | | metaobjects-core-types | The native class an object binds to (ADR-0001), in place of its default runtime class: on the JVM, a fully-qualified class name. Cross-port vocabulary only Java implements so far; every port loads and type-checks it, and the others do not act on it yet. |
+| `@objectAdapter` | string | no | | | metaobjects-core-types | JVM-only (ADR-0005): the fully-qualified name of an ObjectAdapter that takes over value access for this object's instances. Every port loads and type-checks it; only Java and Kotlin act on it. |
**Allowed children**
@@ -104,6 +111,8 @@ A declared report (FR-044): a fixed combination of dimensions and measures of ON
| `@filter` | filter | no | | | metaobjects-core-types | Optional row scope over @from's fields; may use relative-date values. |
| `@from` | string | yes | | | metaobjects-core-types | The object.entity whose rows the report aggregates. |
| `@measures` | string[] | yes | | | metaobjects-core-types | Measure names of @from. |
+| `@object` | string | no | | | metaobjects-core-types | The native class an object binds to (ADR-0001), in place of its default runtime class: on the JVM, a fully-qualified class name. Cross-port vocabulary only Java implements so far; every port loads and type-checks it, and the others do not act on it yet. |
+| `@objectAdapter` | string | no | | | metaobjects-core-types | JVM-only (ADR-0005): the fully-qualified name of an ObjectAdapter that takes over value access for this object's instances. Every port loads and type-checks it; only Java and Kotlin act on it. |
| `@segment` | string | no | | | metaobjects-core-types | Optional segment of @from scoping the rows. |
| `@spine` | string | no | | | metaobjects-core-types | Optional to-one path from @from to the entity whose rows supply the report's rows (e.g. 'Purchase.program'), written like a dimension's @via. With it the report has one row per distinct dimension tuple among THAT entity's rows, including the ones no row of @from refers to: a count there is 0 and any other measure is null unless it declares @default. Every listed dimension must be reached through this path. @segment and @filter still scope the rows of @from and never remove a row. A row of @from whose reference is null or matches nothing is in no row of the report. |
@@ -131,6 +140,8 @@ A value object — pure shape with NO identity and NO source, ever. Constructed
| Attribute | Type | Required | Default | Allowed values | Provider | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `@normalize` | string | no | `strip` | `none`, `collapse`, `strip` | metaobjects-prompt | ASCII normalization mode for tolerant enum extract (none\|collapse\|strip, default strip). On field.enum it is per-field; on object.value it is the default for the object's enum fields. |
+| `@object` | string | no | | | metaobjects-core-types | The native class an object binds to (ADR-0001), in place of its default runtime class: on the JVM, a fully-qualified class name. Cross-port vocabulary only Java implements so far; every port loads and type-checks it, and the others do not act on it yet. |
+| `@objectAdapter` | string | no | | | metaobjects-core-types | JVM-only (ADR-0005): the fully-qualified name of an ObjectAdapter that takes over value access for this object's instances. Every port loads and type-checks it; only Java and Kotlin act on it. |
**Allowed children**
diff --git a/fixtures/metamodel-docs/expected/types/view.md b/fixtures/metamodel-docs/expected/types/view.md
index 1615655f98..edc362ecd3 100644
--- a/fixtures/metamodel-docs/expected/types/view.md
+++ b/fixtures/metamodel-docs/expected/types/view.md
@@ -24,6 +24,22 @@ _No subtype-specific attributes._
_No structural children._
+### view.checkbox
+
+Checkbox selection control.
+
+**Owning provider:** metaobjects-core-types
+
+**Implemented in:** typescript so far — every port loads and type-checks it; the others do not act on it yet.
+
+**Attributes**
+
+_No subtype-specific attributes._
+
+**Allowed children**
+
+_No structural children._
+
### view.currency
Currency display formatting (locale-aware via @locale).
@@ -40,3 +56,203 @@ Currency display formatting (locale-aware via @locale).
_No structural children._
+### view.date
+
+Date picker / date display.
+
+**Owning provider:** metaobjects-core-types
+
+**Implemented in:** typescript so far — every port loads and type-checks it; the others do not act on it yet.
+
+**Attributes**
+
+_No subtype-specific attributes._
+
+**Allowed children**
+
+_No structural children._
+
+### view.dropdown
+
+Single-select dropdown control.
+
+**Owning provider:** metaobjects-core-types
+
+**Implemented in:** typescript so far — every port loads and type-checks it; the others do not act on it yet.
+
+**Attributes**
+
+_No subtype-specific attributes._
+
+**Allowed children**
+
+_No structural children._
+
+### view.hidden
+
+Not rendered; carried but not shown.
+
+**Owning provider:** metaobjects-core-types
+
+**Implemented in:** typescript so far — every port loads and type-checks it; the others do not act on it yet.
+
+**Attributes**
+
+_No subtype-specific attributes._
+
+**Allowed children**
+
+_No structural children._
+
+### view.hotlink
+
+Renders the value as a clickable link.
+
+**Owning provider:** metaobjects-core-types
+
+**Implemented in:** typescript so far — every port loads and type-checks it; the others do not act on it yet.
+
+**Attributes**
+
+_No subtype-specific attributes._
+
+**Allowed children**
+
+_No structural children._
+
+### view.image
+
+Image upload/display control; the field stores an opaque storage key (field.string).
+
+**Owning provider:** metaobjects-core-types
+
+**Implemented in:** typescript so far — every port loads and type-checks it; the others do not act on it yet.
+
+**Attributes**
+
+| Attribute | Type | Required | Default | Allowed values | Provider | Description |
+| --- | --- | --- | --- | --- | --- | --- |
+| `@accept` | string[] | no | | | metaobjects-ui-web | Accepted MIME types (client guard; the server re-enforces). |
+| `@aspectRatio` | double | no | | | metaobjects-ui-web | Crop aspect ratio (width / height). Omit for freeform. |
+| `@maxBytes` | int | no | | | metaobjects-ui-web | Client-side size ceiling in bytes. |
+| `@maxEdge` | int | no | | | metaobjects-ui-web | Longest-edge bound in px for the re-encoded output. |
+| `@store` | string | no | | | metaobjects-ui-web | Opaque storage-namespace hint passed to the upload adapter (not infrastructure). |
+
+**Allowed children**
+
+_No structural children._
+
+### view.month
+
+Month picker / month display.
+
+**Owning provider:** metaobjects-core-types
+
+**Implemented in:** typescript so far — every port loads and type-checks it; the others do not act on it yet.
+
+**Attributes**
+
+_No subtype-specific attributes._
+
+**Allowed children**
+
+_No structural children._
+
+### view.number
+
+Numeric input control.
+
+**Owning provider:** metaobjects-core-types
+
+**Implemented in:** typescript so far — every port loads and type-checks it; the others do not act on it yet.
+
+**Attributes**
+
+_No subtype-specific attributes._
+
+**Allowed children**
+
+_No structural children._
+
+### view.password
+
+Masked input for sensitive values.
+
+**Owning provider:** metaobjects-core-types
+
+**Implemented in:** typescript so far — every port loads and type-checks it; the others do not act on it yet.
+
+**Attributes**
+
+_No subtype-specific attributes._
+
+**Allowed children**
+
+_No structural children._
+
+### view.radio
+
+Radio-button selection control.
+
+**Owning provider:** metaobjects-core-types
+
+**Implemented in:** typescript so far — every port loads and type-checks it; the others do not act on it yet.
+
+**Attributes**
+
+_No subtype-specific attributes._
+
+**Allowed children**
+
+_No structural children._
+
+### view.text
+
+Single-line text input/display.
+
+**Owning provider:** metaobjects-core-types
+
+**Implemented in:** typescript so far — every port loads and type-checks it; the others do not act on it yet.
+
+**Attributes**
+
+_No subtype-specific attributes._
+
+**Allowed children**
+
+_No structural children._
+
+### view.textarea
+
+Multi-line text area.
+
+**Owning provider:** metaobjects-core-types
+
+**Implemented in:** typescript so far — every port loads and type-checks it; the others do not act on it yet.
+
+**Attributes**
+
+| Attribute | Type | Required | Default | Allowed values | Provider | Description |
+| --- | --- | --- | --- | --- | --- | --- |
+| `@rows` | int | no | | | metaobjects-ui-web | Visible row count for the generated
+ * must accept an authored {@code extends:}). {@code object} (ADR-0001) and
+ * {@code objectAdapter} (ADR-0005) are NOT carved out: they are in the manifest,
+ * marked {@code implementedIn: ["java"]} and {@code ports: ["java", "kotlin"]}.
*/
private static final Map EXCLUDED_PER_TYPE_ATTRS = Map.of(
MetaField.ATTR_IS_ARRAY, ExclusionReason.STRUCTURAL_KEYWORD,
MetaData.ATTR_IS_ABSTRACT, ExclusionReason.STRUCTURAL_KEYWORD,
com.metaobjects.object.MetaObject.ATTR_EXTENDS, ExclusionReason.STRUCTURAL_KEYWORD,
com.metaobjects.object.MetaObject.ATTR_IMPLEMENTS, ExclusionReason.STRUCTURAL_KEYWORD,
- com.metaobjects.object.MetaObject.ATTR_IS_INTERFACE, ExclusionReason.STRUCTURAL_KEYWORD,
- com.metaobjects.object.MetaObject.ATTR_OBJECT, ExclusionReason.NATIVE_BINDING,
- com.metaobjects.object.AbstractObjectRepresentation.ATTR_OBJECT_ADAPTER, ExclusionReason.NATIVE_BINDING);
+ com.metaobjects.object.MetaObject.ATTR_IS_INTERFACE, ExclusionReason.STRUCTURAL_KEYWORD);
/**
* Classify a per-type attr: an {@link ExclusionReason} (carved out) or
@@ -390,21 +383,21 @@ public static ExclusionReason classifyPerTypeAttr(String name) {
/**
* Classify a {@code (type, subType)} row: the {@code metadata.base} inheritance
- * anchor (C-5) / the generic presentation-only {@code view.*} controls (B-2) /
- * {@link ExclusionReason#INCLUDED}.
+ * anchor (C-5) / {@link ExclusionReason#INCLUDED}. The generic {@code view.*}
+ * controls are NOT carved out: they are in the manifest with {@code implementedIn}.
*/
public static ExclusionReason classifyTypeSubType(String type, String subType) {
if (MetaData.TYPE_METADATA.equals(type) && MetaData.SUBTYPE_BASE.equals(subType)) {
return ExclusionReason.INHERITANCE_ANCHOR; // C-5 — Java's internal inheritance anchor
}
- if (MetaView.TYPE_VIEW.equals(type)
- && !MetaData.SUBTYPE_BASE.equals(subType)
- && !CurrencyView.SUBTYPE_CURRENCY.equals(subType)) {
- return ExclusionReason.PRESENTATION_ONLY; // B-2 — TS-web-presentation generic view controls
- }
return ExclusionReason.INCLUDED;
}
+ /** A sorted copy of a spec port list, or {@code null} when it is absent or empty. */
+ private static List sortedOrNull(List ports) {
+ return ports != null && !ports.isEmpty() ? ports.stream().sorted().toList() : null;
+ }
+
/** True if a {@code (type, subType)} row is carved out of the manifest (any reason). */
private static boolean isExcludedTypeSubType(String type, String subType) {
return classifyTypeSubType(type, subType) != ExclusionReason.INCLUDED;
@@ -427,7 +420,7 @@ private static boolean isExcludedTypeSubType(String type, String subType) {
* {@code rules}/{@code example}/{@code whenToUse} doc facets (null = omitted).
*/
private record ManifestAttr(String name, String valueType, boolean isArray, boolean required,
- List allowedValues, List ports,
+ List allowedValues, List implementedIn, List ports,
String description, String rules, String example, String whenToUse) {}
/**
@@ -446,7 +439,7 @@ private record ManifestChild(String childType, Object childSubType, String child
* structural constraint graph (FR-033). {@code rules}/{@code example}/
* {@code whenToUse} are null when absent; {@code parents} is empty when absent.
*/
- private record ManifestType(String type, String subType, String description,
+ private record ManifestType(String type, String subType, List implementedIn, String description,
String rules, String example, String whenToUse,
List attrs, List children,
List parents) {}
@@ -513,9 +506,10 @@ private static List attrsOf(MetaDataRegistry registry, String type
// (omitted by the serializer). Sub-step B may source them.
// The ports that act on a language-specific attr, from the spec, sorted.
var doc = SPEC.attrDoc(type, subType, name);
- List ports = doc != null && doc.ports() != null && !doc.ports().isEmpty()
- ? doc.ports().stream().sorted().toList() : null;
- byName.put(name, new ManifestAttr(name, valueType, isArray, required, allowedValues, ports,
+ List ports = sortedOrNull(doc != null ? doc.ports() : null);
+ // The ports that implement a cross-port attr so far, from the spec, sorted.
+ List implementedIn = sortedOrNull(doc != null ? doc.implementedIn() : null);
+ byName.put(name, new ManifestAttr(name, valueType, isArray, required, allowedValues, implementedIn, ports,
description, null, null, null));
}
@@ -714,7 +708,7 @@ public static String emit(MetaDataRegistry registry) {
List types = new ArrayList<>();
for (MetaDataTypeId id : registry.getRegisteredTypes()) {
if (isExcludedTypeSubType(id.type(), id.subType())) {
- continue; // metadata.base anchor (C-5) / generic view.* controls (B-2)
+ continue; // metadata.base anchor (C-5)
}
TypeDefinition def = registry.getTypeDefinition(id.type(), id.subType());
// FR-033: per-type docs. description is the existing TypeDefinition
@@ -725,7 +719,10 @@ public static String emit(MetaDataRegistry registry) {
String example = def != null ? def.getExample() : null;
String whenToUse = def != null ? def.getWhenToUse() : null;
List parents = def != null ? def.getParents() : List.of();
- types.add(new ManifestType(id.type(), id.subType(), description,
+ // The ports that implement this cross-port type so far, from the spec.
+ var typeDoc = SPEC.typeDoc(id.type(), id.subType());
+ List implementedIn = sortedOrNull(typeDoc != null ? typeDoc.implementedIn() : null);
+ types.add(new ManifestType(id.type(), id.subType(), implementedIn, description,
rules, example, whenToUse,
attrsOf(registry, id.type(), id.subType(), arrayAttrNames),
childrenOf(registry, id.type(), id.subType()),
@@ -744,7 +741,7 @@ public static String emit(MetaDataRegistry registry) {
// ADR-0036 Wave 1 — no common attr declares a closed value-set today
// (null → the allowedValues key is omitted by the serializer).
commonAttrs.add(new ManifestAttr(def.name(), def.valueType(), def.isArray(), false,
- null, null, def.description() != null ? def.description() : "", null, null, null));
+ null, null, null, def.description() != null ? def.description() : "", null, null, null));
}
commonAttrs.sort(Comparator.comparing(ManifestAttr::name));
@@ -788,6 +785,12 @@ private static String serialize(List types,
sb.append(" {\n");
sb.append(" \"type\": ").append(jsonString(t.type())).append(",\n");
sb.append(" \"subType\": ").append(jsonString(t.subType())).append(",\n");
+ // The ports that implement it so far, only when named.
+ if (t.implementedIn() != null) {
+ sb.append(" \"implementedIn\": ");
+ appendStringArray(sb, t.implementedIn(), " ");
+ sb.append(",\n");
+ }
sb.append(" \"description\": ").append(jsonString(t.description())).append(",\n");
// FR-033: optional type-level docs, emitted ONLY when present, in
// fixed order between `description` and `attrs`.
@@ -871,7 +874,13 @@ private static void appendAttrs(StringBuilder sb, List attrs, Stri
appendStringArray(sb, a.allowedValues(), fieldIndent);
sb.append(",\n");
}
- // The ports that act on a language-specific attr, only when named.
+ // The ports that implement a cross-port attr so far, then the ports that act
+ // on a language-specific attr; each only when named.
+ if (a.implementedIn() != null && !a.implementedIn().isEmpty()) {
+ sb.append(fieldIndent).append("\"implementedIn\": ");
+ appendStringArray(sb, a.implementedIn(), fieldIndent);
+ sb.append(",\n");
+ }
if (a.ports() != null && !a.ports().isEmpty()) {
sb.append(fieldIndent).append("\"ports\": ");
appendStringArray(sb, a.ports(), fieldIndent);
diff --git a/server/java/metadata/src/main/java/com/metaobjects/registry/spec/SpecMetamodelReader.java b/server/java/metadata/src/main/java/com/metaobjects/registry/spec/SpecMetamodelReader.java
index 425dac2d15..c123a4c99b 100644
--- a/server/java/metadata/src/main/java/com/metaobjects/registry/spec/SpecMetamodelReader.java
+++ b/server/java/metadata/src/main/java/com/metaobjects/registry/spec/SpecMetamodelReader.java
@@ -83,7 +83,9 @@ public final class SpecMetamodelReader {
* One documented metamodel facet (a type/subType or an attr) — its description
* plus the optional doc fields. Any may be {@code null} when absent in the JSON.
*/
- public record DocFacet(String description, String rules, String example, String whenToUse) {
+ public record DocFacet(String description, String rules, String example, String whenToUse,
+ /* The ports that implement this cross-port type so far; null = every port. */
+ java.util.List implementedIn) {
}
/**
@@ -95,7 +97,9 @@ public record AttrEntry(String name, String subType, boolean isArray,
String description, String rules, String example, String whenToUse,
java.util.List allowedValues,
/* The ports that act on a language-specific attr; null = every port. */
- java.util.List ports) {
+ java.util.List ports,
+ /* The ports that implement this cross-port attr so far; null = every port. */
+ java.util.List implementedIn) {
}
/**
@@ -211,7 +215,8 @@ private void parse(JsonObject provider) {
declaredKeys.add(key);
typeDocs.put(key, new DocFacet(
nullable(t, "description"), nullable(t, "rules"),
- nullable(t, "example"), nullable(t, "whenToUse")));
+ nullable(t, "example"), nullable(t, "whenToUse"),
+ parseStringList(t, "implementedIn")));
Map attrs = typeAttrDocs.computeIfAbsent(key, k -> new LinkedHashMap<>());
for (AttrEntry a : parseAttrChildren(t)) {
attrs.put(a.name(), a);
@@ -293,7 +298,8 @@ private List parseAttrChildren(JsonObject owner) {
str(c, "name"), nullable(c, "subType"), isArray, min, max, maxIsNull,
nullable(c, "description"), nullable(c, "rules"),
nullable(c, "example"), nullable(c, "whenToUse"),
- parseAllowedValues(c), parseStringList(c, "ports")));
+ parseAllowedValues(c), parseStringList(c, "ports"),
+ parseStringList(c, "implementedIn")));
}
return out;
}
diff --git a/server/java/metadata/src/main/java/com/metaobjects/view/PresentationView.java b/server/java/metadata/src/main/java/com/metaobjects/view/PresentationView.java
index 03503f2e10..a9cd20400a 100644
--- a/server/java/metadata/src/main/java/com/metaobjects/view/PresentationView.java
+++ b/server/java/metadata/src/main/java/com/metaobjects/view/PresentationView.java
@@ -9,9 +9,11 @@
* {@code view.dropdown}, ...).
*
* They are registered so a metadata document that carries them LOADS in the
- * Java port, as it does in every other port. They stay TS-web-PRESENTATION-ONLY:
- * no Java codegen, render or runtime consumer reads them, and the registry
- * manifest excludes them ({@code RegistryManifest}, {@code PRESENTATION_ONLY}).
+ * Java port, as it does in every other port. They are cross-port vocabulary only
+ * TypeScript implements so far: no Java codegen, render or runtime consumer reads
+ * them, and the registry manifest carries them marked {@code implementedIn: ["typescript"]}.
+ * Their attrs ({@code view.textarea @rows}, the {@code view.image} attrs) come from
+ * the {@code metaobjects-ui-web} provider.
* One class serves every control kind because a view carries no per-subtype
* behavior here.
*
diff --git a/server/java/metadata/src/main/resources/META-INF/services/com.metaobjects.registry.MetaDataTypeProvider b/server/java/metadata/src/main/resources/META-INF/services/com.metaobjects.registry.MetaDataTypeProvider
index bee88db4ec..949d3c2b88 100644
--- a/server/java/metadata/src/main/resources/META-INF/services/com.metaobjects.registry.MetaDataTypeProvider
+++ b/server/java/metadata/src/main/resources/META-INF/services/com.metaobjects.registry.MetaDataTypeProvider
@@ -16,5 +16,6 @@ com.metaobjects.view.ViewTypesMetaDataProvider
com.metaobjects.layout.LayoutTypesMetaDataProvider
com.metaobjects.template.TemplateTypesMetaDataProvider
com.metaobjects.presentation.ui.UiTypesMetaDataProvider
+com.metaobjects.presentation.ui.UiWebTypesMetaDataProvider
com.metaobjects.template.PromptTypesMetaDataProvider
com.metaobjects.object.CoreObjectsMetaDataProvider
diff --git a/server/java/metadata/src/test/java/com/metaobjects/conformance/ConformanceTest.java b/server/java/metadata/src/test/java/com/metaobjects/conformance/ConformanceTest.java
index ac113346be..86f4a815a1 100644
--- a/server/java/metadata/src/test/java/com/metaobjects/conformance/ConformanceTest.java
+++ b/server/java/metadata/src/test/java/com/metaobjects/conformance/ConformanceTest.java
@@ -187,6 +187,9 @@ public class ConformanceTest {
"metaobjects-ui", List.of(
com.metaobjects.presentation.ui.UiTypesMetaDataProvider.PROVIDER_ID
),
+ "metaobjects-ui-web", List.of(
+ com.metaobjects.presentation.ui.UiWebTypesMetaDataProvider.PROVIDER_ID
+ ),
"metaobjects-prompt", List.of(
com.metaobjects.template.PromptTypesMetaDataProvider.PROVIDER_ID
)
diff --git a/server/java/metadata/src/test/java/com/metaobjects/registry/RegistryManifestConformanceTest.java b/server/java/metadata/src/test/java/com/metaobjects/registry/RegistryManifestConformanceTest.java
index f4622e7897..eb7ca0e504 100644
--- a/server/java/metadata/src/test/java/com/metaobjects/registry/RegistryManifestConformanceTest.java
+++ b/server/java/metadata/src/test/java/com/metaobjects/registry/RegistryManifestConformanceTest.java
@@ -212,9 +212,10 @@ public void carveOutsCarryTheirDeclaredReasonCategory() {
RegistryManifest.classifyPerTypeAttr("extends"));
assertEquals(RegistryManifest.ExclusionReason.STRUCTURAL_KEYWORD,
RegistryManifest.classifyPerTypeAttr("isArray"));
- assertEquals(RegistryManifest.ExclusionReason.NATIVE_BINDING,
+ // @object and @objectAdapter are in the manifest (implementedIn / ports), not carved out.
+ assertEquals(RegistryManifest.ExclusionReason.INCLUDED,
RegistryManifest.classifyPerTypeAttr("object"));
- assertEquals(RegistryManifest.ExclusionReason.NATIVE_BINDING,
+ assertEquals(RegistryManifest.ExclusionReason.INCLUDED,
RegistryManifest.classifyPerTypeAttr("objectAdapter"));
// Genuinely logical attrs are INCLUDED.
assertEquals(RegistryManifest.ExclusionReason.INCLUDED,
@@ -224,7 +225,8 @@ public void carveOutsCarryTheirDeclaredReasonCategory() {
// Row classification.
assertEquals(RegistryManifest.ExclusionReason.INHERITANCE_ANCHOR,
RegistryManifest.classifyTypeSubType("metadata", "base"));
- assertEquals(RegistryManifest.ExclusionReason.PRESENTATION_ONLY,
+ // The generic view controls are in the manifest, marked implementedIn.
+ assertEquals(RegistryManifest.ExclusionReason.INCLUDED,
RegistryManifest.classifyTypeSubType("view", "dropdown"));
assertEquals(RegistryManifest.ExclusionReason.INCLUDED,
RegistryManifest.classifyTypeSubType("view", "currency"));
diff --git a/server/python/src/metaobjects/core_types.py b/server/python/src/metaobjects/core_types.py
index 08b48caaf8..893133929f 100644
--- a/server/python/src/metaobjects/core_types.py
+++ b/server/python/src/metaobjects/core_types.py
@@ -60,6 +60,8 @@
OBJECT_ATTR_DISCRIMINATOR,
OBJECT_ATTR_DISCRIMINATOR_VALUE,
OBJECT_ATTR_DB_SET_NAME,
+ OBJECT_ATTR_OBJECT,
+ OBJECT_ATTR_OBJECT_ADAPTER,
OBJECT_PROJECTION_ATTR_FILTER,
OBJECT_REPORT_ATTR_DIMENSIONS,
OBJECT_REPORT_ATTR_FILTER,
@@ -299,6 +301,10 @@ def _register_subtypes(
# declares it on entity / projection / report; the strict per-subtype scoping
# (apply_spec_descriptions) prunes it from the others.
AttrSchema(name=OBJECT_ATTR_DB_SET_NAME, value_type=ATTR_SUBTYPE_STRING, required=False),
+ # @object / @objectAdapter: the JVM's native binding attrs, declared on object.base
+ # in object.json and so kept on every subtype. Loaded and type-checked; unused here.
+ AttrSchema(name=OBJECT_ATTR_OBJECT, value_type=ATTR_SUBTYPE_STRING, required=False),
+ AttrSchema(name=OBJECT_ATTR_OBJECT_ADAPTER, value_type=ATTR_SUBTYPE_STRING, required=False),
]
# FR-011: @normalize is an object-level default for the enum fields of a payload
# value-object — registered on object.value ONLY (not entity/base). FR-033 re-homes
@@ -1256,6 +1262,7 @@ def _register_subtypes(
from .meta.persistence.db.db_provider import db_provider # noqa: E402
from .meta.template.prompt_provider import prompt_provider # noqa: E402
from .meta.presentation.ui.ui_provider import ui_provider # noqa: E402
+from .meta.presentation.ui.ui_web_provider import ui_web_provider # noqa: E402
from .documentation.doc_provider import doc_provider # noqa: E402
core_providers: list[Provider] = [
@@ -1264,6 +1271,9 @@ def _register_subtypes(
doc_provider,
prompt_provider,
ui_provider,
+ # view.textarea @rows and the view.image attrs (ui-web.json): cross-port vocabulary
+ # only TypeScript implements so far. Loaded strictly here and ignored.
+ ui_web_provider,
]
# #265 — the ids of the LIBRARY providers above, as a frozen set. Consulted by
diff --git a/server/python/src/metaobjects/meta/core/object/object_constants.py b/server/python/src/metaobjects/meta/core/object/object_constants.py
index 153a0d2837..8d3e31d6d0 100644
--- a/server/python/src/metaobjects/meta/core/object/object_constants.py
+++ b/server/python/src/metaobjects/meta/core/object/object_constants.py
@@ -24,6 +24,11 @@
# C#-only (ports ["csharp"]): the EF Core DbSet property name. Loaded and type-checked here
# on object.entity / projection / report; no Python generator acts on it.
OBJECT_ATTR_DB_SET_NAME = "dbSetName"
+# On object.base, so every object subtype carries them. ``@object`` (ADR-0001) is
+# cross-port vocabulary only Java implements so far; ``@objectAdapter`` (ADR-0005) is
+# JVM-only (ports ["java", "kotlin"]). Python loads and type-checks both; it acts on neither.
+OBJECT_ATTR_OBJECT = "object"
+OBJECT_ATTR_OBJECT_ADAPTER = "objectAdapter"
# #207 — object.projection row-scope @filter (view-level WHERE). An optional
# row-scope predicate on an object.projection (a portable attr.filter object —
diff --git a/server/python/src/metaobjects/meta/presentation/ui/ui_web_provider.py b/server/python/src/metaobjects/meta/presentation/ui/ui_web_provider.py
new file mode 100644
index 0000000000..509b29a76d
--- /dev/null
+++ b/server/python/src/metaobjects/meta/presentation/ui/ui_web_provider.py
@@ -0,0 +1,33 @@
+"""ui_web_provider — the web presentation view-attr MetaDataTypeProvider
+(cross-port ``metaobjects-ui-web``).
+
+Registers the attrs the embedded ``spec/metamodel/ui-web.json`` ``extends``
+directives declare: ``view.textarea @rows`` and the ``view.image`` control's
+``@aspectRatio`` / ``@maxEdge`` / ``@store`` / ``@accept`` / ``@maxBytes``. They are
+cross-port vocabulary only TypeScript implements so far: Python registers them so a
+shared model loads strictly here, and acts on none of them. Mirrors the TS
+``uiWebProvider``, the C# ``UiWebMetaDataProvider`` and Java's
+``UiWebTypesMetaDataProvider``.
+"""
+from __future__ import annotations
+
+from ....provider import Provider
+from ....registry import TypeRegistry
+from ...provider_extends import register_extends
+
+#: The embedded shared provider-definition file this provider sources its attrs from.
+_UI_WEB_DEFINITION_FILE = "ui-web.json"
+
+
+def _register(registry: TypeRegistry) -> None:
+ # Exact subtypes only (view.textarea / view.image): no wildcard to expand.
+ register_extends(registry, _UI_WEB_DEFINITION_FILE)
+
+
+def _make_ui_web_provider() -> Provider:
+ p = Provider("metaobjects-ui-web", dependencies=("metaobjects-core-types",))
+ p.on_register(_register)
+ return p
+
+
+ui_web_provider = _make_ui_web_provider()
diff --git a/server/python/src/metaobjects/meta/presentation/view/view_constants.py b/server/python/src/metaobjects/meta/presentation/view/view_constants.py
index 51f7a182c1..c2a9605ca8 100644
--- a/server/python/src/metaobjects/meta/presentation/view/view_constants.py
+++ b/server/python/src/metaobjects/meta/presentation/view/view_constants.py
@@ -5,11 +5,10 @@
Every port REGISTERS the full view vocabulary, so one metadata document loads
in every port. The generic web-presentation control kinds (text/textarea/date/
month/hotlink/dropdown/radio/checkbox/number/password/hidden/web/image) are
-still TS-web-PRESENTATION-ONLY: they have no backend / codegen / render consumer
-in Python, and the registry manifest excludes them (``registry_manifest``,
-PRESENTATION_ONLY). Registration here is for LOADING only. ``base`` +
-``currency`` (the cross-port currency ``@locale`` wire contract) are the two
-rows in the cross-port manifest.
+cross-port vocabulary only TypeScript implements so far: they have no backend /
+codegen / render consumer in Python, and the registry manifest carries them marked
+``implementedIn: ["typescript"]``. Registration here is for LOADING only; their
+attrs (``view.textarea @rows``, the ``view.image`` attrs) come from ``ui_web_provider``.
Mirrors typescript/packages/metadata/src/presentation/view/view-constants.ts.
"""
diff --git a/server/python/src/metaobjects/registry.py b/server/python/src/metaobjects/registry.py
index 82dce36cc1..d484c182bc 100644
--- a/server/python/src/metaobjects/registry.py
+++ b/server/python/src/metaobjects/registry.py
@@ -46,6 +46,8 @@ class AttrSchema:
when_to_use: str | None = None
# The ports that act on a language-specific attr (``("csharp",)``); None = every port.
ports: tuple[str, ...] | None = None
+ # The ports that implement this cross-port attr so far (``("java",)``); None = every port.
+ implemented_in: tuple[str, ...] | None = None
@dataclass(frozen=True)
@@ -87,6 +89,9 @@ class TypeDefinition:
example: str | None = None
when_to_use: str | None = None
parents: list[str] = field(default_factory=list)
+ # The ports that implement this cross-port type so far (``("typescript",)``);
+ # None = every port. Sourced from the embedded spec.
+ implemented_in: tuple[str, ...] | None = None
# Max children of this type.subType per parent (1 = singleton). Loader-enforced.
max_occurs: int | None = None
# Default name for a singleton (max_occurs==1) child declared with no name.
diff --git a/server/python/src/metaobjects/registry_manifest.py b/server/python/src/metaobjects/registry_manifest.py
index b4884b864b..efd5666851 100644
--- a/server/python/src/metaobjects/registry_manifest.py
+++ b/server/python/src/metaobjects/registry_manifest.py
@@ -31,9 +31,8 @@
ATTR_SUBTYPE_STRING,
ATTR_SUBTYPE_STRINGARRAY,
)
-from .meta.presentation.view.view_constants import VIEW_SUBTYPE_CURRENCY
from .registry import AttrSchema, ChildRule, TypeDefinition, TypeRegistry
-from .shared.base_types import SUBTYPE_BASE, TYPE_ATTR, TYPE_METADATA, TYPE_VIEW
+from .shared.base_types import SUBTYPE_BASE, TYPE_ATTR, TYPE_METADATA
from .shared.structural import KEY_IS_ARRAY
# Wave 3b — the in/out boundary is an EXPLICIT CLASSIFICATION (a reason category
@@ -53,16 +52,12 @@ class ExclusionReason(str, Enum):
#: Sentinel: NOT excluded — logical cross-port vocabulary.
INCLUDED = "included"
- #: Native type-binding / factory (incl. ADR-0001 ``object``, ADR-0005 ``objectAdapter``).
- NATIVE_BINDING = "native-binding"
#: Bare structural / OO-shape keyword (isArray/isAbstract/extends/implements/isInterface).
STRUCTURAL_KEYWORD = "structural-keyword"
#: A commonAttr (``description``) re-registered per-type — belongs in commonAttrs.
COMMON_ATTR_DUP = "common-attr-dup"
#: The ``metadata.base`` per-port inheritance anchor (deferred inheritsFrom facet).
INHERITANCE_ANCHOR = "inheritance-anchor"
- #: TS-web-presentation-only facet (the generic ``view.*`` controls).
- PRESENTATION_ONLY = "presentation-only"
#: FR-024 reference-first rollout slot (RETIRED at the atomic flip): the
#: ``object.projection`` row + the ``origin.aggregate.via`` required-override
#: were excluded until every port registered the FR-024 loader-grammar slice,
@@ -76,9 +71,6 @@ class ExclusionReason(str, Enum):
_ATTR_NAME_EXTENDS = "extends"
_ATTR_NAME_IMPLEMENTS = "implements"
_ATTR_NAME_IS_INTERFACE = "isInterface"
-# ADR-0001 class-FQN type binding + ADR-0005 hybrid value-access seam.
-_ATTR_NAME_OBJECT = "object"
-_ATTR_NAME_OBJECT_ADAPTER = "objectAdapter"
# Per-type attr names carved out of the agreed vocabulary, each mapped to its
# PORT_PRIVATE reason. An attr NOT in this map is logical (INCLUDED) by the
@@ -90,8 +82,6 @@ class ExclusionReason(str, Enum):
_ATTR_NAME_EXTENDS: ExclusionReason.STRUCTURAL_KEYWORD,
_ATTR_NAME_IMPLEMENTS: ExclusionReason.STRUCTURAL_KEYWORD,
_ATTR_NAME_IS_INTERFACE: ExclusionReason.STRUCTURAL_KEYWORD,
- _ATTR_NAME_OBJECT: ExclusionReason.NATIVE_BINDING,
- _ATTR_NAME_OBJECT_ADAPTER: ExclusionReason.NATIVE_BINDING,
DOC_ATTR_DESCRIPTION: ExclusionReason.COMMON_ATTR_DUP,
}
@@ -104,11 +94,10 @@ def classify_per_type_attr(name: str) -> ExclusionReason:
def classify_type_subtype(type_name: str, sub_type: str) -> ExclusionReason:
"""Classify a ``(type, subType)`` row: the metadata.base inheritance anchor
- (C-5) / the generic ``view.*`` presentation controls (B-2) / INCLUDED."""
+ (C-5) / INCLUDED. The generic ``view.*`` controls are NOT carved out: they are in
+ the manifest with ``implementedIn``."""
if type_name == TYPE_METADATA and sub_type == SUBTYPE_BASE:
return ExclusionReason.INHERITANCE_ANCHOR # C-5 — Java's internal inheritance anchor
- if type_name == TYPE_VIEW and sub_type not in (SUBTYPE_BASE, VIEW_SUBTYPE_CURRENCY):
- return ExclusionReason.PRESENTATION_ONLY # B-2 — TS-web-presentation generic view controls
return ExclusionReason.INCLUDED
@@ -152,7 +141,10 @@ def _to_manifest_attr(attr: AttrSchema) -> dict[str, object]:
# is stringified for a stable cross-port surface.
if attr.allowed_values is not None and len(attr.allowed_values) > 0:
out["allowedValues"] = [str(v) for v in attr.allowed_values]
- # The ports that act on a language-specific attr, sorted, only when named.
+ # The ports that implement a cross-port attr so far, then the ports that act on a
+ # language-specific attr; each sorted, only when named.
+ if attr.implemented_in:
+ out["implementedIn"] = sorted(attr.implemented_in)
if attr.ports:
out["ports"] = sorted(attr.ports)
out["description"] = attr.description
@@ -283,7 +275,7 @@ def build_registry_manifest(registry: TypeRegistry) -> dict[str, object]:
# walk, so dict iteration order is irrelevant.
for definition in registry._defs.values(): # noqa: SLF001 (no public iterator)
if _is_excluded_type_subtype(definition.type, definition.sub_type):
- continue # metadata.base anchor (C-5) / generic view.* controls (B-2)
+ continue # metadata.base anchor (C-5)
# Fixed TS key order: type, subType, description, optional
# rules/example/whenToUse (only when present), attrs, children, optional
# parents (only when non-empty + sorted ASCII). Insertion order on the
@@ -291,8 +283,11 @@ def build_registry_manifest(registry: TypeRegistry) -> dict[str, object]:
type_dict: dict[str, object] = {
"type": definition.type,
"subType": definition.sub_type,
- "description": definition.description,
}
+ # The ports that implement it so far, only when named.
+ if definition.implemented_in:
+ type_dict["implementedIn"] = sorted(definition.implemented_in)
+ type_dict["description"] = definition.description
if definition.rules is not None:
type_dict["rules"] = definition.rules
if definition.example is not None:
diff --git a/server/python/src/metaobjects/spec_metamodel/__init__.py b/server/python/src/metaobjects/spec_metamodel/__init__.py
index da9e705c7f..079ca88673 100644
--- a/server/python/src/metaobjects/spec_metamodel/__init__.py
+++ b/server/python/src/metaobjects/spec_metamodel/__init__.py
@@ -69,6 +69,12 @@
_BASE_SUBTYPE = "base"
+
+def _str_tuple(o: dict, key: str) -> tuple[str, ...] | None:
+ """A list of strings under ``key`` (``ports`` / ``implementedIn``), or None when absent."""
+ v = o.get(key)
+ return tuple(v) if v is not None else None
+
@dataclass(frozen=True)
class DocFacet:
"""One documented metamodel facet (a type/subType) — its description plus the
@@ -78,6 +84,8 @@ class DocFacet:
rules: str | None = None
example: str | None = None
when_to_use: str | None = None
+ # The ports that implement this cross-port type so far; None = every port.
+ implemented_in: tuple[str, ...] | None = None
@dataclass(frozen=True)
@@ -104,6 +112,8 @@ class AttrEntry:
default: object | None = None
# The ports that act on a language-specific attr; None = every port.
ports: tuple[str, ...] | None = None
+ # The ports that implement this cross-port attr so far; None = every port.
+ implemented_in: tuple[str, ...] | None = None
@property
def required(self) -> bool:
@@ -204,6 +214,7 @@ def _parse(self, provider: dict) -> None:
rules=t.get("rules"),
example=t.get("example"),
when_to_use=t.get("whenToUse"),
+ implemented_in=_str_tuple(t, "implementedIn"),
)
attrs = self._type_attr_docs.setdefault(key, {})
for a in self._parse_attr_children(t):
@@ -254,7 +265,8 @@ def _parse_attr_children(owner: dict) -> list[AttrEntry]:
when_to_use=c.get("whenToUse"),
allowed_values=tuple(allowed) if allowed is not None else None,
default=c.get("default"),
- ports=tuple(c["ports"]) if c.get("ports") is not None else None,
+ ports=_str_tuple(c, "ports"),
+ implemented_in=_str_tuple(c, "implementedIn"),
)
)
return out
@@ -489,6 +501,7 @@ def apply_spec_descriptions(registry) -> None:
definition.rules = facet.rules
definition.example = facet.example
definition.when_to_use = facet.when_to_use
+ definition.implemented_in = facet.implemented_in
new_attrs = []
for attr in definition.attrs:
@@ -502,6 +515,7 @@ def apply_spec_descriptions(registry) -> None:
example=a.example,
when_to_use=a.when_to_use,
ports=a.ports,
+ implemented_in=a.implemented_in,
)
)
else:
diff --git a/server/python/src/metaobjects/spec_metamodel/object.json b/server/python/src/metaobjects/spec_metamodel/object.json
index 7aa14f909e..f9d9280409 100644
--- a/server/python/src/metaobjects/spec_metamodel/object.json
+++ b/server/python/src/metaobjects/spec_metamodel/object.json
@@ -11,7 +11,9 @@
{ "type": "validator", "subType": "*", "name": "*", "min": 0, "max": null },
{ "type": "layout", "subType": "*", "name": "*", "min": 0, "max": null },
{ "type": "source", "subType": "*", "name": "*", "min": 0, "max": null },
- { "type": "index", "subType": "*", "name": "*", "min": 0, "max": null }
+ { "type": "index", "subType": "*", "name": "*", "min": 0, "max": null },
+ { "type": "attr", "subType": "string", "name": "object", "min": 0, "max": 1, "implementedIn": ["java"], "description": "The native class an object binds to (ADR-0001), in place of its default runtime class: on the JVM, a fully-qualified class name. Cross-port vocabulary only Java implements so far; every port loads and type-checks it, and the others do not act on it yet." },
+ { "type": "attr", "subType": "string", "name": "objectAdapter", "min": 0, "max": 1, "ports": ["java", "kotlin"], "description": "JVM-only (ADR-0005): the fully-qualified name of an ObjectAdapter that takes over value access for this object's instances. Every port loads and type-checks it; only Java and Kotlin act on it." }
]
},
{
diff --git a/server/python/src/metaobjects/spec_metamodel/view.json b/server/python/src/metaobjects/spec_metamodel/view.json
index d5e2ac9370..d839d9c431 100644
--- a/server/python/src/metaobjects/spec_metamodel/view.json
+++ b/server/python/src/metaobjects/spec_metamodel/view.json
@@ -9,61 +9,73 @@
{
"type": "view",
"subType": "text",
+ "implementedIn": ["typescript"],
"description": "Single-line text input/display."
},
{
"type": "view",
"subType": "textarea",
+ "implementedIn": ["typescript"],
"description": "Multi-line text area."
},
{
"type": "view",
"subType": "date",
+ "implementedIn": ["typescript"],
"description": "Date picker / date display."
},
{
"type": "view",
"subType": "month",
+ "implementedIn": ["typescript"],
"description": "Month picker / month display."
},
{
"type": "view",
"subType": "hotlink",
+ "implementedIn": ["typescript"],
"description": "Renders the value as a clickable link."
},
{
"type": "view",
"subType": "dropdown",
+ "implementedIn": ["typescript"],
"description": "Single-select dropdown control."
},
{
"type": "view",
"subType": "radio",
+ "implementedIn": ["typescript"],
"description": "Radio-button selection control."
},
{
"type": "view",
"subType": "checkbox",
+ "implementedIn": ["typescript"],
"description": "Checkbox selection control."
},
{
"type": "view",
"subType": "number",
+ "implementedIn": ["typescript"],
"description": "Numeric input control."
},
{
"type": "view",
"subType": "password",
+ "implementedIn": ["typescript"],
"description": "Masked input for sensitive values."
},
{
"type": "view",
"subType": "hidden",
+ "implementedIn": ["typescript"],
"description": "Not rendered; carried but not shown."
},
{
"type": "view",
"subType": "web",
+ "implementedIn": ["typescript"],
"description": "Abstract base for web-rendered views."
},
{
@@ -71,6 +83,6 @@
"subType": "currency",
"description": "Currency display formatting (locale-aware via @locale)."
},
- { "type": "view", "subType": "image", "description": "Image upload/display control; the field stores an opaque storage key (field.string)." }
+ { "type": "view", "subType": "image", "implementedIn": ["typescript"], "description": "Image upload/display control; the field stores an opaque storage key (field.string)." }
]
}
diff --git a/server/python/tests/conformance/conformance_adapter.py b/server/python/tests/conformance/conformance_adapter.py
index 5ab97f2e02..b5f4bc5375 100644
--- a/server/python/tests/conformance/conformance_adapter.py
+++ b/server/python/tests/conformance/conformance_adapter.py
@@ -12,6 +12,7 @@
from metaobjects.documentation import doc_provider
from metaobjects.meta.persistence.db.db_provider import db_provider
from metaobjects.meta.presentation.ui.ui_provider import ui_provider
+from metaobjects.meta.presentation.ui.ui_web_provider import ui_web_provider
from metaobjects.meta.template.prompt_provider import prompt_provider
from metaobjects.errors import ErrorCode, ParseError
from metaobjects.meta.meta_data import MetaData
@@ -126,6 +127,8 @@ def _noop_provider(provider_id: str, *deps: str) -> Provider:
# The UI / query-surface domain provider — registers @filterable / @sortable /
# @sortableDefaultOrder (field), view.currency @locale, layout.dataGrid attrs.
ui_provider.id: ui_provider, # "metaobjects-ui"
+ # view.textarea @rows and the view.image attrs (ui-web.json).
+ ui_web_provider.id: ui_web_provider, # "metaobjects-ui-web"
# Back-compat alias: pre-rename fixtures (and other ports' fixtures) still name the
# provider "metaobjects-template"; map it to the renamed prompt provider — the same
# alias the TS adapter keeps (adapter.ts: "metaobjects-template": promptProvider).
diff --git a/server/python/tests/conformance/fixture_discovery.py b/server/python/tests/conformance/fixture_discovery.py
index 3d5aa58187..5e984ade58 100644
--- a/server/python/tests/conformance/fixture_discovery.py
+++ b/server/python/tests/conformance/fixture_discovery.py
@@ -22,6 +22,8 @@
"metaobjects-documentation",
"metaobjects-prompt",
"metaobjects-ui",
+ # view.textarea @rows and the view.image attrs are cross-port vocabulary.
+ "metaobjects-ui-web",
)
diff --git a/server/python/tests/conformance/test_registry_conformance.py b/server/python/tests/conformance/test_registry_conformance.py
index 05d5724864..e5cac837fc 100644
--- a/server/python/tests/conformance/test_registry_conformance.py
+++ b/server/python/tests/conformance/test_registry_conformance.py
@@ -68,13 +68,15 @@ def test_every_registered_per_type_attr_is_explicitly_classified() -> None:
def test_carve_outs_carry_their_declared_reason_category() -> None:
assert classify_per_type_attr("extends") is ExclusionReason.STRUCTURAL_KEYWORD
assert classify_per_type_attr("isArray") is ExclusionReason.STRUCTURAL_KEYWORD
- assert classify_per_type_attr("object") is ExclusionReason.NATIVE_BINDING
- assert classify_per_type_attr("objectAdapter") is ExclusionReason.NATIVE_BINDING
+ # @object / @objectAdapter are in the manifest (implementedIn / ports), not carved out.
+ assert classify_per_type_attr("object") is ExclusionReason.INCLUDED
+ assert classify_per_type_attr("objectAdapter") is ExclusionReason.INCLUDED
assert classify_per_type_attr("description") is ExclusionReason.COMMON_ATTR_DUP
# Genuinely logical attrs are INCLUDED.
assert classify_per_type_attr("column") is ExclusionReason.INCLUDED
assert classify_per_type_attr("maxLength") is ExclusionReason.INCLUDED
# Row classification.
assert classify_type_subtype("metadata", "base") is ExclusionReason.INHERITANCE_ANCHOR
- assert classify_type_subtype("view", "dropdown") is ExclusionReason.PRESENTATION_ONLY
+ # The generic view controls are in the manifest, marked implementedIn.
+ assert classify_type_subtype("view", "dropdown") is ExclusionReason.INCLUDED
assert classify_type_subtype("view", "currency") is ExclusionReason.INCLUDED
diff --git a/server/typescript/packages/cli/src/commands/types.ts b/server/typescript/packages/cli/src/commands/types.ts
index 41ab0d69c3..2cb478a912 100644
--- a/server/typescript/packages/cli/src/commands/types.ts
+++ b/server/typescript/packages/cli/src/commands/types.ts
@@ -71,9 +71,10 @@ construct.
--format toon / --format json emit ONE machine-readable document and nothing else. It
carries every match with its full record, so --limit, --detail and --no-headers — all
-three TEXT display controls — do not change it. The terse line's [base] / [ts-only]
-markers are the sharedRoot / tsOnly fields there, a closed-enum attr carries its
-allowedValues, and no match is an empty matches list rather than a prose hint.`;
+three TEXT display controls — do not change it. The terse line's [base] / [ts; pending
+elsewhere] / [-only] markers are the sharedRoot / implementedIn / ports fields there
+(the 1.2 field tsOnly is still emitted, derived from implementedIn; it is deprecated and goes
+in 1.4), a closed-enum attr carries its allowedValues, and no match is an empty matches list rather than a prose hint.`;
/** The flags `parse` below accepts, for the unknown-flag refusal. */
const TYPES_FLAGS: readonly string[] = ["--all", "--desc", "--detail", "--no-headers", "--limit", "--type", "--kind"];
@@ -130,8 +131,11 @@ interface Entry {
attrNames: string[]; // attr-name hints (for subtype terse lines)
/** Accepted on EVERY node, so no `--type` scope excludes it (#357). */
common: boolean;
- /** Registered here but carved out of the cross-port manifest — TS-only vocabulary. */
- tsOnly: boolean;
+ /**
+ * The ports that implement it so far (`["typescript"]`); absent = every
+ * port. An attr carries its owning type's. Every port loads it strictly either way.
+ */
+ implementedIn?: readonly string[];
/** A type's shared root (`.base`): its attrs apply to every subtype. */
sharedRoot: boolean;
/** An attr entry: the id of the provider that registered it (ADR-0062). */
@@ -142,11 +146,32 @@ interface Entry {
}
/** Compact markers appended to a terse line, and the legend that explains them. */
-const MARK_TS_ONLY = "[ts-only]";
const MARK_BASE = "[base]";
const LEGEND = ` ${MARK_BASE} the type's shared root — its @attrs apply to every subtype of that type\n` +
- ` ${MARK_TS_ONLY} registered in TypeScript only; the cross-port metamodel contract does not carry it\n` +
- ` [-only] every port loads and type-checks it; only acts on it`;
+ ` [; pending elsewhere] cross-port vocabulary only implements so far (e.g. [ts; …]); every port loads and type-checks it, the others do not act on it yet\n` +
+ ` [-only] every port loads and type-checks it; only acts on it ([java+kotlin]: only those ports)`;
+
+/** The short label a port name takes in a marker. */
+function portLabel(port: string): string {
+ return port === "typescript" ? "ts" : port;
+}
+
+/** `[ts; pending elsewhere]` for vocabulary only TypeScript implements so far. */
+function implementedInMark(implementedIn: readonly string[] | undefined): string {
+ if (implementedIn === undefined || implementedIn.length === 0) return "";
+ return `[${implementedIn.map(portLabel).join("+")}; pending elsewhere]`;
+}
+
+/** The deprecated JSON `tsOnly`: true when TypeScript is the only port that implements it. */
+function isTsOnly(implementedIn: readonly string[] | undefined): boolean {
+ return implementedIn !== undefined && implementedIn.length === 1 && implementedIn[0] === "typescript";
+}
+
+/** The --detail sentence for the same thing. */
+function implementedInSentence(implementedIn: readonly string[]): string {
+ return ` Implemented in ${implementedIn.join(", ")} so far; pending in the other ports. ` +
+ "Every port loads and type-checks it.";
+}
/** `[csharp-only]` for an attr only C# acts on; `[csharp+java]` style for several. */
function portsMark(ports: readonly string[] | undefined): string {
@@ -155,7 +180,7 @@ function portsMark(ports: readonly string[] | undefined): string {
}
function marks(e: Entry): string {
- const m = [e.sharedRoot ? MARK_BASE : "", e.tsOnly ? MARK_TS_ONLY : "", portsMark(e.ports)].filter(Boolean);
+ const m = [e.sharedRoot ? MARK_BASE : "", implementedInMark(e.implementedIn), portsMark(e.ports)].filter(Boolean);
return m.length > 0 ? ` ${m.join(" ")}` : "";
}
@@ -167,6 +192,7 @@ interface RawAttr {
required: boolean;
allowedValues?: readonly string[];
ports?: readonly string[];
+ implementedIn?: readonly string[];
description: string;
whenToUse?: string;
}
@@ -179,6 +205,7 @@ function attrRecord(at: RawAttr, provider?: string): Record {
isArray: at.isArray,
required: at.required,
...(at.allowedValues !== undefined ? { allowedValues: at.allowedValues } : {}),
+ ...(at.implementedIn !== undefined ? { implementedIn: at.implementedIn } : {}),
...(at.ports !== undefined ? { ports: at.ports } : {}),
description: at.description,
...(at.whenToUse !== undefined ? { whenToUse: at.whenToUse } : {}),
@@ -195,7 +222,10 @@ function payloadMatch(e: Entry): Record {
type: e.common || e.type === "" ? null : e.type,
owner: e.common ? null : e.owner,
common: e.common,
- tsOnly: e.tsOnly,
+ // Deprecated: 1.2 carried `tsOnly`, and 1.3 keeps it, derived from `implementedIn`, so a
+ // consumer reading it keeps working. Read `implementedIn` instead; `tsOnly` goes in 1.4.
+ tsOnly: isTsOnly(e.implementedIn),
+ ...(e.implementedIn !== undefined ? { implementedIn: e.implementedIn } : {}),
sharedRoot: e.sharedRoot,
...(e.ports !== undefined ? { ports: e.ports } : {}),
description: e.description,
@@ -297,14 +327,18 @@ export async function typesCommand(args: string[], fmt: OutputFormat = "text"):
kind: "subtype", name: tsName, owner: tsName, type: mt.type,
description: mt.description, whenToUse: mt.whenToUse,
attrNames: mt.attrs.map((a) => `@${a.name}`),
- common: false, tsOnly: !mt.crossPort, sharedRoot: mt.sharedRoot, provider: undefined, raw: mt,
+ common: false, sharedRoot: mt.sharedRoot,
+ ...(mt.implementedIn !== undefined ? { implementedIn: mt.implementedIn } : {}), provider: undefined, raw: mt,
});
for (const at of mt.attrs) {
entries.push({
kind: "attr", name: `${tsName} @${at.name}`, owner: tsName, type: mt.type,
description: at.description, whenToUse: at.whenToUse, attrNames: [],
- common: false, tsOnly: !mt.crossPort, sharedRoot: false, provider: mt.attrProviders[at.name],
- ...((at as { ports?: readonly string[] }).ports !== undefined ? { ports: (at as { ports?: readonly string[] }).ports } : {}), raw: at,
+ common: false, sharedRoot: false, provider: mt.attrProviders[at.name],
+ // An attr's own kind-1 marking, else its owning type's.
+ ...((at.implementedIn ?? mt.implementedIn) !== undefined
+ ? { implementedIn: at.implementedIn ?? mt.implementedIn } : {}),
+ ...(at.ports !== undefined ? { ports: at.ports } : {}), raw: at,
});
}
}
@@ -317,7 +351,7 @@ export async function typesCommand(args: string[], fmt: OutputFormat = "text"):
entries.push({
kind: "attr", name: `@${at.name}`, owner: "(any node)", type: "",
description: at.description, whenToUse: at.whenToUse, attrNames: [],
- common: true, tsOnly: false, sharedRoot: false, provider: catalog.commonAttrProviders[at.name], raw: at,
+ common: true, sharedRoot: false, provider: catalog.commonAttrProviders[at.name], raw: at,
});
}
@@ -373,8 +407,8 @@ export async function typesCommand(args: string[], fmt: OutputFormat = "text"):
if (e.description) log.info(` ${e.description}`);
if (e.sharedRoot)
log.info(` shared root: @attrs registered here apply to every ${e.type}.* subtype.`);
- if (e.tsOnly)
- log.info(" TypeScript-only: registered here, but not part of the cross-port metamodel contract.");
+ if (e.implementedIn !== undefined && e.implementedIn.length > 0)
+ log.info(implementedInSentence(e.implementedIn));
if (e.whenToUse) log.info(` → reach for it when: ${e.whenToUse}`);
if (e.kind === "subtype") {
const mt = e.raw as {
diff --git a/server/typescript/packages/cli/test/types-command.test.ts b/server/typescript/packages/cli/test/types-command.test.ts
index 47d7b153ad..ed8ad15ea3 100644
--- a/server/typescript/packages/cli/test/types-command.test.ts
+++ b/server/typescript/packages/cli/test/types-command.test.ts
@@ -69,12 +69,13 @@ describe("#357 every registered view subtype is findable", () => {
expect(await run(["view.bogusxyz"])).toContain("No vocabulary matches");
});
- test("TS-only vocabulary is marked rather than hidden", async () => {
+ test("kind-1 vocabulary is marked as pending elsewhere, not as outside the contract", async () => {
const out = await run(["--type", "view", "--kind", "subtype", "--limit", "0"]);
- // `view.currency` is in the cross-port contract; `view.text` is not.
- expect(out).toMatch(/view\.text .*\[ts-only\]/);
- expect(out).not.toMatch(/view\.currency .*\[ts-only\]/);
- expect(out).toContain("cross-port metamodel contract does not carry it");
+ // Both are cross-port; only TypeScript implements `view.text` so far.
+ expect(out).toMatch(/view\.text .*\[ts; pending elsewhere\]/);
+ expect(out).not.toMatch(/view\.currency .*pending elsewhere/);
+ expect(out).toContain("every port loads and type-checks it, the others do not act on it yet");
+ expect(out).not.toContain("does not carry it");
});
test("a type's shared root is marked, so the concrete list is not mistaken for it", async () => {
@@ -232,11 +233,33 @@ describe("meta types --format", () => {
test("the legend's markers become FIELDS, not text appended to a name", async () => {
const doc = await json(["--type", "view", "--kind", "subtype", "--limit", "0"]);
const byName = (n: string) => doc.matches.find((m: any) => m.name === n);
+ // Cross-port vocabulary only TypeScript implements so far.
+ expect(byName("view.text").implementedIn).toEqual(["typescript"]);
+ expect(byName("view.currency").implementedIn).toBeUndefined();
+ expect(byName("view.base").sharedRoot).toBe(true);
+ // `tsOnly` is deprecated: still emitted through 1.3.x, derived from implementedIn.
expect(byName("view.text").tsOnly).toBe(true);
expect(byName("view.currency").tsOnly).toBe(false);
- expect(byName("view.base").sharedRoot).toBe(true);
// And no row smuggles the text rendering's markers into its name.
- for (const m of doc.matches) expect(m.name).not.toContain("[ts-only]");
+ for (const m of doc.matches) expect(m.name).not.toContain("pending elsewhere");
+ });
+
+ test("kind-1 and kind-2 vocabulary are marked in text and carried in JSON", async () => {
+ const text = await run(["object.entity", "--limit", "0"]);
+ expect(text).toContain("[java; pending elsewhere]");
+ expect(text).toContain("[java+kotlin]");
+ expect(text).not.toContain("[java-only]");
+ expect(text).toContain("[csharp-only]");
+ expect(text).not.toContain("[ts-only]");
+ const doc = await json(["object.entity", "--kind", "attr", "--limit", "0"]);
+ const attr = (n: string) => doc.matches.find((m: any) => m.name === `object.entity @${n}`);
+ expect(attr("object").implementedIn).toEqual(["java"]);
+ // Implemented in Java only, so the deprecated derived `tsOnly` is false.
+ expect(attr("object").tsOnly).toBe(false);
+ expect(attr("objectAdapter").ports).toEqual(["java", "kotlin"]);
+ // A view's attr inherits its type's kind-1 marking.
+ const img = await json(["view.image", "--kind", "attr", "--limit", "0"]);
+ expect(img.matches.find((m: any) => m.name === "view.image @accept").implementedIn).toEqual(["typescript"]);
});
test("a closed-enum attr carries its allowed values", async () => {
diff --git a/server/typescript/packages/conformance/src/fixture.ts b/server/typescript/packages/conformance/src/fixture.ts
index a074a0d750..a7e300d62a 100644
--- a/server/typescript/packages/conformance/src/fixture.ts
+++ b/server/typescript/packages/conformance/src/fixture.ts
@@ -56,6 +56,8 @@ const DEFAULT_PROVIDERS = [
"metaobjects-documentation",
"metaobjects-prompt",
"metaobjects-ui",
+ // View.textarea @rows and the view.image attrs are cross-port vocabulary.
+ "metaobjects-ui-web",
];
export interface Fixture {
diff --git a/server/typescript/packages/metadata/src/core/object/object-definition.embedded.ts b/server/typescript/packages/metadata/src/core/object/object-definition.embedded.ts
index 8dc6d20b68..5a5ca8d266 100644
--- a/server/typescript/packages/metadata/src/core/object/object-definition.embedded.ts
+++ b/server/typescript/packages/metadata/src/core/object/object-definition.embedded.ts
@@ -55,6 +55,29 @@ export const OBJECT_DEFINITION: ProviderDefinition = {
"name": "*",
"min": 0,
"max": null
+ },
+ {
+ "type": "attr",
+ "subType": "string",
+ "name": "object",
+ "min": 0,
+ "max": 1,
+ "implementedIn": [
+ "java"
+ ],
+ "description": "The native class an object binds to (ADR-0001), in place of its default runtime class: on the JVM, a fully-qualified class name. Cross-port vocabulary only Java implements so far; every port loads and type-checks it, and the others do not act on it yet."
+ },
+ {
+ "type": "attr",
+ "subType": "string",
+ "name": "objectAdapter",
+ "min": 0,
+ "max": 1,
+ "ports": [
+ "java",
+ "kotlin"
+ ],
+ "description": "JVM-only (ADR-0005): the fully-qualified name of an ObjectAdapter that takes over value access for this object's instances. Every port loads and type-checks it; only Java and Kotlin act on it."
}
]
},
diff --git a/server/typescript/packages/metadata/src/metamodel-docs/render.ts b/server/typescript/packages/metadata/src/metamodel-docs/render.ts
index 87a974ba47..97a3f1a12d 100644
--- a/server/typescript/packages/metadata/src/metamodel-docs/render.ts
+++ b/server/typescript/packages/metadata/src/metamodel-docs/render.ts
@@ -153,6 +153,11 @@ function subtypeSection(
const parts: string[] = [`### ${type}.${subType}\n`];
parts.push(`${def.description}\n`);
if (owner !== undefined) parts.push(`**Owning provider:** ${owner}\n`);
+ if (def.implementedIn !== undefined && def.implementedIn.length > 0) {
+ // Cross-port vocabulary only these ports implement so far.
+ const ports = [...def.implementedIn].sort(compare).join(", ");
+ parts.push(`**Implemented in:** ${ports} so far — every port loads and type-checks it; the others do not act on it yet.\n`);
+ }
if (def.rules !== undefined) parts.push(`**Rules:** ${def.rules}\n`);
if (def.whenToUse !== undefined) parts.push(`**When to use:** ${def.whenToUse}\n`);
if (def.example !== undefined) parts.push(`**Example:**\n\n\`\`\`\n${def.example}\n\`\`\`\n`);
@@ -173,8 +178,8 @@ function subtypeSection(
/**
* The registered (type, subType) definitions the metamodel docs cover — every
* registered pair EXCEPT the ones the registry-conformance manifest carves out
- * (TS-web-presentation `view.*` controls + the `metadata.base` inheritance
- * anchor) so the docs describe exactly the agreed cross-port vocabulary.
+ * (the `metadata.base` inheritance anchor) so the docs describe exactly the agreed
+ * cross-port vocabulary. Kind-1 rows are covered and marked "Implemented in".
*/
function coveredDefs(registry: TypeRegistry): TypeDefinition[] {
return registry
diff --git a/server/typescript/packages/metadata/src/presentation/ui-web/ui-web-provider.ts b/server/typescript/packages/metadata/src/presentation/ui-web/ui-web-provider.ts
index 1786d519ae..a6e841e547 100644
--- a/server/typescript/packages/metadata/src/presentation/ui-web/ui-web-provider.ts
+++ b/server/typescript/packages/metadata/src/presentation/ui-web/ui-web-provider.ts
@@ -1,12 +1,13 @@
-// uiWebProvider — TS-web presentation view attrs. Extends view.textarea with
+// uiWebProvider — web presentation view attrs. Extends view.textarea with
// @rows and registers the view.image control's five attrs
-// (@aspectRatio/@maxEdge/@store/@accept/@maxBytes). These are TS-web
-// presentation-only concerns, NOT core view properties, so they live here —
-// mirroring the uiProvider / dbProvider / promptProvider pattern.
+// (@aspectRatio/@maxEdge/@store/@accept/@maxBytes). These are presentation
+// concerns, NOT core view properties, so they live here — mirroring the
+// uiProvider / dbProvider / promptProvider pattern.
//
-// The non-TS ports commit a byte-identical mirror of spec/metamodel/ui-web.json
-// (Step 4) for the embed drift gates, but NEVER apply this provider — the
-// view.textarea/view.image control attrs are TS-web presentation-only.
+// This is cross-port vocabulary that only TypeScript
+// implements so far (view.json marks the types `implementedIn: ["typescript"]`).
+// Every port applies this provider, so every port loads these attrs strictly
+// and ignores them; an unregistered attr on these views still fails.
//
// FR-033-style: the attrs + their descriptions are DATA — read from
// spec/metamodel/ui-web.json (embedded as UI_WEB_DEFINITION) via the unified
@@ -22,7 +23,7 @@ export const uiWebProvider: MetaDataTypeProvider = {
id: "metaobjects-ui-web",
dependencies: ["metaobjects-core-types"],
description:
- "TS-web presentation view attrs — @rows (view.textarea) and the view.image control attrs. Applied only in TypeScript; the non-TS ports mirror the spec file but never apply this provider (the view subtypes are TS-web presentation-only).",
+ "Web presentation view attrs — @rows (view.textarea) and the view.image control attrs. Every port applies this provider and loads them strictly; only TypeScript acts on them so far.",
registerTypes(registry: TypeRegistry): void {
applyProviderDefinition(registry, UI_WEB_DEFINITION, {});
},
diff --git a/server/typescript/packages/metadata/src/presentation/view/view-definition.embedded.ts b/server/typescript/packages/metadata/src/presentation/view/view-definition.embedded.ts
index 388c76aa7f..c5bdaa7ef2 100644
--- a/server/typescript/packages/metadata/src/presentation/view/view-definition.embedded.ts
+++ b/server/typescript/packages/metadata/src/presentation/view/view-definition.embedded.ts
@@ -17,61 +17,97 @@ export const VIEW_DEFINITION: ProviderDefinition = {
{
"type": "view",
"subType": "text",
+ "implementedIn": [
+ "typescript"
+ ],
"description": "Single-line text input/display."
},
{
"type": "view",
"subType": "textarea",
+ "implementedIn": [
+ "typescript"
+ ],
"description": "Multi-line text area."
},
{
"type": "view",
"subType": "date",
+ "implementedIn": [
+ "typescript"
+ ],
"description": "Date picker / date display."
},
{
"type": "view",
"subType": "month",
+ "implementedIn": [
+ "typescript"
+ ],
"description": "Month picker / month display."
},
{
"type": "view",
"subType": "hotlink",
+ "implementedIn": [
+ "typescript"
+ ],
"description": "Renders the value as a clickable link."
},
{
"type": "view",
"subType": "dropdown",
+ "implementedIn": [
+ "typescript"
+ ],
"description": "Single-select dropdown control."
},
{
"type": "view",
"subType": "radio",
+ "implementedIn": [
+ "typescript"
+ ],
"description": "Radio-button selection control."
},
{
"type": "view",
"subType": "checkbox",
+ "implementedIn": [
+ "typescript"
+ ],
"description": "Checkbox selection control."
},
{
"type": "view",
"subType": "number",
+ "implementedIn": [
+ "typescript"
+ ],
"description": "Numeric input control."
},
{
"type": "view",
"subType": "password",
+ "implementedIn": [
+ "typescript"
+ ],
"description": "Masked input for sensitive values."
},
{
"type": "view",
"subType": "hidden",
+ "implementedIn": [
+ "typescript"
+ ],
"description": "Not rendered; carried but not shown."
},
{
"type": "view",
"subType": "web",
+ "implementedIn": [
+ "typescript"
+ ],
"description": "Abstract base for web-rendered views."
},
{
@@ -82,6 +118,9 @@ export const VIEW_DEFINITION: ProviderDefinition = {
{
"type": "view",
"subType": "image",
+ "implementedIn": [
+ "typescript"
+ ],
"description": "Image upload/display control; the field stores an opaque storage key (field.string)."
}
]
diff --git a/server/typescript/packages/metadata/src/provider-data.ts b/server/typescript/packages/metadata/src/provider-data.ts
index d387a134ac..62afcb0b64 100644
--- a/server/typescript/packages/metadata/src/provider-data.ts
+++ b/server/typescript/packages/metadata/src/provider-data.ts
@@ -70,6 +70,8 @@ export interface ChildDef {
isArray?: boolean;
/** The ports that act on a language-specific attr (`["csharp"]`); absent = every port. */
ports?: readonly string[];
+ /** The ports that implement this cross-port attr so far (`["java"]`); absent = every port. */
+ implementedIn?: readonly string[];
}
/**
@@ -85,6 +87,12 @@ export interface TypeDef {
rules?: string;
example?: string;
whenToUse?: string;
+ /**
+ * The ports that IMPLEMENT this type so far (`["typescript"]`), when it
+ * is cross-port vocabulary the other ports have not implemented yet. Every port
+ * registers it and loads it strictly; the rest ignore it. Absent = every port.
+ */
+ implementedIn?: readonly string[];
/** Unified children list: attrs (`type: "attr"`) + structural children. */
children?: ChildDef[];
/** Child-side placement: the `type.subType`s under which this type is allowed. */
@@ -257,6 +265,7 @@ export function defineProviderFromData(
...(t.rules !== undefined ? { rules: t.rules } : {}),
...(t.example !== undefined ? { example: t.example } : {}),
...(t.whenToUse !== undefined ? { whenToUse: t.whenToUse } : {}),
+ ...(t.implementedIn !== undefined ? { implementedIn: t.implementedIn } : {}),
...(t.maxOccurs !== undefined ? { maxOccurs: t.maxOccurs } : {}),
...(t.defaultName !== undefined ? { defaultName: t.defaultName } : {}),
};
@@ -444,6 +453,7 @@ function toAttrSchema(provider: string, typeKey: string, child: ChildDef): AttrS
? { allowedValues: child.allowedValues as readonly AttrValue[] }
: {}),
...(child.ports !== undefined ? { ports: child.ports } : {}),
+ ...(child.implementedIn !== undefined ? { implementedIn: child.implementedIn } : {}),
description: child.description ?? "",
...(child.rules !== undefined ? { rules: child.rules } : {}),
...(child.example !== undefined ? { example: child.example } : {}),
diff --git a/server/typescript/packages/metadata/src/registry-manifest-exclusions.ts b/server/typescript/packages/metadata/src/registry-manifest-exclusions.ts
index 47e71ce21c..1a5fea331c 100644
--- a/server/typescript/packages/metadata/src/registry-manifest-exclusions.ts
+++ b/server/typescript/packages/metadata/src/registry-manifest-exclusions.ts
@@ -21,9 +21,6 @@
// port-PRIVATE-mechanism, not abstract-vs-physical.
//
// • PORT_PRIVATE — a per-port mechanism, NOT cross-port vocabulary:
-// - NATIVE_BINDING factories / native type classes / the ADR-0001
-// `object` class-FQN binding + ADR-0005 `objectAdapter`
-// value-access seam (OO-port runtime mechanisms).
// - STRUCTURAL_KEYWORD bare body keywords (`isArray`/`isAbstract`/
// `extends`/`implements`/`isInterface`) — the
// structural/OO-shape spine, peers of name/children,
@@ -33,13 +30,15 @@
// - INHERITANCE_ANCHOR the `metadata.base` per-port abstract anchor (the
// deferred `inheritsFrom` facet; other ports register
// only the concrete `metadata.root`).
-// - PRESENTATION_ONLY the 13 generic `view.*` entries — 12 controls
-// (checkbox/date/…) plus the `web` abstract base
-// — a TS-web presentation facet (TS web client + TS
-// form codegen consume them; zero backend/codegen/
-// render consumers in any port), like the TS-only `D1`
-// dialect. REGISTERED in every port, for LOADING
-// only; EXCLUDED from the manifest everywhere.
+//
+// NOT a carve-out: vocabulary only SOME ports act on is still
+// cross-port vocabulary, and it is IN the manifest, marked rather than removed.
+// • kind 1 — implemented so far only in some ports (the 13 generic `view.*`
+// entries and their attrs): the type carries `implementedIn: ["typescript"]`.
+// • kind 2 — language-specific by nature (`@dbSetName`, the JVM's `@object` /
+// `@objectAdapter`): the attr carries `ports: [...]`.
+// Every port registers both kinds and loads them strictly; a port not named loads
+// them and does nothing with them. An unregistered name still fails.
//
// The boundary is now an EXPLICIT classification (not a silent default): the
// port-private set is enumerated WITH a reason category, and a TRIPWIRE
@@ -69,21 +68,16 @@
import { RESERVED_KEY_IS_ARRAY, RESERVED_KEY_EXTENDS } from "./shared/structural.js";
import { DOC_ATTR_DESCRIPTION } from "./core/documentation/doc-constants.js";
-import { SUBTYPE_BASE, TYPE_METADATA, TYPE_VIEW } from "./shared/base-types.js";
-import { VIEW_SUBTYPE_CURRENCY } from "./presentation/view/view-constants.js";
+import { SUBTYPE_BASE, TYPE_METADATA } from "./shared/base-types.js";
/** The reason an attr/row is classified PORT_PRIVATE (carved out of the agreed vocabulary). */
export enum ExclusionReason {
- /** Native type-binding / factory mechanism (incl. ADR-0001 `object`, ADR-0005 `objectAdapter`). */
- NativeBinding = "native-binding",
/** Bare structural / OO-shape keyword (`isArray`/`isAbstract`/`extends`/`implements`/`isInterface`). */
StructuralKeyword = "structural-keyword",
/** A `commonAttr` (`description`) re-registered per-type — belongs in the commonAttrs block. */
CommonAttrDup = "common-attr-dup",
/** The `metadata.base` per-port inheritance anchor (deferred `inheritsFrom` facet). */
InheritanceAnchor = "inheritance-anchor",
- /** TS-web-presentation-only facet (the generic `view.*` controls). */
- PresentationOnly = "presentation-only",
/**
* FR-024 TS-reference-first rollout (RETIRED at the atomic flip): new
* vocabulary genuinely registered (and gated by TS tests) but carved out of
@@ -103,9 +97,6 @@ const ATTR_NAME_IS_ABSTRACT = "isAbstract";
/** Java-OO structural-shape keywords as per-type attr names (the OO modeling spine). */
const ATTR_NAME_IMPLEMENTS = "implements";
const ATTR_NAME_IS_INTERFACE = "isInterface";
-/** ADR-0001 class-FQN type binding + ADR-0005 hybrid value-access seam (per-port runtime mechanisms). */
-const ATTR_NAME_OBJECT = "object";
-const ATTR_NAME_OBJECT_ADAPTER = "objectAdapter";
/**
* The per-type attr names carved OUT of the agreed vocabulary, each mapped to
@@ -120,16 +111,14 @@ export const EXCLUDED_PER_TYPE_ATTRS: ReadonlyMap = new
[RESERVED_KEY_EXTENDS, ExclusionReason.StructuralKeyword],
[ATTR_NAME_IMPLEMENTS, ExclusionReason.StructuralKeyword],
[ATTR_NAME_IS_INTERFACE, ExclusionReason.StructuralKeyword],
- [ATTR_NAME_OBJECT, ExclusionReason.NativeBinding],
- [ATTR_NAME_OBJECT_ADAPTER, ExclusionReason.NativeBinding],
[DOC_ATTR_DESCRIPTION, ExclusionReason.CommonAttrDup],
]);
/**
* `(type, subType)` rows carved OUT of the agreed vocabulary, with reason.
- * `metadata.base` is the per-port inheritance anchor (INHERITANCE_ANCHOR); the
- * 13 generic `view.*` entries — 12 controls plus the `web` abstract base — are
- * TS-web-presentation-only (PRESENTATION_ONLY).
+ * `metadata.base` is the per-port inheritance anchor (INHERITANCE_ANCHOR). The
+ * generic `view.*` controls are NOT carved out: they are kind-1 vocabulary, IN the
+ * manifest with `implementedIn`.
* Returns the reason, or `undefined` for an INCLUDED (logical) row.
*/
export function classifyTypeSubType(
@@ -139,9 +128,6 @@ export function classifyTypeSubType(
if (type === TYPE_METADATA && subType === SUBTYPE_BASE) {
return ExclusionReason.InheritanceAnchor; // C-5 — Java's internal inheritance anchor
}
- if (type === TYPE_VIEW && subType !== SUBTYPE_BASE && subType !== VIEW_SUBTYPE_CURRENCY) {
- return ExclusionReason.PresentationOnly; // B-2 — TS-web-presentation generic view controls
- }
return undefined;
}
diff --git a/server/typescript/packages/metadata/src/registry-manifest.ts b/server/typescript/packages/metadata/src/registry-manifest.ts
index aa182a17df..a26883f51b 100644
--- a/server/typescript/packages/metadata/src/registry-manifest.ts
+++ b/server/typescript/packages/metadata/src/registry-manifest.ts
@@ -52,6 +52,8 @@ export interface ManifestAttr {
* value-set drifting between ports (the failure mode the dbColumnType slim hit).
*/
allowedValues?: readonly string[];
+ /** The ports that implement this cross-port attr so far, sorted. Emitted ONLY when named. */
+ implementedIn?: readonly string[];
/** The ports that act on a language-specific attr, sorted. Emitted ONLY when the attr names them. */
ports?: readonly string[];
/** FR-033 — human/AI-facing description of the attribute (required, non-empty). */
@@ -84,6 +86,11 @@ export interface ManifestChild {
export interface ManifestType {
type: string;
subType: string;
+ /**
+ * The ports that implement this type so far, sorted. Emitted ONLY when
+ * the type names them; absent = every port implements it.
+ */
+ implementedIn?: readonly string[];
/** FR-033 — human/AI-facing description of the type/subType (required, non-empty). */
description: string;
/** FR-033 — prose documenting the complex rules enforced in code. Emitted only when present. */
@@ -163,7 +170,10 @@ function toManifestAttr(attr: AttrSchema): ManifestAttr {
isArray,
required: attr.required,
...(hasAllowed ? { allowedValues: attr.allowedValues!.map((v) => String(v)) } : {}),
- ...(attr.ports !== undefined && attr.ports.length > 0 ? { ports: [...attr.ports].sort() } : {}),
+ ...(attr.implementedIn !== undefined && attr.implementedIn.length > 0
+ ? { implementedIn: [...attr.implementedIn].sort(compareStrings) }
+ : {}),
+ ...(attr.ports !== undefined && attr.ports.length > 0 ? { ports: [...attr.ports].sort(compareStrings) } : {}),
description: attr.description,
};
if (attr.rules !== undefined) out.rules = attr.rules;
@@ -285,6 +295,9 @@ function toManifestType(def: TypeDefinition): ManifestType {
const out: ManifestType = {
type: def.typeId.type,
subType: def.typeId.subType,
+ ...(def.implementedIn !== undefined && def.implementedIn.length > 0
+ ? { implementedIn: [...def.implementedIn].sort(compareStrings) }
+ : {}),
description: def.description,
...(def.rules !== undefined ? { rules: def.rules } : {}),
...(def.example !== undefined ? { example: def.example } : {}),
@@ -311,7 +324,7 @@ export function buildRegistryManifest(registry: TypeRegistry): RegistryManifest
const types: ManifestType[] = registry
.allTypes()
// Skip excluded (type, subType) rows: the `metadata.base` inheritance
- // anchor (C-5) + the generic TS-presentation `view.*` controls (B-2).
+ // anchor (C-5).
.filter((typeId) => !isExcludedTypeSubType(typeId.type, typeId.subType))
.map((typeId) =>
toManifestType(
@@ -353,9 +366,11 @@ export function buildRegistryManifest(registry: TypeRegistry): RegistryManifest
* header), then `types`, `commonAttrs`, `defaultSubTypes`.
* - Each attr: `name`, `valueType`, `isArray`, `required`, then optional
* `allowedValues` (omitted unless the attr declares a non-empty closed set —
- * decision 5), then `description`, then optional `rules`, `example`,
+ * decision 5), then optional `implementedIn` and `ports`,
+ * each sorted, then `description`, then optional `rules`, `example`,
* `whenToUse` (each omitted when absent).
- * - Each type: `type`, `subType`, `description`, then optional `rules`,
+ * - Each type: `type`, `subType`, then optional `implementedIn`
+ * (sorted, omitted unless the type names it), `description`, then optional `rules`,
* `example`, `whenToUse` (omitted when absent), then `attrs`, `children`,
* then optional `parents` (omitted when absent/empty).
* - Each child (FR-033 constraint graph): `childType`, `childSubType`,
diff --git a/server/typescript/packages/metadata/src/registry.ts b/server/typescript/packages/metadata/src/registry.ts
index 530b538ade..1ef3500108 100644
--- a/server/typescript/packages/metadata/src/registry.ts
+++ b/server/typescript/packages/metadata/src/registry.ts
@@ -83,6 +83,12 @@ export interface AttrSchema {
* loads it and does nothing with it. Absent = every port acts on it.
*/
ports?: readonly string[];
+ /**
+ * The ports that implement this attribute so far (`["java"]`), when it is
+ * cross-port vocabulary the others have not implemented yet. Every port registers and
+ * type-checks it. Absent = every port implements it.
+ */
+ implementedIn?: readonly string[];
/** Human/AI-facing description of what the attribute means. */
description: string;
/** FR-033 — prose documenting the complex rules enforced in code. Optional. */
@@ -114,6 +120,12 @@ export interface TypeDefinition {
example?: string;
/** FR-033 — guidance on when to reach for this type/subType. Optional. */
whenToUse?: string;
+ /**
+ * The ports that implement this type so far, when the other ports have
+ * not yet (`["typescript"]`). It is cross-port vocabulary: every port registers it
+ * and loads it strictly. Absent = every port implements it.
+ */
+ implementedIn?: readonly string[];
/** Max children of this type.subType per parent (`1` = singleton). Loader-enforced. */
maxOccurs?: number;
/** Default name for a singleton (`maxOccurs===1`) child declared with no name. */
diff --git a/server/typescript/packages/metadata/src/vocabulary-catalog.ts b/server/typescript/packages/metadata/src/vocabulary-catalog.ts
index 0eaa1481e9..65ca832e24 100644
--- a/server/typescript/packages/metadata/src/vocabulary-catalog.ts
+++ b/server/typescript/packages/metadata/src/vocabulary-catalog.ts
@@ -1,23 +1,15 @@
// #357 — "what can I author HERE?", which is NOT the question the registry manifest answers.
//
// `buildRegistryManifest` answers "what must all five ports byte-match?" and deliberately
-// carves rows OUT of its answer: the `metadata.base` inheritance anchor, and the 13
-// TS-web-presentation `view.*` controls, which are REGISTERED in every port (the loader
-// must accept an authored `view.dropdown`) but consumed only by TypeScript, and so are
-// excluded from the shared canonical everywhere. That carve-out is correct and documented
-// (fixtures/registry-conformance/README.md, B-2).
+// carves rows OUT of its answer: the `metadata.base` inheritance anchor, which only the
+// JVM registers. This catalog enumerates every (type, subType) the registry accepts and
+// MARKS the rows the cross-port manifest omits, so a carve-out reads as "not cross-port"
+// rather than "missing".
//
-// `meta types` — the vocabulary search the generated `AGENTS.md` and the
-// `metaobjects-authoring` skill both make STEP 1 of the authoring procedure — was built on
-// that same function, so the answer it gave an author was the cross-port contract with
-// this port's own vocabulary removed: 2 of the 15 registered `view.*` subtypes, with the
-// other 13 reported exactly as a genuine typo is. Following the documented procedure, an
-// author correctly concluded `view.text` does not exist.
-//
-// One function cannot answer both questions, so there are two. This one enumerates every
-// (type, subType) the registry accepts and MARKS the rows the cross-port manifest omits,
-// which is the honest way to surface what the carve-out means: not "missing" but
-// "TypeScript-only".
+// History: the 13 generic `view.*` controls used to be such a carve-out, and `meta types`
+// once reported them exactly as a genuine typo is. They are back in the
+// manifest as kind-1 vocabulary: cross-port, every port loads them strictly, and
+// `implementedIn: ["typescript"]` says which ports act on them so far.
import type { TypeDefinition, TypeRegistry } from "./registry.js";
import {
@@ -50,6 +42,11 @@ export interface VocabularyType {
attrProviders: Record;
children: ManifestChild[];
parents?: string[];
+ /**
+ * The ports that implement this type so far (`["typescript"]`); absent =
+ * every port. Cross-port vocabulary either way: every port loads it strictly.
+ */
+ implementedIn?: readonly string[];
/**
* False when the cross-port manifest carves this row out — vocabulary THIS port
* accepts that the shared five-port contract does not carry. An author can use it
@@ -99,6 +96,9 @@ function toVocabularyType(def: TypeDefinition, registry: TypeRegistry): Vocabula
attrs: sortedAttrs(def.attributes.filter((a) => classifyPerTypeAttr(a.name) === INCLUDED)),
attrProviders: {},
children: sortedChildren(def.childRules),
+ ...(def.implementedIn !== undefined && def.implementedIn.length > 0
+ ? { implementedIn: [...def.implementedIn].sort(compareStrings) }
+ : {}),
crossPort: reason === undefined,
...(reason !== undefined ? { portPrivateReason: reason } : {}),
sharedRoot: subType === SUBTYPE_BASE,
diff --git a/server/typescript/packages/metadata/test/conformance/adapter.ts b/server/typescript/packages/metadata/test/conformance/adapter.ts
index 77991fbe4e..0761067d7d 100644
--- a/server/typescript/packages/metadata/test/conformance/adapter.ts
+++ b/server/typescript/packages/metadata/test/conformance/adapter.ts
@@ -20,6 +20,7 @@ import { dbProvider } from "../../src/persistence/db/db-provider.js";
import { docProvider } from "../../src/core/documentation/doc-provider.js";
import { promptProvider } from "../../src/template/prompt-provider.js";
import { uiProvider } from "../../src/presentation/ui/ui-provider.js";
+import { uiWebProvider } from "../../src/presentation/ui-web/ui-web-provider.js";
import { MetaDataLoader } from "../../src/loader/meta-data-loader.js";
import type { MetaData } from "../../src/shared/meta-data.js";
import { canonicalSerialize, canonicalSerializeEffective } from "../../src/serializer-json.js";
@@ -157,6 +158,7 @@ export const PROVIDERS: Readonly> = {
// the TS adapter keeps an alias to the renamed promptProvider for those fixtures.
"metaobjects-template": promptProvider,
[uiProvider.id]: uiProvider, // "metaobjects-ui" (filter/sort field markers)
+ [uiWebProvider.id]: uiWebProvider, // "metaobjects-ui-web" (view control attrs)
// Test-only — provider-extension-* fixtures.
"example-template-briefing": exampleTemplateBriefingProvider,
"example-custom-vocabulary": exampleCustomVocabularyProvider,
diff --git a/server/typescript/packages/metadata/test/metamodel-docs.test.ts b/server/typescript/packages/metadata/test/metamodel-docs.test.ts
index 8c0b5bcedd..c5f5d3b10b 100644
--- a/server/typescript/packages/metadata/test/metamodel-docs.test.ts
+++ b/server/typescript/packages/metadata/test/metamodel-docs.test.ts
@@ -133,43 +133,22 @@ test("providers.md indexes all six concern providers with descriptions", () => {
}
});
-test("providers.md 'Owns' honors the same exclusions as INDEX/type pages (no generic view.* controls)", () => {
+test("providers.md 'Owns' and INDEX carry the kind-1 view.* controls, marked on their pages", () => {
const out = docs();
const providers = out.get("providers.md")!;
- // The "Owns (registers)" core list must NOT advertise the 11 manifest-
- // excluded TS-web-presentation controls (absent from INDEX/type pages) — a
- // provider that CONTRIBUTES attrs to one of these controls (e.g.
- // metaobjects-ui-web extending view.textarea/view.image with @rows / the
- // image attrs) may still legitimately name it under its own "Contributes
- // attributes" section, so scope this assertion to the "Owns (registers)"
- // line specifically rather than the whole page.
+ // The generic view.* controls are cross-port vocabulary only TypeScript implements so
+ // far: IN the manifest, so the docs cover them like any other subtype.
const ownsMatch = providers.match(/\*\*Owns \(registers\):\*\* (.+)/);
expect(ownsMatch).not.toBeNull();
const owns = ownsMatch![1];
- for (const excluded of [
- "view.checkbox",
- "view.date",
- "view.dropdown",
- "view.hidden",
- "view.hotlink",
- "view.month",
- "view.number",
- "view.password",
- "view.radio",
- "view.text",
- "view.textarea",
- "view.web",
- ]) {
- expect(owns).not.toContain(`\`${excluded}\``);
+ for (const control of ["view.base", "view.currency", "view.text", "view.textarea", "view.image", "view.web"]) {
+ expect(owns).toContain(`\`${control}\``);
}
- // ... but DOES keep the agreed view subtypes (`view.base`, `view.currency`).
- expect(owns).toContain("`view.base`");
- expect(owns).toContain("`view.currency`");
-
- // The same vocabulary INDEX covers: no excluded control leaks into INDEX either.
const index = out.get("INDEX.md")!;
- expect(index).not.toContain("`view.text`");
+ expect(index).toContain("`view.text`");
expect(index).toContain("`view.currency`");
+ const viewPage = [...out.entries()].find(([k]) => k.includes("view"))![1];
+ expect(viewPage).toContain("**Implemented in:** typescript so far");
});
test("provenance: core owns field.currency; db contributes @column; ui contributes @filterable", () => {
diff --git a/server/typescript/packages/metadata/test/object-definition-completeness.test.ts b/server/typescript/packages/metadata/test/object-definition-completeness.test.ts
index 89b19a4a10..31a42f3945 100644
--- a/server/typescript/packages/metadata/test/object-definition-completeness.test.ts
+++ b/server/typescript/packages/metadata/test/object-definition-completeness.test.ts
@@ -86,18 +86,25 @@ const REPORT_ATTRS: Record = {
spine: { valueType: "string", required: false },
};
-// @dbSetName: C#-only vocabulary every port loads, on entity / projection / report.
+// Language-specific attrs every port loads: @dbSetName (C#) on entity /
+// projection / report, and the JVM's @object / @objectAdapter on object.base, which
+// every subtype composes.
const DB_SET_NAME: Record = {
dbSetName: { valueType: "string", required: false },
};
+const JVM_BINDING: Record = {
+ object: { valueType: "string", required: false },
+ objectAdapter: { valueType: "string", required: false },
+};
+
function expectedAttrsFor(subType: string): Record {
- if (subType === OBJECT_SUBTYPE_ENTITY) return { ...DISCRIMINATOR, ...DB_SET_NAME };
- if (subType === OBJECT_SUBTYPE_VALUE) return { ...NORMALIZE };
- if (subType === OBJECT_SUBTYPE_PROJECTION) return { ...PROJECTION_FILTER, ...DB_SET_NAME };
- if (subType === OBJECT_SUBTYPE_REPORT) return { ...REPORT_ATTRS, ...DB_SET_NAME };
- // base carries NO attrs (discriminator moved to entity only).
- return {};
+ if (subType === OBJECT_SUBTYPE_ENTITY) return { ...DISCRIMINATOR, ...DB_SET_NAME, ...JVM_BINDING };
+ if (subType === OBJECT_SUBTYPE_VALUE) return { ...NORMALIZE, ...JVM_BINDING };
+ if (subType === OBJECT_SUBTYPE_PROJECTION) return { ...PROJECTION_FILTER, ...DB_SET_NAME, ...JVM_BINDING };
+ if (subType === OBJECT_SUBTYPE_REPORT) return { ...REPORT_ATTRS, ...DB_SET_NAME, ...JVM_BINDING };
+ // base carries only the JVM bindings (discriminator moved to entity only).
+ return { ...JVM_BINDING };
}
// The EXACT composed structural childRule childType set per subtype (the critical
diff --git a/server/typescript/packages/metadata/test/port-scoped-attr.test.ts b/server/typescript/packages/metadata/test/port-scoped-attr.test.ts
index a2659dd7f8..8557d16ba9 100644
--- a/server/typescript/packages/metadata/test/port-scoped-attr.test.ts
+++ b/server/typescript/packages/metadata/test/port-scoped-attr.test.ts
@@ -42,3 +42,36 @@ describe("a C#-only attribute", () => {
expect(entity.attrs.find((a) => a.name === "dbSetName")?.ports).toEqual(["csharp"]);
});
});
+
+// Cross-port vocabulary only some ports implement so far.
+const KIND1 = `{ "metadata.root": { "package": "acme", "children": [
+ { "object.entity": { "name": "Product", "@object": "com.acme.Product", "children": [
+ { "field.long": { "name": "id" } },
+ { "field.string": { "name": "notes", "children": [
+ { "view.textarea": { "name": "form", "@rows": 6 } } ] } },
+ { "field.string": { "name": "coverKey", "children": [
+ { "view.image": { "name": "form", "@aspectRatio": 1.5, "@maxEdge": 2000, "@store": "photos",
+ "@accept": ["image/png"], "@maxBytes": 1048576 } } ] } },
+ { "identity.primary": { "@fields": "id" } }
+ ] } }
+] } }`;
+
+describe("kind-1 vocabulary", () => {
+ test("loads under the strict loader", async () => {
+ expect(await codes(KIND1)).toEqual([]);
+ });
+
+ test("an attribute the registry does not carry on a kind-1 view still fails", async () => {
+ expect(await codes(KIND1.replace(`"@maxBytes": 1048576`, `"@cropShape": "circle"`))).toContain("ERR_UNKNOWN_ATTR");
+ });
+
+ test("the vocabulary names the ports that implement it so far", async () => {
+ const catalog = buildVocabularyCatalog(composeRegistry(coreProviders));
+ const image = catalog.types.find((t) => t.type === "view" && t.subType === "image")!;
+ expect(image.implementedIn).toEqual(["typescript"]);
+ expect(image.crossPort).toBe(true);
+ const entity = catalog.types.find((t) => t.type === "object" && t.subType === "entity")!;
+ expect(entity.attrs.find((a) => a.name === "object")?.implementedIn).toEqual(["java"]);
+ expect(entity.attrs.find((a) => a.name === "objectAdapter")?.ports).toEqual(["java", "kotlin"]);
+ });
+});
diff --git a/server/typescript/packages/metadata/test/registry-conformance.test.ts b/server/typescript/packages/metadata/test/registry-conformance.test.ts
index 1ab38aece1..426feabb9f 100644
--- a/server/typescript/packages/metadata/test/registry-conformance.test.ts
+++ b/server/typescript/packages/metadata/test/registry-conformance.test.ts
@@ -67,13 +67,33 @@ test("manifest excludes the metadata.base inheritance anchor (keeps metadata.roo
expect(metadataSubTypes).toContain("root");
});
-test("manifest cuts the 13 generic view.* controls, keeps view.base + view.currency", () => {
+// The generic view.* controls are cross-port vocabulary only TypeScript
+// implements so far: IN the manifest, marked `implementedIn: ["typescript"]`.
+const KIND1_VIEW_SUBTYPES = [
+ "checkbox", "date", "dropdown", "hidden", "hotlink", "image", "month",
+ "number", "password", "radio", "text", "textarea", "web",
+];
+
+test("manifest carries the 13 generic view.* controls as kind 1, implemented in TypeScript", () => {
const manifest = buildRegistryManifest(composeRegistry(coreProviders));
- const viewSubTypes = manifest.types
- .filter((t) => t.type === "view")
- .map((t) => t.subType)
- .sort();
- expect(viewSubTypes).toEqual(["base", "currency"]);
+ const views = manifest.types.filter((t) => t.type === "view");
+ expect(views.map((t) => t.subType).sort()).toEqual(["base", "currency", ...KIND1_VIEW_SUBTYPES].sort());
+ for (const t of views) {
+ if (KIND1_VIEW_SUBTYPES.includes(t.subType)) expect(t.implementedIn).toEqual(["typescript"]);
+ else expect(t.implementedIn).toBeUndefined();
+ }
+ // Their attrs are in the contract too, so every port must register them.
+ const image = views.find((t) => t.subType === "image")!;
+ expect(image.attrs.map((a) => a.name)).toEqual(["accept", "aspectRatio", "maxBytes", "maxEdge", "store"]);
+ const textarea = views.find((t) => t.subType === "textarea")!;
+ expect(textarea.attrs.map((a) => a.name)).toEqual(["rows"]);
+});
+
+test("implementedIn sits after subType and before description in the serialized manifest", () => {
+ const json = emitRegistryManifest(composeRegistry(coreProviders));
+ expect(json).toContain(
+ '"subType": "image",\n "implementedIn": [\n "typescript"\n ],\n "description":',
+ );
});
// Wave 3b — the in/out boundary is an EXPLICIT classification (a reason category
@@ -98,12 +118,13 @@ test("every carved-out per-type attr name carries a declared ExclusionReason", (
expect(validReasons.has(reason)).toBe(true);
expect(classifyPerTypeAttr(name)).toBe(reason);
}
- // The structural/OO-shape keywords are classified StructuralKeyword; the
- // ADR-0001/0005 bindings NativeBinding; the per-type description CommonAttrDup.
+ // The structural/OO-shape keywords are classified StructuralKeyword; the per-type
+ // description CommonAttrDup. The JVM's ADR-0001/0005 bindings are NOT carved out
+ //: they are in the contract with `ports: ["java", "kotlin"]`.
expect(classifyPerTypeAttr("extends")).toBe(ExclusionReason.StructuralKeyword);
expect(classifyPerTypeAttr("isArray")).toBe(ExclusionReason.StructuralKeyword);
- expect(classifyPerTypeAttr("object")).toBe(ExclusionReason.NativeBinding);
- expect(classifyPerTypeAttr("objectAdapter")).toBe(ExclusionReason.NativeBinding);
+ expect(classifyPerTypeAttr("object")).toBe("included");
+ expect(classifyPerTypeAttr("objectAdapter")).toBe("included");
expect(classifyPerTypeAttr("description")).toBe(ExclusionReason.CommonAttrDup);
// A genuinely logical attr is INCLUDED.
expect(classifyPerTypeAttr("column")).toBe("included");
diff --git a/site-reference/index.html b/site-reference/index.html
index 49cb609caa..f500c94a86 100644
--- a/site-reference/index.html
+++ b/site-reference/index.html
@@ -441,10 +441,75 @@
types/view.md#viewbase |
+view.checkbox |
+Checkbox selection control. |
+types/view.md#viewcheckbox |
+
+
view.currency |
Currency display formatting (locale-aware via @locale). |
types/view.md#viewcurrency |
+
+view.date |
+Date picker / date display. |
+types/view.md#viewdate |
+
+
+view.dropdown |
+Single-select dropdown control. |
+types/view.md#viewdropdown |
+
+
+view.hidden |
+Not rendered; carried but not shown. |
+types/view.md#viewhidden |
+
+
+view.hotlink |
+Renders the value as a clickable link. |
+types/view.md#viewhotlink |
+
+
+view.image |
+Image upload/display control; the field stores an opaque storage key (field.string). |
+types/view.md#viewimage |
+
+
+view.month |
+Month picker / month display. |
+types/view.md#viewmonth |
+
+
+view.number |
+Numeric input control. |
+types/view.md#viewnumber |
+
+
+view.password |
+Masked input for sensitive values. |
+types/view.md#viewpassword |
+
+
+view.radio |
+Radio-button selection control. |
+types/view.md#viewradio |
+
+
+view.text |
+Single-line text input/display. |
+types/view.md#viewtext |
+
+
+view.textarea |
+Multi-line text area. |
+types/view.md#viewtextarea |
+
+
+view.web |
+Abstract base for web-rendered views. |
+types/view.md#viewweb |
+
diff --git a/site-reference/providers.html b/site-reference/providers.html
index 9d9bcb8f79..ab2c127bb5 100644
--- a/site-reference/providers.html
+++ b/site-reference/providers.html
@@ -51,7 +51,7 @@
INDEX.md lists by type.
metaobjects-core-types
Core metaobjects metamodel types and subtypes.
-Owns (registers): attr.base, attr.boolean, attr.class, attr.double, attr.expression, attr.filter, attr.int, attr.intMap, attr.long, attr.properties, attr.string, field.base, field.boolean, field.currency, field.date, field.decimal, field.double, field.duration, field.enum, field.float, field.inet, field.int, field.long, field.map, field.object, field.string, field.time, field.timestamp, field.uri, field.uuid, identity.primary, identity.reference, identity.secondary, layout.base, layout.dataGrid, object.base, object.entity, object.projection, object.report, object.value, origin.aggregate, origin.base, origin.computed, origin.first, origin.passthrough, relationship.aggregation, relationship.association, relationship.base, relationship.composition, source.base, source.rdb, template.base, template.output, template.prompt, template.toolcall, validator.array, validator.atLeastOne, validator.base, validator.comparison, validator.exactlyOne, validator.length, validator.numeric, validator.presentIff, validator.regex, validator.required, validator.requiredWhen, view.base, view.currency
+Owns (registers): attr.base, attr.boolean, attr.class, attr.double, attr.expression, attr.filter, attr.int, attr.intMap, attr.long, attr.properties, attr.string, field.base, field.boolean, field.currency, field.date, field.decimal, field.double, field.duration, field.enum, field.float, field.inet, field.int, field.long, field.map, field.object, field.string, field.time, field.timestamp, field.uri, field.uuid, identity.primary, identity.reference, identity.secondary, layout.base, layout.dataGrid, object.base, object.entity, object.projection, object.report, object.value, origin.aggregate, origin.base, origin.computed, origin.first, origin.passthrough, relationship.aggregation, relationship.association, relationship.base, relationship.composition, source.base, source.rdb, template.base, template.output, template.prompt, template.toolcall, validator.array, validator.atLeastOne, validator.base, validator.comparison, validator.exactlyOne, validator.length, validator.numeric, validator.presentIff, validator.regex, validator.required, validator.requiredWhen, view.base, view.checkbox, view.currency, view.date, view.dropdown, view.hidden, view.hotlink, view.image, view.month, view.number, view.password, view.radio, view.text, view.textarea, view.web
Contributes attributes:
field.base: @default, @mutability, @required, @unique
@@ -66,6 +66,7 @@ metaobjects-core-types
identity.primary: @fields, @generation
identity.reference: @enforce, @fields, @references
identity.secondary: @fields
+object.base: @object, @objectAdapter
object.entity: @dbSetName, @discriminator, @discriminatorValue
object.projection: @dbSetName, @filter
object.report: @dbSetName, @dimensions, @filter, @from, @measures, @segment, @spine
@@ -183,7 +184,7 @@ metaobjects-ui
view.currency: @locale
metaobjects-ui-web
-TS-web presentation view attrs — @rows (view.textarea) and the view.image control attrs. Applied only in TypeScript; the non-TS ports mirror the spec file but never apply this provider (the view subtypes are TS-web presentation-only).
+Web presentation view attrs — @rows (view.textarea) and the view.image control attrs. Every port applies this provider and loads them strictly; only TypeScript acts on them so far.
Contributes attributes:
view.image: @accept, @aspectRatio, @maxBytes, @maxEdge, @store
diff --git a/site-reference/types/object.html b/site-reference/types/object.html
index e4dada772c..366a0c512b 100644
--- a/site-reference/types/object.html
+++ b/site-reference/types/object.html
@@ -54,7 +54,37 @@ object.base
Abstract object base — the shared root subtype that concrete object subtypes (entity/value/projection) specialize. Declares the structural children common to EVERY object subtype (the intersection: field/identity/validator/layout/source); subtype-specific children (relationship, template) and attrs (discriminator) ride their own subtypes. Has no runtime semantics of its own; not authored directly. A object.base node fails to load (ERR_ABSTRACT_SUBTYPE_AUTHORED): this subtype is a registry anchor concrete subtypes inherit from, never a node in a document.
Owning provider: metaobjects-core-types
Attributes
-No subtype-specific attributes.
+
+
+
+| Attribute |
+Type |
+Required |
+Default |
+Allowed values |
+Provider |
+Description |
+
+
+
+@object |
+string |
+no |
+ |
+ |
+metaobjects-core-types |
+The native class an object binds to (ADR-0001), in place of its default runtime class: on the JVM, a fully-qualified class name. Cross-port vocabulary only Java implements so far; every port loads and type-checks it, and the others do not act on it yet. |
+
+
+@objectAdapter |
+string |
+no |
+ |
+ |
+metaobjects-core-types |
+JVM-only (ADR-0005): the fully-qualified name of an ObjectAdapter that takes over value access for this object's instances. Every port loads and type-checks it; only Java and Kotlin act on it. |
+
+
Allowed children
field.* — 0..*
@@ -109,6 +139,24 @@ object.entity
metaobjects-core-types |
FR-014: on a subtype of an entity with @discriminator — the value that identifies rows of this subtype in the shared discriminator field. Wire form is always a string; the underlying field's subtype (enum / int / string) controls codegen + storage coercion. Required on every concrete subtype of a discriminated entity. |
+
+@object |
+string |
+no |
+ |
+ |
+metaobjects-core-types |
+The native class an object binds to (ADR-0001), in place of its default runtime class: on the JVM, a fully-qualified class name. Cross-port vocabulary only Java implements so far; every port loads and type-checks it, and the others do not act on it yet. |
+
+
+@objectAdapter |
+string |
+no |
+ |
+ |
+metaobjects-core-types |
+JVM-only (ADR-0005): the fully-qualified name of an ObjectAdapter that takes over value access for this object's instances. Every port loads and type-checks it; only Java and Kotlin act on it. |
+
Allowed children
@@ -160,6 +208,24 @@ object.projection
metaobjects-core-types |
Optional row-scope predicate (a portable attr.filter object: eq/ne/gt/gte/lt/lte/like/in/isNull with and/or, desugared to { field: { op: value } } at parse time) selecting which rows the view returns — lowered to an outer SQL WHERE. Resolves against the projection's own declared fields; an aggregate-derived field is not addressable (fail-closed). |
+
+@object |
+string |
+no |
+ |
+ |
+metaobjects-core-types |
+The native class an object binds to (ADR-0001), in place of its default runtime class: on the JVM, a fully-qualified class name. Cross-port vocabulary only Java implements so far; every port loads and type-checks it, and the others do not act on it yet. |
+
+
+@objectAdapter |
+string |
+no |
+ |
+ |
+metaobjects-core-types |
+JVM-only (ADR-0005): the fully-qualified name of an ObjectAdapter that takes over value access for this object's instances. Every port loads and type-checks it; only Java and Kotlin act on it. |
+
Allowed children
@@ -233,6 +299,24 @@ object.report
Measure names of @from. |
+@object |
+string |
+no |
+ |
+ |
+metaobjects-core-types |
+The native class an object binds to (ADR-0001), in place of its default runtime class: on the JVM, a fully-qualified class name. Cross-port vocabulary only Java implements so far; every port loads and type-checks it, and the others do not act on it yet. |
+
+
+@objectAdapter |
+string |
+no |
+ |
+ |
+metaobjects-core-types |
+JVM-only (ADR-0005): the fully-qualified name of an ObjectAdapter that takes over value access for this object's instances. Every port loads and type-checks it; only Java and Kotlin act on it. |
+
+
@segment |
string |
no |
@@ -287,6 +371,24 @@ object.value
metaobjects-prompt |
ASCII normalization mode for tolerant enum extract (none|collapse|strip, default strip). On field.enum it is per-field; on object.value it is the default for the object's enum fields. |
+
+@object |
+string |
+no |
+ |
+ |
+metaobjects-core-types |
+The native class an object binds to (ADR-0001), in place of its default runtime class: on the JVM, a fully-qualified class name. Cross-port vocabulary only Java implements so far; every port loads and type-checks it, and the others do not act on it yet. |
+
+
+@objectAdapter |
+string |
+no |
+ |
+ |
+metaobjects-core-types |
+JVM-only (ADR-0005): the fully-qualified name of an ObjectAdapter that takes over value access for this object's instances. Every port loads and type-checks it; only Java and Kotlin act on it. |
+
Allowed children
diff --git a/site-reference/types/view.html b/site-reference/types/view.html
index d519571d3b..2e3be8647b 100644
--- a/site-reference/types/view.html
+++ b/site-reference/types/view.html
@@ -57,6 +57,14 @@ view.base
No subtype-specific attributes.
Allowed children
No structural children.
+view.checkbox
+Checkbox selection control.
+Owning provider: metaobjects-core-types
+Implemented in: typescript so far — every port loads and type-checks it; the others do not act on it yet.
+Attributes
+No subtype-specific attributes.
+Allowed children
+No structural children.
view.currency
Currency display formatting (locale-aware via @locale).
Owning provider: metaobjects-core-types
@@ -84,6 +92,180 @@ view.currency
Allowed children
+No structural children.
+view.date
+Date picker / date display.
+Owning provider: metaobjects-core-types
+Implemented in: typescript so far — every port loads and type-checks it; the others do not act on it yet.
+Attributes
+No subtype-specific attributes.
+Allowed children
+No structural children.
+view.dropdown
+Single-select dropdown control.
+Owning provider: metaobjects-core-types
+Implemented in: typescript so far — every port loads and type-checks it; the others do not act on it yet.
+Attributes
+No subtype-specific attributes.
+Allowed children
+No structural children.
+view.hidden
+Not rendered; carried but not shown.
+Owning provider: metaobjects-core-types
+Implemented in: typescript so far — every port loads and type-checks it; the others do not act on it yet.
+Attributes
+No subtype-specific attributes.
+Allowed children
+No structural children.
+view.hotlink
+Renders the value as a clickable link.
+Owning provider: metaobjects-core-types
+Implemented in: typescript so far — every port loads and type-checks it; the others do not act on it yet.
+Attributes
+No subtype-specific attributes.
+Allowed children
+No structural children.
+view.image
+Image upload/display control; the field stores an opaque storage key (field.string).
+Owning provider: metaobjects-core-types
+Implemented in: typescript so far — every port loads and type-checks it; the others do not act on it yet.
+Attributes
+
+
+
+| Attribute |
+Type |
+Required |
+Default |
+Allowed values |
+Provider |
+Description |
+
+
+
+@accept |
+string[] |
+no |
+ |
+ |
+metaobjects-ui-web |
+Accepted MIME types (client guard; the server re-enforces). |
+
+
+@aspectRatio |
+double |
+no |
+ |
+ |
+metaobjects-ui-web |
+Crop aspect ratio (width / height). Omit for freeform. |
+
+
+@maxBytes |
+int |
+no |
+ |
+ |
+metaobjects-ui-web |
+Client-side size ceiling in bytes. |
+
+
+@maxEdge |
+int |
+no |
+ |
+ |
+metaobjects-ui-web |
+Longest-edge bound in px for the re-encoded output. |
+
+
+@store |
+string |
+no |
+ |
+ |
+metaobjects-ui-web |
+Opaque storage-namespace hint passed to the upload adapter (not infrastructure). |
+
+
+Allowed children
+No structural children.
+view.month
+Month picker / month display.
+Owning provider: metaobjects-core-types
+Implemented in: typescript so far — every port loads and type-checks it; the others do not act on it yet.
+Attributes
+No subtype-specific attributes.
+Allowed children
+No structural children.
+view.number
+Numeric input control.
+Owning provider: metaobjects-core-types
+Implemented in: typescript so far — every port loads and type-checks it; the others do not act on it yet.
+Attributes
+No subtype-specific attributes.
+Allowed children
+No structural children.
+view.password
+Masked input for sensitive values.
+Owning provider: metaobjects-core-types
+Implemented in: typescript so far — every port loads and type-checks it; the others do not act on it yet.
+Attributes
+No subtype-specific attributes.
+Allowed children
+No structural children.
+view.radio
+Radio-button selection control.
+Owning provider: metaobjects-core-types
+Implemented in: typescript so far — every port loads and type-checks it; the others do not act on it yet.
+Attributes
+No subtype-specific attributes.
+Allowed children
+No structural children.
+view.text
+Single-line text input/display.
+Owning provider: metaobjects-core-types
+Implemented in: typescript so far — every port loads and type-checks it; the others do not act on it yet.
+Attributes
+No subtype-specific attributes.
+Allowed children
+No structural children.
+view.textarea
+Multi-line text area.
+Owning provider: metaobjects-core-types
+Implemented in: typescript so far — every port loads and type-checks it; the others do not act on it yet.
+Attributes
+
+
+
+| Attribute |
+Type |
+Required |
+Default |
+Allowed values |
+Provider |
+Description |
+
+
+
+@rows |
+int |
+no |
+ |
+ |
+metaobjects-ui-web |
+Visible row count for the generated |
+
+
+Allowed children
+No structural children.
+view.web
+Abstract base for web-rendered views.
+Owning provider: metaobjects-core-types
+Implemented in: typescript so far — every port loads and type-checks it; the others do not act on it yet.
+Attributes
+No subtype-specific attributes.
+Allowed children
No structural children.
diff --git a/spec/metamodel/object.json b/spec/metamodel/object.json
index 7aa14f909e..f9d9280409 100644
--- a/spec/metamodel/object.json
+++ b/spec/metamodel/object.json
@@ -11,7 +11,9 @@
{ "type": "validator", "subType": "*", "name": "*", "min": 0, "max": null },
{ "type": "layout", "subType": "*", "name": "*", "min": 0, "max": null },
{ "type": "source", "subType": "*", "name": "*", "min": 0, "max": null },
- { "type": "index", "subType": "*", "name": "*", "min": 0, "max": null }
+ { "type": "index", "subType": "*", "name": "*", "min": 0, "max": null },
+ { "type": "attr", "subType": "string", "name": "object", "min": 0, "max": 1, "implementedIn": ["java"], "description": "The native class an object binds to (ADR-0001), in place of its default runtime class: on the JVM, a fully-qualified class name. Cross-port vocabulary only Java implements so far; every port loads and type-checks it, and the others do not act on it yet." },
+ { "type": "attr", "subType": "string", "name": "objectAdapter", "min": 0, "max": 1, "ports": ["java", "kotlin"], "description": "JVM-only (ADR-0005): the fully-qualified name of an ObjectAdapter that takes over value access for this object's instances. Every port loads and type-checks it; only Java and Kotlin act on it." }
]
},
{
diff --git a/spec/metamodel/view.json b/spec/metamodel/view.json
index d5e2ac9370..d839d9c431 100644
--- a/spec/metamodel/view.json
+++ b/spec/metamodel/view.json
@@ -9,61 +9,73 @@
{
"type": "view",
"subType": "text",
+ "implementedIn": ["typescript"],
"description": "Single-line text input/display."
},
{
"type": "view",
"subType": "textarea",
+ "implementedIn": ["typescript"],
"description": "Multi-line text area."
},
{
"type": "view",
"subType": "date",
+ "implementedIn": ["typescript"],
"description": "Date picker / date display."
},
{
"type": "view",
"subType": "month",
+ "implementedIn": ["typescript"],
"description": "Month picker / month display."
},
{
"type": "view",
"subType": "hotlink",
+ "implementedIn": ["typescript"],
"description": "Renders the value as a clickable link."
},
{
"type": "view",
"subType": "dropdown",
+ "implementedIn": ["typescript"],
"description": "Single-select dropdown control."
},
{
"type": "view",
"subType": "radio",
+ "implementedIn": ["typescript"],
"description": "Radio-button selection control."
},
{
"type": "view",
"subType": "checkbox",
+ "implementedIn": ["typescript"],
"description": "Checkbox selection control."
},
{
"type": "view",
"subType": "number",
+ "implementedIn": ["typescript"],
"description": "Numeric input control."
},
{
"type": "view",
"subType": "password",
+ "implementedIn": ["typescript"],
"description": "Masked input for sensitive values."
},
{
"type": "view",
"subType": "hidden",
+ "implementedIn": ["typescript"],
"description": "Not rendered; carried but not shown."
},
{
"type": "view",
"subType": "web",
+ "implementedIn": ["typescript"],
"description": "Abstract base for web-rendered views."
},
{
@@ -71,6 +83,6 @@
"subType": "currency",
"description": "Currency display formatting (locale-aware via @locale)."
},
- { "type": "view", "subType": "image", "description": "Image upload/display control; the field stores an opaque storage key (field.string)." }
+ { "type": "view", "subType": "image", "implementedIn": ["typescript"], "description": "Image upload/display control; the field stores an opaque storage key (field.string)." }
]
}
From 1257c14799794e0ab903b6b0691f14f7f5edcbca Mon Sep 17 00:00:00 2001
From: Doug Mealing
Date: Wed, 11 Nov 2026 13:11:29 -0500
Subject: [PATCH 2/4] no-mistakes(review): apply reviewer inline fixes
---
scripts/lib/requirement-vocabulary.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/lib/requirement-vocabulary.ts b/scripts/lib/requirement-vocabulary.ts
index 0a2bdabd46..656804ce13 100644
--- a/scripts/lib/requirement-vocabulary.ts
+++ b/scripts/lib/requirement-vocabulary.ts
@@ -93,7 +93,7 @@ export interface Entry { key: string; attrs: Set }
* `expected-registry.json` is the cross-port contract, `spec/metamodel/*.json` is what
* the providers declare, and the gap is exactly `metadata.root`, which has no spec file.
* (The 13 generic `view.*` controls were once carved out of the manifest;
- * them back as kind-1 vocabulary, so both artifacts now carry them.) A carve-out from the
+ * they are back as kind-1 vocabulary, so both artifacts now carry them.) A carve-out from the
* cross-port manifest is a statement about portability, not a licence to leave
* vocabulary unpromised.
*
From 829ad66f22f207d2d9e6ef6bab779eb0e3091705 Mon Sep 17 00:00:00 2001
From: Doug Mealing
Date: Wed, 11 Nov 2026 13:12:19 -0500
Subject: [PATCH 3/4] no-mistakes(review): Restore CI-safe golden path in
GeneratorGoldenTests
---
.../MetaObjects.Codegen.Tests/GeneratorGoldenTests.cs | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/server/csharp/MetaObjects.Codegen.Tests/GeneratorGoldenTests.cs b/server/csharp/MetaObjects.Codegen.Tests/GeneratorGoldenTests.cs
index a5b2d53d05..a625d9e131 100644
--- a/server/csharp/MetaObjects.Codegen.Tests/GeneratorGoldenTests.cs
+++ b/server/csharp/MetaObjects.Codegen.Tests/GeneratorGoldenTests.cs
@@ -20,7 +20,6 @@
// Recapture only for a deliberate change to default output: GOLDEN_WRITE=1 dotnet test --filter
// GeneratorGoldenTests, then review the diff line by line. Compare mode is the default.
-using System.Runtime.CompilerServices;
using MetaObjects.Codegen;
using MetaObjects.Codegen.Generators;
using MetaObjects.Loader;
@@ -93,8 +92,10 @@ public class GeneratorGoldenTests
]}}
""";
- private static string GoldenDir([CallerFilePath] string here = "") =>
- Path.Combine(Path.GetDirectoryName(here)!, "Golden", "pre-hooks");
+ // Found from the repo root, not [CallerFilePath]: a CI build (CI=true turns on
+ // ContinuousIntegrationBuild) maps source paths to /_/, where no golden file exists.
+ private static string GoldenDir() =>
+ Path.Combine(CorpusPaths.RepoRoot(), "server", "csharp", "MetaObjects.Codegen.Tests", "Golden", "pre-hooks");
[Fact]
public void Default_output_matches_the_output_captured_before_the_hooks()
From dcf93a0d53c8a6ddfde946db1767b35bf712e487 Mon Sep 17 00:00:00 2001
From: Doug Mealing
Date: Wed, 11 Nov 2026 13:41:17 -0500
Subject: [PATCH 4/4] no-mistakes(document): Docs already updated on branch for
implementedIn, tsOnly, counts (454) and carve-out removal. One stale
historical sentence in registry README fixed. No unresolved gaps.
---
fixtures/registry-conformance/README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/fixtures/registry-conformance/README.md b/fixtures/registry-conformance/README.md
index db55e4f192..e0afc8a162 100644
--- a/fixtures/registry-conformance/README.md
+++ b/fixtures/registry-conformance/README.md
@@ -377,8 +377,8 @@ Detail per port:
uniform emitter exclusions documented in "EXCLUDED" above (no Java behavior
change): the structural keywords `isArray`/`isAbstract` + the `description`
per-type duplicate are filtered from the `attrs` list, the `metadata.base`
- anchor row is skipped, and the generic `view.*` controls are cut from the
- manifest (every port registers them for loading only). The REMAINING (Phase 2) divergence between Java's
+ anchor row is skipped. (The generic `view.*` controls were cut from the
+ manifest at that point; they are now in it, marked `implementedIn`.) The REMAINING (Phase 2) divergence between Java's
registry and the cross-port logical vocabulary that TS, C#, and Python agree
on: Java carries a parallel physical-DB attr vocabulary
(`dbType`/`dbIndex`/`dbLength`/`dbNullable`/`dbForeignKey`/`dbPrecision`/