Skip to content

Commit e471ad8

Browse files
dmealingclaude
andcommitted
Merge WA2: Java object representation redesign (EntityMetaObject/ValueMetaObject + @objectadapter) [ADR-0005]
Collapses the object-representation sprawl (Pojo/Mapped/Proxy/Data/dynamic-Value/Managed) to two semantic classes — EntityMetaObject (object.entity) + ValueMetaObject (object.value) — backed by one live-object reflection/map hybrid in AbstractObjectRepresentation, with an optional Java-only @objectadapter FQN extension hook (proxy demoted to a reference example). Fixes the omdb ValueObject NoSuchMethodError regression. Portable vocabulary unchanged (entity/value); @objectadapter never enters conformance. ValueObject runtime relocated into metadata. Reactor green except the 2 known pre-existing CanonicalJsonParserTest corpus NPEs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2 parents c99880b + 606cbdc commit e471ad8

96 files changed

Lines changed: 2149 additions & 1849 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/superpowers/plans/2026-05-23-wa2-object-entity-value-representation.md

Lines changed: 252 additions & 0 deletions
Large diffs are not rendered by default.

docs/superpowers/plans/2026-05-23-wa2-object-representation-redesign.md

Lines changed: 409 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Java Object Representation Redesign — two MetaObjects + a thin adapter hook
2+
3+
**Status:** Design approved 2026-05-23. Supersedes the `ObjectRepresentationResolver` design from WA2 (the in-progress "WA2 — object.entity/value + binding-resolved representation" plan). Amends [ADR-0005](../../../spec/decisions/ADR-0005-object-representation-binding.md).
4+
5+
**Scope:** Java port only. The cross-language metamodel vocabulary is unaffected (it stays `object.entity` / `object.value`); no other port (TS, C#, Python) gains anything from this.
6+
7+
---
8+
9+
## 1. Problem
10+
11+
WA2 set out to move Java objects to the cross-language standard (`object.entity` / `object.value`) and resolve the Java *representation* (reflection vs map vs proxy) from the binding, per ADR-0005's resolver model. While retiring the `metadata`-module representation subtypes (`object.pojo`/`map`/`proxy`), running the **full** reactor surfaced an architecture the original WA2 research never saw: the `dynamic` module already carries a second, richer representation hierarchy that `ObjectManager` actually uses.
12+
13+
```
14+
MetaObject (abstract, metadata)
15+
├── PojoMetaObject (metadata, "pojo") — reflection get/set
16+
│ └── DataMetaObject (dynamic, "data") — HYBRID: map for ValueObject/DataObject, reflection otherwise
17+
│ └── ValueMetaObject (dynamic, "value") — map-backed, runtime object = ValueObject
18+
│ └── ManagedMetaObject (om, "managed") — stateful pojo
19+
├── MappedMetaObject (metadata, "map") — map get/set
20+
└── ProxyMetaObject (metadata, "proxy") — dynamic proxy over an interface
21+
```
22+
23+
Two concrete failures fell out of this:
24+
25+
1. **`object.value` already existed.** `ValueMetaObject` (dynamic) registers `object.value`. WA2's Task 2 registered `object.value → MappedMetaObject`, producing a duplicate-registration collision once `dynamic` is on the classpath.
26+
2. **The resolver's heuristic is wrong for this codebase.** ADR-0005 says "bound concrete class → reflection." But the omdb fixtures bind `@object="com.metaobjects.object.value.ValueObject"` — a *map-backed* concrete class. The resolver dutifully picked `PojoMetaObject` → reflection → `NoSuchMethodError: No getter exists named [...] on ValueObject` (3 omdb tests: `JsonbFieldDBTest.testValueObjectFallbackWhenNoBinding`, `testTypedJsonbRoundTrip`, `FruitDBTest.testBasket`).
27+
28+
Root cause: representation is **per-live-object**, not per-declared-class, and the existing `DataMetaObject` already knew that (it checks `instanceof ValueObject` at access time). The per-node resolver was the wrong layer, and it couldn't even see the `dynamic` classes (`metadata` is upstream of `dynamic`).
29+
30+
A secondary observation: the whole proxy/data/map/managed sprawl was, in the maintainer's words, "super sophisticated crap" — theoretical flexibility almost nobody uses. The practical representations are exactly two: a **POJO** (reflection) and a **value object** (map).
31+
32+
## 2. Decisions (locked during brainstorming, 2026-05-23)
33+
34+
1. **Two MetaObject classes only:** `EntityMetaObject` (`object.entity`) and `ValueMetaObject` (`object.value`). Delete `PojoMetaObject`, `MappedMetaObject`, `ProxyMetaObject`, `DataMetaObject`, `ManagedMetaObject`.
35+
2. **Built-in representations = two**, baked into a shared base as one hybrid: reflection-on-a-bound-POJO, and map-backed (`ValueObject`). Chosen by the live object + whether a class is bound — **no attribute in the common case.**
36+
3. **One extension seam:** an optional **Java-only** `@objectAdapter="<FQN>"` attribute naming a class that implements a tiny 3-method `ObjectAdapter` interface. The MetaObject instantiates it and delegates. **No registry, no ServiceLoader** — just instantiate-and-delegate. Keeps `entity`/`value` as the only subtypes.
37+
4. **Proxy is demoted** out of core to a test/example `ProxyObjectAdapter` that demonstrates the `@objectAdapter` hook.
38+
5. **Cross-language:** representation stays a Java-runtime concern. `@objectAdapter` is **not** in the conformance vocabulary; other ports ignore it. It is the narrow, FQN-based, never-required successor to the retired `@javaRuntime`.
39+
40+
## 3. Design
41+
42+
### 3.1 The two classes + shared hybrid base
43+
44+
`metadata` defines two concrete object subtypes sharing the common value-access logic (placed on an abstract base — either `MetaObject` itself or a small intermediate such as `AbstractObjectRepresentation`; the plan picks the cleanest seam that does not bloat `MetaObject`):
45+
46+
- **`EntityMetaObject`**`object.entity` — semantic: persistent identity (has an `identity.primary`).
47+
- **`ValueMetaObject`**`object.value` — semantic: no identity (embedded / value object).
48+
49+
Both register via the existing `MetaDataTypeProvider` pattern, each `inheritsFrom(object, base)` so they inherit base's attr/child placement rules (verified in WA2 Task 3 that base-level placement is inherited).
50+
51+
The semantic difference drives codegen/persistence (identity, embedding), **not** runtime value access — both use the same hybrid (3.2). Keeping them as two thin classes (rather than one class + a flag) matches the registry's one-impl-class-per-subtype model and reads clearly.
52+
53+
### 3.2 Built-in representation: the hybrid (no attribute)
54+
55+
The shared base implements value access keyed on the **live object**, mirroring today's `DataMetaObject`:
56+
57+
- `getValue(field, obj)` / `setValue(field, obj, value)`: if `obj instanceof ValueObject` (or a plain `Map`) → map access; otherwise → reflection (getter/setter on a bound POJO).
58+
- `newInstance()`: if a Java class is bound for this object's FQN (via the `@object` attribute or `ObjectClassRegistry`) → instantiate it reflectively; else → `new ValueObject(this)`. `ValueMetaObject` defaults to `ValueObject` regardless (a value object is map-backed by nature).
59+
60+
Properties:
61+
- **Zero-config** for the two real cases (bound POJO; unbound/value → ValueObject).
62+
- **Robust:** because dispatch is on the live object, a `ValueObject` is always read as a map even if a class name was present — this is the direct fix for the omdb `NoSuchMethodError`.
63+
- `getDefaultObjectClass()` and `produces(obj)` equivalents are preserved on the new classes (ObjectManager relies on `newInstance`/`produces`).
64+
65+
### 3.3 The `@objectAdapter` hook
66+
67+
```java
68+
package com.metaobjects.object;
69+
70+
/** Java-only extension seam for a custom object representation (e.g. a dynamic proxy).
71+
* Selected per object node via the @objectAdapter attribute (an FQN). Not portable. */
72+
public interface ObjectAdapter {
73+
Object newInstance(MetaObject mo);
74+
Object getValue(MetaObject mo, MetaField field, Object obj);
75+
void setValue(MetaObject mo, MetaField field, Object obj, Object value);
76+
}
77+
```
78+
79+
- The Java-only attribute `@objectAdapter` (a `string` holding an FQN) is registered as an optional attribute on `object.base` (so both entity and value inherit it). It is **not** added to the cross-language conformance corpus.
80+
- When present on a node, the MetaObject resolves the class via `Class.forName` (+ the loader's class loader), instantiates it once (no-arg ctor), caches it on the instance, and delegates `newInstance`/`getValue`/`setValue` to it. When absent → the built-in hybrid (3.2).
81+
- Missing/invalid `@objectAdapter` class → a clear `MetaDataException` (this *is* an explicit user request, unlike the permissive `@object` fallback, so failing loud is correct).
82+
83+
### 3.4 Proxy as the reference extension example
84+
85+
`ProxyMetaObject` / `ProxyObject` leave core. The capability is rebuilt as a `ProxyObjectAdapter implements ObjectAdapter` living in **test/example** scope (e.g. under `metadata` test sources, package `com.metaobjects.test.proxy`). `newInstance` builds a `java.lang.reflect.Proxy` over the interface named by the node's `@object`; `getValue`/`setValue` route through the proxy's invocation handler.
86+
87+
The fruitbasket-proxy fixtures change from `object.proxy` to `object.entity` + `@objectAdapter="com.metaobjects.test.proxy.fruitbasket.ProxyObjectAdapter"` (keeping `@object=<interface FQN>`). `ProxyObjectTests` (which asserts `java.lang.reflect.Proxy.isProxyClass(...)`) stays green — living proof the hook works end to end.
88+
89+
### 3.5 Module placement
90+
91+
Consolidate the runtime representation into `metadata` (the durable core), which also serves the WA4 goal of shrinking module sprawl:
92+
93+
- **Move into `metadata`:** `ValueObject` (the map-backed runtime), `ObjectAdapter`, and the new `EntityMetaObject` / `ValueMetaObject`.
94+
- **Preserve FQNs where possible** to minimize import/fixture churn — e.g. keep `ValueObject` at package `com.metaobjects.object.value` so existing `@object="com.metaobjects.object.value.ValueObject"` fixtures and `import` statements stay valid; only the owning jar changes.
95+
- `DataObject` folds into `ValueObject` (or is dropped if unreferenced after the collapse).
96+
- This eliminates the cross-module direction problem: the built-ins live entirely in upstream `metadata`; no downstream module is required for them.
97+
98+
### 3.6 Cross-language stance + ADR-0005 amendment
99+
100+
- The portable metamodel vocabulary remains **`object.entity` / `object.value`** only. No representation/adapter concept enters the shared spec or the conformance corpus.
101+
- Other ports are data-oriented (the generated code *is* the representation); they need none of this and ignore `@objectAdapter` if present.
102+
- ADR-0005 is amended: replace the "`@object` → registry → default picks a `Pojo`/`Proxy`/`Map` *class*" resolver with "two semantic classes carrying a built-in reflection/map hybrid, plus an optional Java-only `@objectAdapter` FQN hook." `@objectAdapter` is documented as the minimal successor to the retired `@javaRuntime`.
103+
104+
## 4. What changes
105+
106+
**Deleted:** `PojoMetaObject`, `MappedMetaObject`, `ProxyMetaObject` (metadata), `DataMetaObject`, `ValueMetaObject`-as-`object.value`-via-dynamic (dynamic), `ManagedMetaObject` (om), `ObjectRepresentationResolver` (the WA2 Task 1 class). Their `registerTypes`/constraint helpers go with them.
107+
108+
**Added:** `EntityMetaObject`, the new `ValueMetaObject`, `ObjectAdapter` (metadata); a test/example `ProxyObjectAdapter`.
109+
110+
**Moved into `metadata`:** `ValueObject` (FQN preserved), `ObjectAdapter`.
111+
112+
**Kept from the committed WA2 branch (Tasks 1–4b):** `object.entity`/`object.value` as registered subtypes; fixture migrations to `object.entity`; retirement of pojo/proxy/map subtype registration; test-assertion migrations to entity/value. Only the resolver and Task 2's representation-class wiring are replaced.
113+
114+
**Touched (call-site updates):** `om`/`omdb`/`dynamic` references to the deleted classes are re-pointed at `EntityMetaObject`/`ValueMetaObject` or the polymorphic `MetaObject` API; `ObjectManagerDB.createFromTemplate` and `ExpressionParser`'s `new PojoMetaObject(...)` updated.
115+
116+
## 5. Testing
117+
118+
- **Unit:** the hybrid base (reflection path on a bound POJO; map path on a `ValueObject`/`Map`; `newInstance` bound vs unbound); the `@objectAdapter` delegation (present → delegates; absent → hybrid; bad FQN → `MetaDataException`).
119+
- **Representation conformance:** `EntityValueRepresentationTest` (from WA2) updated — entity bound→reflection-backed instance, unbound→ValueObject; value→ValueObject; canonical output is `object.entity`/`object.value`, never a representation name; never `javaRuntime`/`objectAdapter` leaking into the portable corpus.
120+
- **Proxy example:** `ProxyObjectTests` green via the `ProxyObjectAdapter` hook.
121+
- **omdb regression:** the 3 failing tests (`JsonbFieldDBTest.testValueObjectFallbackWhenNoBinding`, `testTypedJsonbRoundTrip`, `FruitDBTest.testBasket`) go green.
122+
- **Full reactor green** (except the 2 known pre-existing CWD-dependent `CanonicalJsonParserTest` corpus NPEs) is the merge gate, followed by the standing review+simplify gate.
123+
124+
## 6. Out of scope / non-goals
125+
126+
- No change to the cross-language vocabulary, conformance corpus, or other ports.
127+
- No general-purpose adapter registry / ServiceLoader discovery (explicitly rejected as YAGNI).
128+
- No new representation beyond reflection + map built-in; proxy and anything exotic ride the `@objectAdapter` hook.
129+
- WA3 (`source.*`/`origin.*`), WA4 (loader→metadata / collapse core), WA5 (conformance gate) remain separate work-areas; this redesign only *aligns with* WA4's consolidation direction (moving `ValueObject` into `metadata`).
130+
131+
## 7. Relationship to the WA2 work-area
132+
133+
This becomes the corrected WA2: the existing branch (`worktree-wa2-entity-value-representation`, Tasks 1–4b committed) is built upon — keep the valid migrations, delete `ObjectRepresentationResolver` and the resolver wiring, and implement §3. The WA2 implementation plan is rewritten to match this design. `main` is untouched throughout; integration is forward-merge only.

server/java/archetype/src/main/resources/archetype-resources/src/main/resources/metadata/application-metadata.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"package": "domain",
44
"children": [
55
{
6-
"object.pojo": {
6+
"object.entity": {
77
"name": "User",
88
"children": [
99
{
@@ -54,7 +54,7 @@
5454
}
5555
},
5656
{
57-
"object.pojo": {
57+
"object.entity": {
5858
"name": "Order",
5959
"children": [
6060
{
@@ -120,7 +120,7 @@
120120
}
121121
},
122122
{
123-
"object.pojo": {
123+
"object.entity": {
124124
"name": "OrderItem",
125125
"children": [
126126
{

server/java/codegen-base/src/main/java/com/metaobjects/generator/direct/metadata/ai/MetaDataAIDocumentationGenerator.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,6 @@ public static void registerAIDocAttributes(com.metaobjects.registry.MetaDataRegi
170170
.optionalAttribute(AI_EXTENSION_GUIDANCE, "string")
171171
.optionalAttribute(AI_CROSS_LANGUAGE_INFO, "string");
172172

173-
registry.findType("object", "pojo")
174-
.optionalAttribute(AI_DESCRIPTION, "string")
175-
.optionalAttribute(AI_BUSINESS_RULE, "string");
176-
177173
// Field-level AI Documentation attributes
178174
registry.findType("field", "base")
179175
.optionalAttribute(AI_DESCRIPTION, "string")

server/java/codegen-base/src/main/java/com/metaobjects/generator/direct/metadata/ai/MetaDataAIDocumentationWriter.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,19 +399,19 @@ private JsonObject generateCrossLanguageSupport() {
399399
JsonObject javaMapping = new JsonObject();
400400
javaMapping.addProperty("fieldString", "StringField.class");
401401
javaMapping.addProperty("fieldInt", "IntegerField.class");
402-
javaMapping.addProperty("objectPojo", "PojoMetaObject.class");
402+
javaMapping.addProperty("objectEntity", "EntityMetaObject.class");
403403
typeMappings.add("java", javaMapping);
404404

405405
JsonObject csharpMapping = new JsonObject();
406406
csharpMapping.addProperty("fieldString", "StringField");
407407
csharpMapping.addProperty("fieldInt", "IntegerField");
408-
csharpMapping.addProperty("objectPojo", "PojoMetaObject");
408+
csharpMapping.addProperty("objectEntity", "EntityMetaObject");
409409
typeMappings.add("csharp", csharpMapping);
410410

411411
JsonObject tsMapping = new JsonObject();
412412
tsMapping.addProperty("fieldString", "StringFieldType");
413413
tsMapping.addProperty("fieldInt", "IntegerFieldType");
414-
tsMapping.addProperty("objectPojo", "PojoMetaObjectType");
414+
tsMapping.addProperty("objectEntity", "EntityMetaObjectType");
415415
typeMappings.add("typescript", tsMapping);
416416

417417
crossLang.add("languageMappings", typeMappings);

server/java/codegen-base/src/main/java/com/metaobjects/generator/direct/metadata/file/json/MetaDataFileJsonSchemaGenerator.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,13 @@ public static void registerJsonSchemaAttributes(com.metaobjects.registry.MetaDat
100100
.optionalAttribute(JSON_SCHEMA_VERSION, "string")
101101
.optionalAttribute(JSON_SCHEMA_ID, "string")
102102
.optionalAttribute(JSON_TITLE, "string")
103-
.optionalAttribute(JSON_DESCRIPTION, "string");
104-
105-
registry.findType("object", "pojo")
106-
.optionalAttribute(JSON_TITLE, "string")
107-
.optionalAttribute(JSON_DESCRIPTION, "string");
103+
.optionalAttribute(JSON_DESCRIPTION, "string")
104+
// Codegen object-level flags (template conditionals). Formerly registered on
105+
// object.pojo; moved to object.base so the entity/value subtypes inherit them
106+
// after the pojo/proxy/map subtype registrations were retired (ADR-0005).
107+
.optionalAttribute("hasAuditing", "boolean")
108+
.optionalAttribute("hasJpa", "boolean")
109+
.optionalAttribute("hasValidation", "boolean");
108110

109111
// Field-level JSON Schema attributes
110112
registry.findType("field", "base")

server/java/codegen-base/src/main/java/com/metaobjects/generator/direct/metadata/html/MetaDataHtmlDocumentationWriter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ private void generateExtensionGuideSection() {
760760
writer.println(" <h3>Creating Custom Object Types</h3>");
761761
writer.println(" <div class=\"code-block\">");
762762
writer.println("@MetaDataType(type = \"object\", subType = \"auditable\", description = \"Object with audit trails\")");
763-
writer.println("public class AuditableObject extends PojoMetaObject {");
763+
writer.println("public class AuditableObject extends EntityMetaObject {");
764764
writer.println(" ");
765765
writer.println(" static {");
766766
writer.println(" MetaDataRegistry.registerType(AuditableObject.class, def -> def");

server/java/codegen-base/src/test/java/com/metaobjects/generator/SchemaReviewTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ public void setUp() {
2828
Class.forName("com.metaobjects.field.DateField");
2929
Class.forName("com.metaobjects.field.TimestampField");
3030
Class.forName("com.metaobjects.object.MetaObject");
31-
Class.forName("com.metaobjects.object.pojo.PojoMetaObject");
32-
Class.forName("com.metaobjects.object.proxy.ProxyMetaObject");
31+
Class.forName("com.metaobjects.object.EntityMetaObject");
32+
Class.forName("com.metaobjects.object.ValueMetaObject");
3333
Class.forName("com.metaobjects.attr.StringAttribute");
3434
Class.forName("com.metaobjects.attr.IntAttribute");
3535
Class.forName("com.metaobjects.attr.BooleanAttribute");

server/java/codegen-base/src/test/resources/schema-validation/invalid-missing-required.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<metadata package="test_missing_required">
33
<children>
44
<child>
5-
<object subType="pojo">
5+
<object subType="entity">
66
<children>
77
<child>
88
<field subType="string"/>

0 commit comments

Comments
 (0)