diff --git a/AGENTS.md b/AGENTS.md index 4ead16eb7..4832e9738 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,7 +75,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/` (361 fixtures; 25 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/` (363 fixtures; 25 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 **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. Two sub-corpora run the **generated lane only, on all five ports** — `write-through/` and `projection/` (F22: a view-only `object.projection` serves GET list + GET by id and answers every write verb with `405 {"error": "method_not_allowed"}`). 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 76af5ef35..c558bbe25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,25 @@ it until 1.1 ships._ ### Fixed +- **Python, C#, Java: the generic `view.*` controls now load.** A document carrying + `view.text`, `view.dropdown` or any of the other web-presentation controls (`textarea`, `date`, + `month`, `hotlink`, `radio`, `checkbox`, `number`, `password`, `hidden`, `web`, `image`) + loaded in TypeScript and failed with `ERR_UNKNOWN_SUBTYPE` in every other port, so metadata + shared between a TypeScript web client and a Python, C#, Java or Kotlin backend could not be + loaded by the backend. Those ports had left the controls unregistered on purpose, as + vocabulary with no backend consumer. They are now registered for LOADING in every port and + remain presentation-only: no backend generator reads them, and they stay out of + `expected-registry.json`, so `metamodelVersion` does not move for this. Gated by the + `view-text-basic` conformance fixture. Attributes on these controls are still registered only + by the TypeScript `ui-web` provider. +- **Python, C#, Java: an inline object-valued attribute now loads as an `attr.properties` bag.** + `"@store": { "collection": "orders" }` on a node that declares no `@store` is the registered + property bag, the same thing the explicit `{ "attr.properties": { "name": ..., "value": {...} } }` + child produces. TypeScript always loaded it that way. Python loaded it as `attr.base` and + failed a strict load with `ERR_UNKNOWN_ATTR`; C# rejected the value with `ERR_BAD_ATTR_VALUE`; + Java (and so Kotlin) loaded it but stored the bag as a JSON-text string. All four loaders now + agree, gated by the `attr-properties-inline` conformance fixture. Nothing that loaded before + stops loading. - **Java OMDB reads a projection whose view is named by `@view`.** The read mapping took the view name from `@table` only, so a projection declared with the kind-matching `@view` alias had no read mapping. It now resolves the source's physical name (`@view`, then the legacy diff --git a/README.md b/README.md index ede493bf2..4721c73ca 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ metaobjects/ ├── CLAUDE.md # project instructions for Claude ├── spec/ # canonical metamodel docs, ADRs, roadmap ├── fixtures/ # 25 cross-language conformance corpora — the oracle -│ ├── conformance/ # metamodel (loader + serializer + navigation), 361 fixtures +│ ├── conformance/ # metamodel (loader + serializer + navigation), 363 fixtures │ ├── yaml-conformance/ # YAML authoring desugar │ ├── render-conformance/ # FR-004 byte-identical render oracle │ ├── verify-conformance/ # FR-004 template-drift gate diff --git a/agent-context/skills/metaobjects-audit/SKILL.md b/agent-context/skills/metaobjects-audit/SKILL.md index bc1d7b5de..bbcde472f 100644 --- a/agent-context/skills/metaobjects-audit/SKILL.md +++ b/agent-context/skills/metaobjects-audit/SKILL.md @@ -560,7 +560,7 @@ The audit never edits code. Pattern: **dry-run → review the diff → apply**. `APIRouter`; relationship / non-`table` source-kind / `field.object flattened` codegen is partial. - **C#** has no ObjectManager runtime tier (EF Core is the runtime) — hand services over the generated `DbContext` are expected. - **Cut subtypes** — `field.byte` / `field.short` / `field.class` are removed; never recommend them. -- **TS/web-only** — `view.*` widget subtypes exist only for TS/web consumers; only `view.base` / `view.currency` are cross-port-gated. +- **TS/web-only** — `view.*` widget subtypes load in every port but are consumed only by TS/web; only `view.base` / `view.currency` are cross-port-gated. - **Planned, not shipped** — `api.*` / `operation.*` / `binding.*` (FR-024) and MCP exposure of declared prompts/tools are not yet in the registry; their absence is not an adopter defect. - **Cross-port version-NUMBER skew is by design** — TS/C#/Python `0.x` vs Java/Kotlin `7.x` Maven is correct; never flag the *number lines* differing. But that is exactly why you can't eyeball cross-language drift: compare **`metamodelVersion`** (Phase 0 cross-language consistency item), not the package numbers. A `metamodelVersion` MISMATCH across ports *is* a finding; so is a port lagging its ecosystem's latest release. Also flag *intra-port* drift (mixed versions within one port, or a runtime package in `devDependencies`). - **Stale upstream prose** — "hand-write the Spring controller" (Java/Kotlin) is out of date; trust `meta gen --list`, not stale prose. diff --git a/agent-context/skills/metaobjects-audit/references/capability-checklist.md b/agent-context/skills/metaobjects-audit/references/capability-checklist.md index 1b38d09f2..61c563d4a 100644 --- a/agent-context/skills/metaobjects-audit/references/capability-checklist.md +++ b/agent-context/skills/metaobjects-audit/references/capability-checklist.md @@ -176,10 +176,12 @@ soft-delete / status / type view it models. options the currency view models. **Cross-port-gated** (with `view.base`). - **`layout.dataGrid`** (`@columns`, `@defaultSortField`, `@defaultSortOrder`, `@pageSize`) — hunt hand-written grid column definitions + data hooks a data-grid layout generates. -- **CALIBRATION — TS/web-only:** the `view.*` widget subtypes exist only for TS/web consumers +- **CALIBRATION — TS/web-only:** the `view.*` widget subtypes are consumed only by TS/web and are NOT in the cross-port registry — `view.text`, `view.textarea`, `view.date`, - `view.month`, `view.hotlink`, `view.dropdown`, `view.radio`, `view.checkbox`, `view.number`, - `view.password`, `view.hidden`, `view.web`. **Audit these only for TS adopters.** Only + `view.month`, `view.hotlink`, `view.image`, `view.dropdown`, `view.radio`, `view.checkbox`, + `view.number`, `view.password`, `view.hidden`, `view.web`. Every port LOADS them, so shared metadata that + carries them is valid in a backend port; nothing outside TS/web reads them. **Audit these + only for TS adopters.** Only `view.base` / `view.currency` are cross-port-gated. ## Template — `template.*` (prompt pillar) diff --git a/docs/CONFORMANCE.md b/docs/CONFORMANCE.md index 9ed4560fc..478fec081 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) | 361 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ | +| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 363 | ✓ | ✓ | 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/) | 31 | ✓ | ✓ | inherits via Java | ✓ | ✓ | | [`fixtures/verify-strict-conformance/`](../fixtures/verify-strict-conformance/) | 1 | ✓ | — | — | — | ✓ | @@ -202,7 +202,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker. ## Fixture-to-doc mapping -### `fixtures/conformance/` — metamodel loader + canonical serializer (361) +### `fixtures/conformance/` — metamodel loader + canonical serializer (363) | Fixture prefix | Feature doc | |---|---| @@ -340,7 +340,7 @@ identically** — every case's `expectFiles`/`expectImported`/`expectSelected`/ `expectMigrateGoverned`/`expectLoadError` assertions apply to both runners with no exemption. The two overlay cases that need a view child as incidental content use `view.currency` (the one concrete `view.*` subtype registered cross-port), not -`view.text` — a prior revision used `view.text`, which Python doesn't register, and +`view.text` — a prior revision used `view.text`, which Python did not register then, and carried a since-discharged allowlist for it. Java, Kotlin and C# have no runner — Phase 1a is TypeScript + Python only; those three ports arrive in Phase 2. @@ -397,7 +397,7 @@ own those two functions), and ## Orphaned fixtures (tested but not yet documented) -The fixtures in the nine corpora mapped above (metamodel 361 + yaml 16 + verify 31 +The fixtures in the nine corpora mapped above (metamodel 363 + yaml 16 + verify 31 + render 15 + persistence 39 + api-contract 61 + source-resolution 25 + scope 10 + dependency 23) each map to a feature doc. None are orphaned today. The remaining corpora in the totals table gate tooling contracts (registry manifests, provider diff --git a/docs/features/image-upload.md b/docs/features/image-upload.md index 0a9ee6709..9d33ead5e 100644 --- a/docs/features/image-upload.md +++ b/docs/features/image-upload.md @@ -7,12 +7,14 @@ image to a consumer-supplied adapter and stores only the key the adapter returns. No image bytes ever cross the MetaObjects wire — the field, the generated Zod schema, and the REST payload all carry a plain string. -This is a **TS-web-only** feature. The metamodel vocabulary (`view.image` and -its five attrs) is registered by the `metaobjects-ui-web` provider and applied -only in TypeScript; the non-TS ports carry a byte-identical mirror of the spec -file for drift parity but never apply the provider, and none of them ship an -upload widget. A `field.string` authored with a `view.image` child is, to a -Java/Kotlin/C#/Python port, just a plain string field. +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. ## 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 ef09746f4..840c53576 100644 --- a/docs/features/migrations/base-subtypes-are-not-authorable.md +++ b/docs/features/migrations/base-subtypes-are-not-authorable.md @@ -67,10 +67,14 @@ the owning field's subtype, so a boolean default stays a boolean rather than bei ### `view.base` is the one genuine removal -`view.base` was a view carrying no kind. Core registers only `view.base` and `view.currency`; the -other view subtypes (`view.text`, `view.textarea`, `view.checkbox`, `view.radio`, `view.image`, …) -come from the TypeScript-side UI provider. A project on a port that does not apply that provider -therefore has `view.currency` and nothing else until it does. +`view.base` was a view carrying no kind. At this migration's cut, TypeScript registered all the +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` +itself is refused in every port. Nothing is lost that carried information — a `view.base` node declared no kind and no attrs, and the JVM never accepted one — but if you were using it as a placeholder, delete it rather than diff --git a/examples/showcase/site-payload.json b/examples/showcase/site-payload.json index 0ecc5e2d9..ee3e186ce 100644 --- a/examples/showcase/site-payload.json +++ b/examples/showcase/site-payload.json @@ -7,7 +7,7 @@ "metamodel": "1.1" }, "counts": { - "fixtures": 361, + "fixtures": 363, "corpora": 25, "baseTypes": 17 }, diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md index bc1d7b5de..bbcde472f 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -560,7 +560,7 @@ The audit never edits code. Pattern: **dry-run → review the diff → apply**. `APIRouter`; relationship / non-`table` source-kind / `field.object flattened` codegen is partial. - **C#** has no ObjectManager runtime tier (EF Core is the runtime) — hand services over the generated `DbContext` are expected. - **Cut subtypes** — `field.byte` / `field.short` / `field.class` are removed; never recommend them. -- **TS/web-only** — `view.*` widget subtypes exist only for TS/web consumers; only `view.base` / `view.currency` are cross-port-gated. +- **TS/web-only** — `view.*` widget subtypes load in every port but are consumed only by TS/web; only `view.base` / `view.currency` are cross-port-gated. - **Planned, not shipped** — `api.*` / `operation.*` / `binding.*` (FR-024) and MCP exposure of declared prompts/tools are not yet in the registry; their absence is not an adopter defect. - **Cross-port version-NUMBER skew is by design** — TS/C#/Python `0.x` vs Java/Kotlin `7.x` Maven is correct; never flag the *number lines* differing. But that is exactly why you can't eyeball cross-language drift: compare **`metamodelVersion`** (Phase 0 cross-language consistency item), not the package numbers. A `metamodelVersion` MISMATCH across ports *is* a finding; so is a port lagging its ecosystem's latest release. Also flag *intra-port* drift (mixed versions within one port, or a runtime package in `devDependencies`). - **Stale upstream prose** — "hand-write the Spring controller" (Java/Kotlin) is out of date; trust `meta gen --list`, not stale prose. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md index 1b38d09f2..61c563d4a 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md @@ -176,10 +176,12 @@ soft-delete / status / type view it models. options the currency view models. **Cross-port-gated** (with `view.base`). - **`layout.dataGrid`** (`@columns`, `@defaultSortField`, `@defaultSortOrder`, `@pageSize`) — hunt hand-written grid column definitions + data hooks a data-grid layout generates. -- **CALIBRATION — TS/web-only:** the `view.*` widget subtypes exist only for TS/web consumers +- **CALIBRATION — TS/web-only:** the `view.*` widget subtypes are consumed only by TS/web and are NOT in the cross-port registry — `view.text`, `view.textarea`, `view.date`, - `view.month`, `view.hotlink`, `view.dropdown`, `view.radio`, `view.checkbox`, `view.number`, - `view.password`, `view.hidden`, `view.web`. **Audit these only for TS adopters.** Only + `view.month`, `view.hotlink`, `view.image`, `view.dropdown`, `view.radio`, `view.checkbox`, + `view.number`, `view.password`, `view.hidden`, `view.web`. Every port LOADS them, so shared metadata that + carries them is valid in a backend port; nothing outside TS/web reads them. **Audit these + only for TS adopters.** Only `view.base` / `view.currency` are cross-port-gated. ## Template — `template.*` (prompt pillar) diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md index bc1d7b5de..bbcde472f 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -560,7 +560,7 @@ The audit never edits code. Pattern: **dry-run → review the diff → apply**. `APIRouter`; relationship / non-`table` source-kind / `field.object flattened` codegen is partial. - **C#** has no ObjectManager runtime tier (EF Core is the runtime) — hand services over the generated `DbContext` are expected. - **Cut subtypes** — `field.byte` / `field.short` / `field.class` are removed; never recommend them. -- **TS/web-only** — `view.*` widget subtypes exist only for TS/web consumers; only `view.base` / `view.currency` are cross-port-gated. +- **TS/web-only** — `view.*` widget subtypes load in every port but are consumed only by TS/web; only `view.base` / `view.currency` are cross-port-gated. - **Planned, not shipped** — `api.*` / `operation.*` / `binding.*` (FR-024) and MCP exposure of declared prompts/tools are not yet in the registry; their absence is not an adopter defect. - **Cross-port version-NUMBER skew is by design** — TS/C#/Python `0.x` vs Java/Kotlin `7.x` Maven is correct; never flag the *number lines* differing. But that is exactly why you can't eyeball cross-language drift: compare **`metamodelVersion`** (Phase 0 cross-language consistency item), not the package numbers. A `metamodelVersion` MISMATCH across ports *is* a finding; so is a port lagging its ecosystem's latest release. Also flag *intra-port* drift (mixed versions within one port, or a runtime package in `devDependencies`). - **Stale upstream prose** — "hand-write the Spring controller" (Java/Kotlin) is out of date; trust `meta gen --list`, not stale prose. diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md index 1b38d09f2..61c563d4a 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md @@ -176,10 +176,12 @@ soft-delete / status / type view it models. options the currency view models. **Cross-port-gated** (with `view.base`). - **`layout.dataGrid`** (`@columns`, `@defaultSortField`, `@defaultSortOrder`, `@pageSize`) — hunt hand-written grid column definitions + data hooks a data-grid layout generates. -- **CALIBRATION — TS/web-only:** the `view.*` widget subtypes exist only for TS/web consumers +- **CALIBRATION — TS/web-only:** the `view.*` widget subtypes are consumed only by TS/web and are NOT in the cross-port registry — `view.text`, `view.textarea`, `view.date`, - `view.month`, `view.hotlink`, `view.dropdown`, `view.radio`, `view.checkbox`, `view.number`, - `view.password`, `view.hidden`, `view.web`. **Audit these only for TS adopters.** Only + `view.month`, `view.hotlink`, `view.image`, `view.dropdown`, `view.radio`, `view.checkbox`, + `view.number`, `view.password`, `view.hidden`, `view.web`. Every port LOADS them, so shared metadata that + carries them is valid in a backend port; nothing outside TS/web reads them. **Audit these + only for TS adopters.** Only `view.base` / `view.currency` are cross-port-gated. ## Template — `template.*` (prompt pillar) diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md index bc1d7b5de..bbcde472f 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -560,7 +560,7 @@ The audit never edits code. Pattern: **dry-run → review the diff → apply**. `APIRouter`; relationship / non-`table` source-kind / `field.object flattened` codegen is partial. - **C#** has no ObjectManager runtime tier (EF Core is the runtime) — hand services over the generated `DbContext` are expected. - **Cut subtypes** — `field.byte` / `field.short` / `field.class` are removed; never recommend them. -- **TS/web-only** — `view.*` widget subtypes exist only for TS/web consumers; only `view.base` / `view.currency` are cross-port-gated. +- **TS/web-only** — `view.*` widget subtypes load in every port but are consumed only by TS/web; only `view.base` / `view.currency` are cross-port-gated. - **Planned, not shipped** — `api.*` / `operation.*` / `binding.*` (FR-024) and MCP exposure of declared prompts/tools are not yet in the registry; their absence is not an adopter defect. - **Cross-port version-NUMBER skew is by design** — TS/C#/Python `0.x` vs Java/Kotlin `7.x` Maven is correct; never flag the *number lines* differing. But that is exactly why you can't eyeball cross-language drift: compare **`metamodelVersion`** (Phase 0 cross-language consistency item), not the package numbers. A `metamodelVersion` MISMATCH across ports *is* a finding; so is a port lagging its ecosystem's latest release. Also flag *intra-port* drift (mixed versions within one port, or a runtime package in `devDependencies`). - **Stale upstream prose** — "hand-write the Spring controller" (Java/Kotlin) is out of date; trust `meta gen --list`, not stale prose. diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md index 1b38d09f2..61c563d4a 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md @@ -176,10 +176,12 @@ soft-delete / status / type view it models. options the currency view models. **Cross-port-gated** (with `view.base`). - **`layout.dataGrid`** (`@columns`, `@defaultSortField`, `@defaultSortOrder`, `@pageSize`) — hunt hand-written grid column definitions + data hooks a data-grid layout generates. -- **CALIBRATION — TS/web-only:** the `view.*` widget subtypes exist only for TS/web consumers +- **CALIBRATION — TS/web-only:** the `view.*` widget subtypes are consumed only by TS/web and are NOT in the cross-port registry — `view.text`, `view.textarea`, `view.date`, - `view.month`, `view.hotlink`, `view.dropdown`, `view.radio`, `view.checkbox`, `view.number`, - `view.password`, `view.hidden`, `view.web`. **Audit these only for TS adopters.** Only + `view.month`, `view.hotlink`, `view.image`, `view.dropdown`, `view.radio`, `view.checkbox`, + `view.number`, `view.password`, `view.hidden`, `view.web`. Every port LOADS them, so shared metadata that + carries them is valid in a backend port; nothing outside TS/web reads them. **Audit these + only for TS adopters.** Only `view.base` / `view.currency` are cross-port-gated. ## Template — `template.*` (prompt pillar) diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md index bc1d7b5de..bbcde472f 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -560,7 +560,7 @@ The audit never edits code. Pattern: **dry-run → review the diff → apply**. `APIRouter`; relationship / non-`table` source-kind / `field.object flattened` codegen is partial. - **C#** has no ObjectManager runtime tier (EF Core is the runtime) — hand services over the generated `DbContext` are expected. - **Cut subtypes** — `field.byte` / `field.short` / `field.class` are removed; never recommend them. -- **TS/web-only** — `view.*` widget subtypes exist only for TS/web consumers; only `view.base` / `view.currency` are cross-port-gated. +- **TS/web-only** — `view.*` widget subtypes load in every port but are consumed only by TS/web; only `view.base` / `view.currency` are cross-port-gated. - **Planned, not shipped** — `api.*` / `operation.*` / `binding.*` (FR-024) and MCP exposure of declared prompts/tools are not yet in the registry; their absence is not an adopter defect. - **Cross-port version-NUMBER skew is by design** — TS/C#/Python `0.x` vs Java/Kotlin `7.x` Maven is correct; never flag the *number lines* differing. But that is exactly why you can't eyeball cross-language drift: compare **`metamodelVersion`** (Phase 0 cross-language consistency item), not the package numbers. A `metamodelVersion` MISMATCH across ports *is* a finding; so is a port lagging its ecosystem's latest release. Also flag *intra-port* drift (mixed versions within one port, or a runtime package in `devDependencies`). - **Stale upstream prose** — "hand-write the Spring controller" (Java/Kotlin) is out of date; trust `meta gen --list`, not stale prose. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md index 1b38d09f2..61c563d4a 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md @@ -176,10 +176,12 @@ soft-delete / status / type view it models. options the currency view models. **Cross-port-gated** (with `view.base`). - **`layout.dataGrid`** (`@columns`, `@defaultSortField`, `@defaultSortOrder`, `@pageSize`) — hunt hand-written grid column definitions + data hooks a data-grid layout generates. -- **CALIBRATION — TS/web-only:** the `view.*` widget subtypes exist only for TS/web consumers +- **CALIBRATION — TS/web-only:** the `view.*` widget subtypes are consumed only by TS/web and are NOT in the cross-port registry — `view.text`, `view.textarea`, `view.date`, - `view.month`, `view.hotlink`, `view.dropdown`, `view.radio`, `view.checkbox`, `view.number`, - `view.password`, `view.hidden`, `view.web`. **Audit these only for TS adopters.** Only + `view.month`, `view.hotlink`, `view.image`, `view.dropdown`, `view.radio`, `view.checkbox`, + `view.number`, `view.password`, `view.hidden`, `view.web`. Every port LOADS them, so shared metadata that + carries them is valid in a backend port; nothing outside TS/web reads them. **Audit these + only for TS adopters.** Only `view.base` / `view.currency` are cross-port-gated. ## Template — `template.*` (prompt pillar) diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md index bc1d7b5de..bbcde472f 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -560,7 +560,7 @@ The audit never edits code. Pattern: **dry-run → review the diff → apply**. `APIRouter`; relationship / non-`table` source-kind / `field.object flattened` codegen is partial. - **C#** has no ObjectManager runtime tier (EF Core is the runtime) — hand services over the generated `DbContext` are expected. - **Cut subtypes** — `field.byte` / `field.short` / `field.class` are removed; never recommend them. -- **TS/web-only** — `view.*` widget subtypes exist only for TS/web consumers; only `view.base` / `view.currency` are cross-port-gated. +- **TS/web-only** — `view.*` widget subtypes load in every port but are consumed only by TS/web; only `view.base` / `view.currency` are cross-port-gated. - **Planned, not shipped** — `api.*` / `operation.*` / `binding.*` (FR-024) and MCP exposure of declared prompts/tools are not yet in the registry; their absence is not an adopter defect. - **Cross-port version-NUMBER skew is by design** — TS/C#/Python `0.x` vs Java/Kotlin `7.x` Maven is correct; never flag the *number lines* differing. But that is exactly why you can't eyeball cross-language drift: compare **`metamodelVersion`** (Phase 0 cross-language consistency item), not the package numbers. A `metamodelVersion` MISMATCH across ports *is* a finding; so is a port lagging its ecosystem's latest release. Also flag *intra-port* drift (mixed versions within one port, or a runtime package in `devDependencies`). - **Stale upstream prose** — "hand-write the Spring controller" (Java/Kotlin) is out of date; trust `meta gen --list`, not stale prose. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md index 1b38d09f2..61c563d4a 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md @@ -176,10 +176,12 @@ soft-delete / status / type view it models. options the currency view models. **Cross-port-gated** (with `view.base`). - **`layout.dataGrid`** (`@columns`, `@defaultSortField`, `@defaultSortOrder`, `@pageSize`) — hunt hand-written grid column definitions + data hooks a data-grid layout generates. -- **CALIBRATION — TS/web-only:** the `view.*` widget subtypes exist only for TS/web consumers +- **CALIBRATION — TS/web-only:** the `view.*` widget subtypes are consumed only by TS/web and are NOT in the cross-port registry — `view.text`, `view.textarea`, `view.date`, - `view.month`, `view.hotlink`, `view.dropdown`, `view.radio`, `view.checkbox`, `view.number`, - `view.password`, `view.hidden`, `view.web`. **Audit these only for TS adopters.** Only + `view.month`, `view.hotlink`, `view.image`, `view.dropdown`, `view.radio`, `view.checkbox`, + `view.number`, `view.password`, `view.hidden`, `view.web`. Every port LOADS them, so shared metadata that + carries them is valid in a backend port; nothing outside TS/web reads them. **Audit these + only for TS adopters.** Only `view.base` / `view.currency` are cross-port-gated. ## Template — `template.*` (prompt pillar) diff --git a/fixtures/conformance/attr-properties-inline/expected.json b/fixtures/conformance/attr-properties-inline/expected.json new file mode 100644 index 000000000..97257c4ed --- /dev/null +++ b/fixtures/conformance/attr-properties-inline/expected.json @@ -0,0 +1,30 @@ +{ + "metadata.root": { + "package": "acme", + "children": [ + { + "object.entity": { + "name": "Order", + "@store": { + "collection": "orders" + }, + "children": [ + { + "field.long": { + "name": "id" + } + }, + { + "identity.primary": { + "name": "id", + "@fields": [ + "id" + ] + } + } + ] + } + } + ] + } +} diff --git a/fixtures/conformance/attr-properties-inline/input/meta.orders.json b/fixtures/conformance/attr-properties-inline/input/meta.orders.json new file mode 100644 index 000000000..140a52aab --- /dev/null +++ b/fixtures/conformance/attr-properties-inline/input/meta.orders.json @@ -0,0 +1,26 @@ +{ + "metadata.root": { + "package": "acme", + "children": [ + { + "object.entity": { + "name": "Order", + "@store": { "collection": "orders" }, + "children": [ + { + "field.long": { + "name": "id" + } + }, + { + "identity.primary": { + "name": "id", + "@fields": ["id"] + } + } + ] + } + } + ] + } +} diff --git a/fixtures/conformance/view-text-basic/expected.json b/fixtures/conformance/view-text-basic/expected.json new file mode 100644 index 000000000..8fc5e897d --- /dev/null +++ b/fixtures/conformance/view-text-basic/expected.json @@ -0,0 +1,51 @@ +{ + "metadata.root": { + "package": "acme", + "children": [ + { + "object.entity": { + "name": "Order", + "children": [ + { + "field.long": { + "name": "id" + } + }, + { + "field.string": { + "name": "note", + "children": [ + { + "view.text": { + "name": "form" + } + } + ] + } + }, + { + "field.string": { + "name": "status", + "children": [ + { + "view.dropdown": { + "name": "form" + } + } + ] + } + }, + { + "identity.primary": { + "name": "id", + "@fields": [ + "id" + ] + } + } + ] + } + } + ] + } +} diff --git a/fixtures/conformance/view-text-basic/input/meta.orders.json b/fixtures/conformance/view-text-basic/input/meta.orders.json new file mode 100644 index 000000000..8fc5e897d --- /dev/null +++ b/fixtures/conformance/view-text-basic/input/meta.orders.json @@ -0,0 +1,51 @@ +{ + "metadata.root": { + "package": "acme", + "children": [ + { + "object.entity": { + "name": "Order", + "children": [ + { + "field.long": { + "name": "id" + } + }, + { + "field.string": { + "name": "note", + "children": [ + { + "view.text": { + "name": "form" + } + } + ] + } + }, + { + "field.string": { + "name": "status", + "children": [ + { + "view.dropdown": { + "name": "form" + } + } + ] + } + }, + { + "identity.primary": { + "name": "id", + "@fields": [ + "id" + ] + } + } + ] + } + } + ] + } +} diff --git a/fixtures/registry-conformance/README.md b/fixtures/registry-conformance/README.md index 8b9f107e5..e06b1b0b1 100644 --- a/fixtures/registry-conformance/README.md +++ b/fixtures/registry-conformance/README.md @@ -253,18 +253,21 @@ them as such with a declared reason. only the concrete tree root (`metadata.root`). It is the not-universally-tracked `inheritsFrom` anchor this manifest already defers, so the `(metadata, base)` row is carved out. See SP-G analysis C-5. -- **`PRESENTATION_ONLY`** — the 11 generic `view.*` controls (`checkbox`, `date`, - `dropdown`, - `hidden`, `hotlink`, `month`, `number`, `password`, `radio`, `text`, - `textarea`, `web`) — a TS-web-PRESENTATION facet, like the TS-only `D1` +- **`PRESENTATION_ONLY`** — the 13 generic `view.*` controls (`checkbox`, `date`, + `dropdown`, `hidden`, `hotlink`, `image`, `month`, `number`, `password`, `radio`, + `text`, `textarea`, `web`) — a TS-web-PRESENTATION facet, like the TS-only `D1` dialect. They have zero backend / codegen / render consumers; only the TS web client (`client/web/packages/{tanstack,angular}/src/`) + TS form codegen (`server/typescript/packages/codegen-ts/src/templates/field-meta.ts`) consume - them. They stay **registered in TypeScript** (the loader must accept an - authored `view.dropdown`) but are **deregistered in C# + Python** (dead vocab - there) and **excluded from the manifest** by all four emitters. Only + them. They are **registered in every port** — TypeScript, C#, Java (so Kotlin) + and Python — so one metadata document loads everywhere (an authored + `view.dropdown` is not an `ERR_UNKNOWN_SUBTYPE` in a backend port; gated by the + `view-text-basic` conformance fixture), and **excluded from the manifest** by + all four emitters, because registration there is for LOADING only. Only `view.base` + `view.currency` (the cross-port currency `@locale` wire - contract) remain. See SP-G analysis B-2. + contract) are manifest rows. See SP-G analysis B-2; C#, Java and Python + originally left them unregistered, which made a document shared between a TS + web client and a backend port unloadable in the backend. ### EXCLUDED from v1 — deferred follow-ons (documented, not silent) @@ -380,8 +383,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 11 generic `view.*` controls are cut (Java - never registered them). The REMAINING (Phase 2) divergence between Java's + 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 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`/ diff --git a/server/csharp/MetaObjects/Parser.cs b/server/csharp/MetaObjects/Parser.cs index e22166a27..009c830f8 100644 --- a/server/csharp/MetaObjects/Parser.cs +++ b/server/csharp/MetaObjects/Parser.cs @@ -1597,6 +1597,26 @@ private static void ApplyInlineAttrsAndUnknownKeys( .AttrsOf(model.Type, model.SubType) .FirstOrDefault(a => a.Name == attrName); + // An UNDECLARED @-attr whose value is a JSON object is the registered + // `attr.properties` bag — materialize it exactly as the explicit + // `{"attr.properties": {...}}` child form does (structural attr child + + // SetAttr map entry), so the strict-attr pass exempts it. Mirrors TS + // parser-core inferUndeclaredAttrSubType. + if (attrSpec is null + && rawVal.ValueKind == JsonValueKind.Object + && st.Registry.Find(TYPE_ATTR, ATTR_SUBTYPE_PROPERTIES) is { } bagDef) + { + object bag = DataConverter.ToAttrObject(rawVal); + MetaData bagModel = bagDef.Factory(bagDef.TypeId, attrName); + st.Builder.PushKey(key); + bagModel.SetSource(st.CurrentSource()); + st.Builder.Pop(); + bagModel.SetAttr(RESERVED_KEY_VALUE, bag); + model.AddChild(bagModel); + model.SetAttr(attrName, bag); + continue; + } + object? value; try { diff --git a/server/csharp/MetaObjects/Presentation/View/ViewConstants.cs b/server/csharp/MetaObjects/Presentation/View/ViewConstants.cs index ee61dd3e1..e537ef81e 100644 --- a/server/csharp/MetaObjects/Presentation/View/ViewConstants.cs +++ b/server/csharp/MetaObjects/Presentation/View/ViewConstants.cs @@ -9,22 +9,49 @@ namespace MetaObjects.Presentation.View; /// /// View concern constants — the view subtypes + the currency-view formatting attrs. /// -/// SP-G Phase1 Unit3 (B-2): the 11 generic web-presentation control kinds -/// (text/textarea/date/month/hotlink/dropdown/radio/checkbox/number/password/ -/// hidden/web) are TS-web-presentation-only — they have NO backend / codegen / -/// render consumer in C#, so they are dead vocab here and were DEREGISTERED. -/// They remain registered in TypeScript (the web client + TS form codegen -/// require them for authoring). Only base + currency (the -/// cross-port currency @locale wire contract) are registered cross-port. +/// 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 C#, and the registry manifest excludes them +/// (RegistryManifest, 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. /// public static class ViewConstants { + public const string VIEW_SUBTYPE_TEXT = "text"; + public const string VIEW_SUBTYPE_TEXTAREA = "textarea"; + public const string VIEW_SUBTYPE_DATE = "date"; + public const string VIEW_SUBTYPE_MONTH = "month"; + public const string VIEW_SUBTYPE_HOTLINK = "hotlink"; + public const string VIEW_SUBTYPE_DROPDOWN = "dropdown"; + public const string VIEW_SUBTYPE_RADIO = "radio"; + public const string VIEW_SUBTYPE_CHECKBOX = "checkbox"; + public const string VIEW_SUBTYPE_NUMBER = "number"; + public const string VIEW_SUBTYPE_PASSWORD = "password"; + public const string VIEW_SUBTYPE_HIDDEN = "hidden"; + public const string VIEW_SUBTYPE_WEB = "web"; public const string VIEW_SUBTYPE_CURRENCY = "currency"; + public const string VIEW_SUBTYPE_IMAGE = "image"; public static readonly string[] VIEW_SUBTYPES = [ BaseTypes.SUBTYPE_BASE, + VIEW_SUBTYPE_TEXT, + VIEW_SUBTYPE_TEXTAREA, + VIEW_SUBTYPE_DATE, + VIEW_SUBTYPE_MONTH, + VIEW_SUBTYPE_HOTLINK, + VIEW_SUBTYPE_DROPDOWN, + VIEW_SUBTYPE_RADIO, + VIEW_SUBTYPE_CHECKBOX, + VIEW_SUBTYPE_NUMBER, + VIEW_SUBTYPE_PASSWORD, + VIEW_SUBTYPE_HIDDEN, + VIEW_SUBTYPE_WEB, VIEW_SUBTYPE_CURRENCY, + VIEW_SUBTYPE_IMAGE, ]; // ----------------------------------------------------------------------- diff --git a/server/csharp/MetaObjects/RegistryManifest.cs b/server/csharp/MetaObjects/RegistryManifest.cs index 8553b1ec2..5d013810f 100644 --- a/server/csharp/MetaObjects/RegistryManifest.cs +++ b/server/csharp/MetaObjects/RegistryManifest.cs @@ -39,8 +39,9 @@ public static class RegistryManifest // them as such; `description` stays in commonAttrs); // - `metadata.base` is a per-port inheritance anchor (Java's), not in the // cross-port contract — other ports register only `metadata.root`; - // - the 11 generic `view.*` controls are a TS-web-presentation facet (cut - // cross-port; C#/Python deregister them, TS keeps them registered). + // - the generic `view.*` controls are a TS-web-presentation facet. C# + // registers them for LOADING only (CoreTypes, via ViewConstants); this + // filter is what keeps them out of the cross-port manifest. // ------------------------------------------------------------------ // Wave 3b — the in/out boundary is an EXPLICIT CLASSIFICATION (a reason diff --git a/server/java/metadata/src/main/java/com/metaobjects/loader/parser/json/CanonicalJsonParser.java b/server/java/metadata/src/main/java/com/metaobjects/loader/parser/json/CanonicalJsonParser.java index 0e073d7be..9b764b647 100644 --- a/server/java/metadata/src/main/java/com/metaobjects/loader/parser/json/CanonicalJsonParser.java +++ b/server/java/metadata/src/main/java/com/metaobjects/loader/parser/json/CanonicalJsonParser.java @@ -1432,6 +1432,20 @@ private void processAttributes(MetaData md, JsonObject body) { // This lets the base parser's existing [...] → comma-delimited → isArray=true // path fire correctly, without any hardcoded attribute names. JsonElement rawValue = entry.getValue(); + // An UNDECLARED @-attr whose value is a JSON object is the registered + // attr.properties bag — materialize it as that subtype, exactly as the + // explicit {"attr.properties": {...}} child form does. The base parser's + // value-shape inference knows only scalars and arrays and would store the + // bag as a JSON-text STRING attr. Mirrors TS inferUndeclaredAttrSubType. + if (isExemptPropertiesAttr(rawValue) && !isDeclaredAttribute(md, attrName)) { + MetaAttribute bag = (MetaAttribute) getTypeRegistry().createInstance( + MetaAttribute.TYPE_ATTR, + com.metaobjects.attr.PropertiesAttribute.SUBTYPE_PROPERTIES, attrName); + md.addChild(bag); + bag.setValueAsString(jsonElementToString(rawValue)); + tagAttrChildWithJsonSource(md, attrName); + continue; + } // Cross-port: object-valued attrs (e.g. attr.filter) MUST be authored as // a JSON object. The legacy form `@filter: "..."` (string-quoted JSON) is // rejected here so the loader surfaces ERR_BAD_ATTR_VALUE before the diff --git a/server/java/metadata/src/main/java/com/metaobjects/registry/RegistryManifest.java b/server/java/metadata/src/main/java/com/metaobjects/registry/RegistryManifest.java index 22d469d6f..c2d7a8b43 100644 --- a/server/java/metadata/src/main/java/com/metaobjects/registry/RegistryManifest.java +++ b/server/java/metadata/src/main/java/com/metaobjects/registry/RegistryManifest.java @@ -288,8 +288,9 @@ public static List metamodelProviders() { // no-ops for TS/C#/Python. `description` stays in the commonAttrs block. // - metadata.base is Java's internal inheritance anchor — other ports // register only metadata.root (the deferred inheritsFrom anchor). - // - the 11 generic view.* controls are a TS-web-presentation facet (Java - // never registered them anyway; the filter keeps the contract explicit). + // - the generic view.* controls are a TS-web-presentation facet. Java + // registers them for LOADING only (PresentationView); this filter is what + // keeps them out of the cross-port manifest. // ------------------------------------------------------------------ /** 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 new file mode 100644 index 000000000..03503f2e1 --- /dev/null +++ b/server/java/metadata/src/main/java/com/metaobjects/view/PresentationView.java @@ -0,0 +1,56 @@ +package com.metaobjects.view; + +import com.metaobjects.registry.MetaDataRegistry; + +import java.util.List; + +/** + * The generic web-presentation view controls ({@code view.text}, + * {@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}). + * One class serves every control kind because a view carries no per-subtype + * behavior here.

+ * + *

Mirrors {@code VIEW_SUBTYPES} in the TypeScript + * {@code presentation/view/view-constants.ts}.

+ */ +public class PresentationView extends MetaView { + + public static final String SUBTYPE_TEXT = "text"; + public static final String SUBTYPE_TEXTAREA = "textarea"; + public static final String SUBTYPE_DATE = "date"; + public static final String SUBTYPE_MONTH = "month"; + public static final String SUBTYPE_HOTLINK = "hotlink"; + public static final String SUBTYPE_DROPDOWN = "dropdown"; + public static final String SUBTYPE_RADIO = "radio"; + public static final String SUBTYPE_CHECKBOX = "checkbox"; + public static final String SUBTYPE_NUMBER = "number"; + public static final String SUBTYPE_PASSWORD = "password"; + public static final String SUBTYPE_HIDDEN = "hidden"; + public static final String SUBTYPE_WEB = "web"; + public static final String SUBTYPE_IMAGE = "image"; + + /** Every generic presentation control kind, in the TypeScript declaration order. */ + public static final List SUBTYPES = List.of( + SUBTYPE_TEXT, SUBTYPE_TEXTAREA, SUBTYPE_DATE, SUBTYPE_MONTH, SUBTYPE_HOTLINK, + SUBTYPE_DROPDOWN, SUBTYPE_RADIO, SUBTYPE_CHECKBOX, SUBTYPE_NUMBER, SUBTYPE_PASSWORD, + SUBTYPE_HIDDEN, SUBTYPE_WEB, SUBTYPE_IMAGE); + + public PresentationView(String type, String subType, String name) { + super(type, subType, name); + } + + public static void registerTypes(MetaDataRegistry registry) { + for (String subType : SUBTYPES) { + registry.registerType(PresentationView.class, def -> def + .type(TYPE_VIEW).subType(subType) + .description("Web presentation control (" + subType + ") — loads in every port; " + + "consumed by the TypeScript web client only") + .inheritsFrom(TYPE_VIEW, SUBTYPE_BASE)); + } + } +} diff --git a/server/java/metadata/src/main/java/com/metaobjects/view/ViewTypesMetaDataProvider.java b/server/java/metadata/src/main/java/com/metaobjects/view/ViewTypesMetaDataProvider.java index 8d5086376..9864f68c7 100644 --- a/server/java/metadata/src/main/java/com/metaobjects/view/ViewTypesMetaDataProvider.java +++ b/server/java/metadata/src/main/java/com/metaobjects/view/ViewTypesMetaDataProvider.java @@ -10,8 +10,8 @@ * concrete subtypes so they are discoverable through the unified registry. * *

Cross-port: the TS/C# ports surface a "view-types" provider for the - * same vocabulary (today: {@code view.currency}; future: validation views, - * display views). Priority 10 mirrors the field/attr providers — runs after + * same vocabulary: {@code view.currency} plus the generic web-presentation + * controls ({@link PresentationView}), which are registered for loading only. Priority 10 mirrors the field/attr providers — runs after * {@code core-types} (0) so {@code metadata.base} is available.

* * @since 7.0.0 @@ -24,6 +24,7 @@ public class ViewTypesMetaDataProvider implements MetaDataTypeProvider { public void registerTypes(MetaDataRegistry registry) { MetaView.registerTypes(registry); CurrencyView.registerTypes(registry); + PresentationView.registerTypes(registry); log.debug("View types registered via provider"); } 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 472813ff6..c395cec38 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 @@ -110,10 +110,12 @@ * ADR-0001/ADR-0005, kept registered but excluded from the manifest) were * reconciled in SP-G Unit 6b/6b-finish; the {@code value*}/{@code data*} * vestigial sets were refactor-dropped (Unit 6b). - *
  • Subtype gaps/extras: Java lacks {@code attr.stringarray} and the 11 generic {@code view.*} subtypes - * (checkbox/date/dropdown/hidden/hotlink/month/number/password/radio/text/ - * textarea/web), and carries an extra {@code metadata.base} (its - * inheritance anchor; the other ports register only {@code metadata.root}).
  • + *
  • Subtype gaps/extras: Java lacks {@code attr.stringarray} (retired + * vocabulary) and carries an extra {@code metadata.base} (its + * inheritance anchor; the other ports register only {@code metadata.root}). + * The generic {@code view.*} presentation controls — which Java formerly + * did not register (SP-G B-2) — now register for LOADING only + * ({@code PresentationView}) and stay excluded from the manifest.
  • * *

    SP-G Unit 6a reconciled (registration-only structural items): * required-ness flips ({@code identity.primary/secondary/reference.fields}, 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 a9b44f6d0..51f7a182c 100644 --- a/server/python/src/metaobjects/meta/presentation/view/view_constants.py +++ b/server/python/src/metaobjects/meta/presentation/view/view_constants.py @@ -2,20 +2,49 @@ The view's subType IS the UI control type. -SP-G Phase1 Unit3 (B-2): the 11 generic web-presentation control kinds -(text/textarea/date/month/hotlink/dropdown/radio/checkbox/number/password/ -hidden/web) are TS-web-presentation-only — they have NO backend / codegen / -render consumer in Python, so they are dead vocab here and were DEREGISTERED. -They remain registered in TypeScript (the web client + TS form codegen require -them for authoring). Only ``base`` + ``currency`` (the cross-port currency -``@locale`` wire contract) are registered cross-port. +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. + +Mirrors typescript/packages/metadata/src/presentation/view/view-constants.ts. """ from ....shared.base_types import SUBTYPE_BASE +VIEW_SUBTYPE_TEXT = "text" +VIEW_SUBTYPE_TEXTAREA = "textarea" +VIEW_SUBTYPE_DATE = "date" +VIEW_SUBTYPE_MONTH = "month" +VIEW_SUBTYPE_HOTLINK = "hotlink" +VIEW_SUBTYPE_DROPDOWN = "dropdown" +VIEW_SUBTYPE_RADIO = "radio" +VIEW_SUBTYPE_CHECKBOX = "checkbox" +VIEW_SUBTYPE_NUMBER = "number" +VIEW_SUBTYPE_PASSWORD = "password" +VIEW_SUBTYPE_HIDDEN = "hidden" +VIEW_SUBTYPE_WEB = "web" VIEW_SUBTYPE_CURRENCY = "currency" +VIEW_SUBTYPE_IMAGE = "image" VIEW_SUBTYPES = ( SUBTYPE_BASE, + VIEW_SUBTYPE_TEXT, + VIEW_SUBTYPE_TEXTAREA, + VIEW_SUBTYPE_DATE, + VIEW_SUBTYPE_MONTH, + VIEW_SUBTYPE_HOTLINK, + VIEW_SUBTYPE_DROPDOWN, + VIEW_SUBTYPE_RADIO, + VIEW_SUBTYPE_CHECKBOX, + VIEW_SUBTYPE_NUMBER, + VIEW_SUBTYPE_PASSWORD, + VIEW_SUBTYPE_HIDDEN, + VIEW_SUBTYPE_WEB, VIEW_SUBTYPE_CURRENCY, + VIEW_SUBTYPE_IMAGE, ) # view.currency attrs — @locale (BCP 47). Only on view.currency; defaults to "en-US". diff --git a/server/python/src/metaobjects/parser.py b/server/python/src/metaobjects/parser.py index 4c8a25a14..44036de5b 100644 --- a/server/python/src/metaobjects/parser.py +++ b/server/python/src/metaobjects/parser.py @@ -5,7 +5,7 @@ from typing import cast from .errors import ErrorCode, MetaError -from .meta.core.attr.attr_constants import ATTR_SUBTYPE_STRINGARRAY +from .meta.core.attr.attr_constants import ATTR_SUBTYPE_PROPERTIES, ATTR_SUBTYPE_STRINGARRAY from .meta.meta_data import MetaData from .meta.meta_root import MetaRoot from .naming_refs import REF_BEARING_ATTR_NAMES, is_relative_ref @@ -460,6 +460,12 @@ def _build( if schema is not None and schema.is_array else (schema.value_type if schema else None) ) + # An UNDECLARED attr whose value is a JSON object is the registered + # `attr.properties` bag, exactly as the explicit + # `{"attr.properties": {...}}` child form loads it — so strict-attr + # exempts it. Mirrors TS parser-core inferUndeclaredAttrSubType. + if schema is None and isinstance(value, dict): + attr_sub_type = ATTR_SUBTYPE_PROPERTIES node.set_attr(attr_name, value, sub_type=attr_sub_type) # FR5a / ADR-0009 — stamp the just-constructed MetaAttribute node with # its origin envelope. Mirrors C# Parser.cs:1039 (attrModel.SetSource). diff --git a/server/typescript/packages/metadata/src/registry-manifest-exclusions.ts b/server/typescript/packages/metadata/src/registry-manifest-exclusions.ts index 8d0e8f5e6..47e71ce21 100644 --- a/server/typescript/packages/metadata/src/registry-manifest-exclusions.ts +++ b/server/typescript/packages/metadata/src/registry-manifest-exclusions.ts @@ -38,8 +38,8 @@ // — 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. Stay REGISTERED in TS; deregistered in -// C#/Python; EXCLUDED from the manifest everywhere. +// dialect. REGISTERED in every port, for LOADING +// only; EXCLUDED from the manifest everywhere. // // The boundary is now an EXPLICIT classification (not a silent default): the // port-private set is enumerated WITH a reason category, and a TRIPWIRE diff --git a/server/typescript/packages/metadata/src/vocabulary-catalog.ts b/server/typescript/packages/metadata/src/vocabulary-catalog.ts index 14ce3271e..953b16826 100644 --- a/server/typescript/packages/metadata/src/vocabulary-catalog.ts +++ b/server/typescript/packages/metadata/src/vocabulary-catalog.ts @@ -2,8 +2,8 @@ // // `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 stay REGISTERED in TypeScript (the loader -// must accept an authored `view.dropdown`) but are deregistered in C# and Python and +// 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). // diff --git a/server/typescript/packages/metadata/test/registry-conformance.test.ts b/server/typescript/packages/metadata/test/registry-conformance.test.ts index 3a3e060ad..1ab38aece 100644 --- a/server/typescript/packages/metadata/test/registry-conformance.test.ts +++ b/server/typescript/packages/metadata/test/registry-conformance.test.ts @@ -67,7 +67,7 @@ test("manifest excludes the metadata.base inheritance anchor (keeps metadata.roo expect(metadataSubTypes).toContain("root"); }); -test("manifest cuts the 11 generic view.* controls, keeps view.base + view.currency", () => { +test("manifest cuts the 13 generic view.* controls, keeps view.base + view.currency", () => { const manifest = buildRegistryManifest(composeRegistry(coreProviders)); const viewSubTypes = manifest.types .filter((t) => t.type === "view") diff --git a/server/typescript/packages/sdk/test/agent-context-capability-grounding.test.ts b/server/typescript/packages/sdk/test/agent-context-capability-grounding.test.ts index 1677d3f8d..321cba22f 100644 --- a/server/typescript/packages/sdk/test/agent-context-capability-grounding.test.ts +++ b/server/typescript/packages/sdk/test/agent-context-capability-grounding.test.ts @@ -12,9 +12,10 @@ const REGISTRY = join(ROOT, "fixtures/registry-conformance/expected-registry.jso const EXEMPT_SUBTYPES = new Set([ // cut stubs (named only to say "do NOT audit for them") "field.byte", "field.short", "field.class", - // TS-only view widgets (not in the cross-port registry; flagged TS-only) - "view.text", "view.textarea", "view.date", "view.month", "view.hotlink", "view.dropdown", - "view.radio", "view.checkbox", "view.number", "view.password", "view.hidden", "view.web", + // TS-consumed view widgets (every port loads them; not in the cross-port registry) + "view.text", "view.textarea", "view.date", "view.month", "view.hotlink", "view.image", + "view.dropdown", "view.radio", "view.checkbox", "view.number", "view.password", "view.hidden", + "view.web", // planned, not yet registered (flagged "not yet in the registry") "api.base", "api.operational", "operation.query", "operation.command", "binding.rest", ]); diff --git a/spec/decisions/ADR-0054-base-subtypes-are-not-authorable.md b/spec/decisions/ADR-0054-base-subtypes-are-not-authorable.md index 9fa769ec8..008fa66b1 100644 --- a/spec/decisions/ADR-0054-base-subtypes-are-not-authorable.md +++ b/spec/decisions/ADR-0054-base-subtypes-are-not-authorable.md @@ -124,3 +124,16 @@ node that declares an anchor rather than a thing has no meaning for codegen or r **Report the divergence and change nothing.** Rejected: the report was the cheap half, and leaving one document with two verdicts is the failure the corpora exist to prevent. + +## Amendment 1 (2026-10-04) — the concrete `view.*` controls now load in every port + +The Consequences paragraph above states that the ports without the TS-side UI provider have +"`view.currency` and nothing else." That was the SP-G B-2 deregistration ruling, and a +loader-parity fix has since superseded its registration half: every port now registers the generic +`view.*` controls (`view.text`, `view.dropdown`, …) for LOADING, so one document carrying them +loads everywhere. They stay TS-web-presentation-only — no backend consumer reads them — and stay +excluded from the cross-port manifest as PRESENTATION_ONLY +(`fixtures/registry-conformance/README.md`); the attrs ON those controls remain registered only by +the TypeScript UI provider. Nothing here reopens the `view.base` removal: the anchors stay +authorable-nowhere, and this amendment only retires the claim that a backend port cannot load a +concrete view.