You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CLAUDE.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -423,6 +423,10 @@ These are the load-bearing principles that have emerged through implementation.
423
423
424
424
-**"Validated by spike" ≠ "right design".** A spike proves a technique works under a specific test. It doesn't prove it is the best production choice. Always ask "what's the UX cost?" alongside "does this work?"
425
425
426
+
-**Bind metadata→native types at build time, never runtime reflection.** Resolving an object's native class/module from its FQN must happen in generated code (static imports for data-oriented ports; a domain-sliced, FQN-keyed registry for OO ports), not via `Class.forName`/`Type.GetType`/`importlib` — runtime reflection is impossible in TS and breaks under GraalVM native-image / .NET AOT. See [ADR-0001](spec/decisions/ADR-0001-cross-language-type-binding.md).
427
+
428
+
-**Record significant cross-cutting decisions as ADRs.** Durable, cross-language/cross-feature architectural contracts live in `spec/decisions/` (Nygard format). Consult them before changing a cross-language contract; add a new ADR when you make one. Feature-level decisions stay in the FR spec; this file holds only the one-line rule + the pointer.
429
+
426
430
## Coding discipline (TS)
427
431
428
432
-**Named constants for metamodel strings — always.** Type names, subtype names, reserved JSON keys, special attribute names, structural separators, and wildcards live in `packages/metadata/src/constants.ts` — import and use them. Gets you compile-time typo safety.
`ObjectConnectionDB(Connection)` already accepts an externally-owned connection. Ship (likely in `metaobjects-core-spring`) a thin adapter that wraps `DataSourceUtils.getConnection(dataSource)` and does **not** close the connection (Spring owns the lifecycle), so OMDB operations join the caller's active `@Transactional` scope — including `REQUIRES_NEW` and pessimistic boundaries. No second, out-of-transaction connection.
52
52
53
-
### 3. jsonb fields modelled as value objects (opt-in)
54
-
A jsonb column may be either opaque JSON **or** declare a nested MetaObject as its value type:
53
+
### 3. jsonb fields modelled as typed value objects (opt-in)
54
+
A jsonb column may be either opaque JSON **or** declare a nested MetaObject as its value type. When modelled, the value is a **code-generated, mutable, typed POJO** (the default — real objects, IDE/type-safe), with `ValueObject` (the dynamic Map container) as the **fallback** for intentionally-open/dynamic shapes:
`PostgresDriver` gains a jsonb column type + a Jackson-backed value converter that reads the field's referenced MetaObject to (de)serialize `ValueObject`⇄ jsonb. Cardinality via `@isArray` (List) and optional `@keyedBy` (Map). This shares one modelling path with projections (below).
65
+
`PostgresDriver` gains a jsonb column type + a Jackson-backed converter that resolves the field's referenced MetaObject to its **bound Java class** (via the binding registry, §8 / ADR-0001) and (de)serializes the typed POJO ⇄ jsonb; cardinality via `@isArray` (`List<T>`) and optional `@keyedBy` (`Map<K,T>`); `ValueObject` when no class is bound. **Mutability matters:** these objects are read-modify-write (e.g. `realm.getMechanics().setX(…)`), not read-only DTOs — so generated jsonb value types are **mutable POJOs**, not immutable records. Because in-place mutation leaves the parent's field reference unchanged (so OMDB's dirty-field check can't detect it), **jsonb fields are always (re)serialized on `updateObject`** (deep-compare is a later optimization). This shares one modelling path with projections (below).
Extend `MetaClassDBValidatorService` + drivers from *create-if-missing* to a **diff-and-converge** engine: add tables/columns/indexes/FKs/sequences/views and additive ALTERs (widen type, add nullable/defaulted column). Surface it through a cross-language-aligned verb set, **not** as boot-time auto-apply:
@@ -78,11 +78,14 @@ A consuming app adopts its existing schema as the baseline; metadata must reprod
78
78
A projection is `object.value`/`object.entity` + `source.dbView`, with fields carrying `origin.passthrough` (cross-entity ref) or `origin.aggregate` (`count`/`sum`/`avg`/`min`/`max` via dotted `@via`/`@of`). OMDB already creates views from `ViewDef` and verifies read mappings; add the **origin→`CREATE VIEW` SQL deriver** so view SQL is generated when expressible, falling back to explicit `dbViewSQL` only when it is not. Projection results materialize as `ValueObject`s (the same container used for opaque/value-object reads), so consumers can blend projected DB data with app-side values.
79
79
80
80
### 6. Codegen templates
81
-
Mustache templates/generators (build-time, via the maven-plugin) for: entity base classes (POJO mapped via `PojoMetaObject`, no business logic), per-entity SQL-name constants (`Tables`/`Columns` — keep residual native SQL rename-safe + drift-checkable), projection value-object classes, and an OMDB-backed repository base (CRUD via `Expression`/`QueryOptions`; advanced queries via `dbViewSQL`/`executeQuery`). Generated artifacts carry no business logic; consumers extend them.
81
+
Mustache templates/generators (build-time, via the maven-plugin) for: entity base classes (POJO mapped via `PojoMetaObject`, no business logic), per-entity SQL-name constants (`Tables`/`Columns` — keep residual native SQL rename-safe + drift-checkable), **typed mutable jsonb value-object POJOs** (§3), projection value-object classes, an OMDB-backed repository base (CRUD via `Expression`/`QueryOptions`; advanced queries via `dbViewSQL`/`executeQuery`), and **one `MetaDataTypeProvider` per generated package that registers its `FQN → class` bindings** (§8 / ADR-0001), `ServiceLoader`-discovered. Generated artifacts carry no business logic; consumers extend them.
82
82
83
83
### 7. Conformance fixtures
84
84
Extend the existing `source-*`/`origin-*` corpus to cover the new vocabulary surfaced here (`@generation`, `@previousName`, jsonb value-object fields, projection origin→view derivation at the **metamodel** level). A full migration-output conformance corpus (byte-identical SQL across languages) is flagged **future** (consistent with the C# design); v1 asserts vocabulary + loader/serializer parity and diff *determinism* per language.
OMDB instantiates **typed objects** — entities, jsonb value-objects (§3), and projection results — by resolving a MetaObject's FQN to its concrete Java class. Per **[ADR-0001](../../../spec/decisions/ADR-0001-cross-language-type-binding.md)**, this is a **build-time, domain-sliced, FQN-keyed registry — never runtime reflection** (`Class.forName` is AOT/native-image-hostile; the cross-language contract forbids it). Java realization: codegen emits **one `MetaDataTypeProvider` per generated package** that registers its `FQN → Class` bindings; `ServiceLoader` discovers and merges them across packages/jars (the same mechanism core already uses for its 8 type providers). OMDB consults the merged registry to pick the class for `newInstance()`/jsonb deserialization, falling back to `ValueObject` when no class is bound. This FR builds the **runtime registry contract** (the registry API + OMDB consulting it, tested with hand-registered bindings); the codegen that *emits* the per-package providers ships with the codegen templates (§6). The deferred metadata overlay (`@object` FQN map) is replaced by this generated, compile-checked registration — and never reappears as a hand-maintained file.
88
+
86
89
## Versioning & compatibility
87
90
88
91
`6.x → 7.0.0`, by SemVer (and OSGi semantic versioning, since the project ships bundles): a major bump is mandated by **breaking public-API changes**, regardless of how much additive work rides along.
**Applies to:** all language ports (TS, Java, Python, C#)
5
+
**Related:** the OMDB persistence work (`docs/superpowers/specs/2026-05-22-fr-003-*`); `spec/metamodel.md`; `spec/conformance-tests.md`
6
+
7
+
## Context
8
+
9
+
Metadata is **language-agnostic**: an object is identified by a fully-qualified name (FQN) like `myapp::commerce::Program`. At runtime and/or codegen time, each language must bind that FQN to its native representation — the generated class to instantiate (OO ports), or the generated module/schema to use (data-oriented ports). The question is *how*, in a way that is consistent across languages, survives modern toolchains, and doesn't pollute the metadata.
10
+
11
+
Three naive approaches fail:
12
+
13
+
-**Runtime reflection by name** (`Class.forName` / `Type.GetType` / `importlib`): impossible in TS/JS (no runtime "class from string" reflection), and increasingly broken in Java/C# by **GraalVM native image** and **.NET Native AOT / trimming**, which remove reflection at build time. The industry is actively moving *away* from this (Spring AOT generates reflection-free hints; `System.Text.Json` requires source generators and lets you disable reflection entirely).
14
+
-**A single global registry**: doesn't scale — codegen runs **per package/domain/target**, often into separate artifacts/jars; one monolithic registry can't be assembled from independently-generated slices.
15
+
-**A hand-maintained (or generated-JSON) metadata overlay** mapping FQN→class: drifts from the code, pollutes the language-agnostic metadata with per-language class paths, and is only validated at runtime.
16
+
17
+
Industry precedent (cross-language schema tools): **Protobuf** uses a `TypeRegistry` keyed by FQN/type-URL, populated by **generated code in each `.proto`'s output that self-registers** (`proto.RegisterType`, etc.). **gRPC** emits **per-service** registration that composes at startup. **Jackson** prefers explicit `@JsonSubTypes` over reflection scanning. **class-transformer** (TS) requires explicit discriminator maps because TS has no reflection. **SQLAlchemy** centers a `registry` construct that declarative and imperative mapping both populate.
18
+
19
+
## Decision
20
+
21
+
**Resolve the binding at build/codegen time, in generated code — never via runtime reflection.** The binding is keyed by the canonical metadata FQN and is **domain-sliced and composable** (each codegen unit contributes its own bindings; the runtime aggregates them). The realization is idiomatic per language paradigm:
|**Data-oriented** (TS) | generated **static imports** (convention: namespace→file path) + lookup by name in the loaded metadata tree; **no registry object** (codegen emits plain types + schemas, not OO classes) | barrel re-export; the metadata tree *is* the runtime registry |
26
+
|**OO** (Java / C# / Python) | generated, **domain-sliced registration** into an **FQN-keyed runtime registry** (typed objects are instantiated, so build-time static imports alone can't provide runtime polymorphic instantiation) |**Java:** one `MetaDataTypeProvider`**per generated package**, auto-discovered + merged via `ServiceLoader`. **C#:** source-generated registration per assembly via a module initializer (AOT-safe). **Python:** per-package registration at import / entry-points. |
27
+
28
+
The **durable contract** (conformance-describable) is: *the binding key is the metadata FQN, it is established in generated code at build time, and it is composable across independently-generated slices.* The **realization is idiomatic** per language (matching the project's "durable contract identical; runtime surface idiomatic" rule).
29
+
30
+
The language-agnostic metadata holds **no** native class paths. Any one-off divergence from convention is expressed as an inline `@object` attribute on a single MetaObject — never a whole overlay file.
31
+
32
+
## Consequences
33
+
34
+
**Positive**
35
+
- Compile/type-checked in TS/Java/C# (rename a class → the generated registration won't compile); import-checked in Python. Drift caught at build, not runtime.
36
+
-**AOT / native-image safe** (no runtime reflection-by-name).
37
+
-**Domain-sliced & composable** — multi-package/multi-jar codegen each contributes a registration unit; the runtime composes them (Java `ServiceLoader` is purpose-built for exactly this; it already loads core's type providers).
38
+
- Metadata stays language-agnostic; no driftable overlay.
39
+
- Matches what the TS port already ships (this ADR validates and generalizes existing behavior, it does not re-architect it).
40
+
41
+
**Negative / costs**
42
+
- OO ports emit one small registration unit per codegen slice (a generated artifact). This is folded into the per-target codegen output already produced, and is the same model protobuf/gRPC use, so the cost is minimal and idiomatic.
43
+
- The runtime needs a registry abstraction + a discovery hook per language (a one-time investment per port).
44
+
45
+
## Alternatives considered (rejected)
46
+
1.**Runtime reflection / convention-by-name** — impossible in TS; AOT/native-image-hostile in Java/C#. Rejected as a *universal* mechanism (it remains available only as a per-language optimization where reflection is acceptable).
47
+
2.**Single global registry** — can't be assembled from independently-generated package slices.
-**Java** — in the OMDB persistence work: per-package `MetaDataTypeProvider` registry via `ServiceLoader`, consulted by ObjectManagerDB to instantiate entities and typed jsonb value-objects.
53
+
-**C#** — future runtime work: source-generated registration per assembly.
54
+
-**Python** — post-H3.
55
+
56
+
## Conformance note
57
+
Runtime instantiation APIs are out of byte-identical conformance scope (per `spec/conformance-tests.md` — runtime surface is idiomatic). What is conformance-relevant is the **key**: the canonical FQN form (`pkg::Name`) used as the registry key, which the loader/serializer corpus already pins.
This directory records **significant, cross-cutting architectural decisions** for the MetaObjects standard — the durable "why" that outlives any single feature spec.
4
+
5
+
## What goes here vs. elsewhere
6
+
7
+
| Doc type | Location | Scope |
8
+
|---|---|---|
9
+
|**ADR**|`spec/decisions/`*(here)*| Durable, **cross-cutting, target-agnostic** contracts that bind multiple language ports (e.g. how metadata→native types are bound; the conformance model; persistence-engine choice). |
10
+
| Feature design (FR spec) |`docs/superpowers/specs/`| A single feature's design, language- or area-scoped. |
11
+
| Implementation plan |`docs/superpowers/plans/`| Task-by-task execution of a feature. |
Write one when a decision is **cross-cutting** (affects more than one language port or more than one feature) and **durable** (future work must respect it). Examples: a metamodel binding contract, the wire-format guarantee, "migrations decoupled from the runtime." Do **not** write ADRs for feature-level or easily-reversible choices — those live in the FR spec.
16
+
17
+
## Format
18
+
[Michael Nygard's ADR format](https://github.com/joelparkerhenderson/architecture-decision-record): **Context → Decision → Consequences → Status**, plus *Alternatives considered* and *Realization status* where useful. One decision per file. Status is `Proposed` | `Accepted` | `Superseded by ADR-NNNN`. Numbered sequentially; never renumber (supersede instead).
0 commit comments