Skip to content

Commit 780fb30

Browse files
authored
Merge pull request #272 from metaobjectsdev/fix/projection-source-doctrine-and-270
docs(adr): amend ADR-0028 — a concrete projection owns its source
2 parents bdeb476 + d6e250f commit 780fb30

6 files changed

Lines changed: 112 additions & 0 deletions

File tree

‎agent-context/skills/metaobjects-authoring/SKILL.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,18 @@ declared field set IS the exposure (fail-closed). Give it a read-only `source.rd
731731
projection detection + view DDL off that read-only source, so without it `meta gen`
732732
emits nothing for the projection.
733733

734+
**A CONCRETE projection declares its OWN source — never inherits one.** A
735+
projection may `extends` another projection to reuse shape, but the child must
736+
declare its own `source.rdb`; inheriting the parent's is
737+
`ERR_PROJECTION_INHERITED_SOURCE`. `extends` only ADDS fields, so an inherited view
738+
cannot provide the child's extra columns, and two objects would claim one physical
739+
view with different exposures. The sanctioned pattern is an **abstract, sourceless**
740+
projection base carrying shared shape, with each concrete projection declaring its
741+
own view — so a versioned successor (`CustomersV2 extends CustomersV1`) declares
742+
`v_customers_v2` rather than silently sharing V1's view. (Same rule JPA gets from
743+
`@MappedSuperclass` and Django documents as the `db_table`-on-abstract trap; ADR-0028
744+
amendment 2026-08-06.)
745+
734746
**Origin vocabulary (#195).** `origin.aggregate @agg` takes `count`/`sum`/`avg`/`min`/`max`
735747
(numeric reduces over `@of`), `any`/`all` (predicate quantifiers over a `@filter`; `@of`
736748
forbidden; empty set → `any=false`, `all=true`), and `collect` (an array rollup of `@of`

‎fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,18 @@ declared field set IS the exposure (fail-closed). Give it a read-only `source.rd
731731
projection detection + view DDL off that read-only source, so without it `meta gen`
732732
emits nothing for the projection.
733733

734+
**A CONCRETE projection declares its OWN source — never inherits one.** A
735+
projection may `extends` another projection to reuse shape, but the child must
736+
declare its own `source.rdb`; inheriting the parent's is
737+
`ERR_PROJECTION_INHERITED_SOURCE`. `extends` only ADDS fields, so an inherited view
738+
cannot provide the child's extra columns, and two objects would claim one physical
739+
view with different exposures. The sanctioned pattern is an **abstract, sourceless**
740+
projection base carrying shared shape, with each concrete projection declaring its
741+
own view — so a versioned successor (`CustomersV2 extends CustomersV1`) declares
742+
`v_customers_v2` rather than silently sharing V1's view. (Same rule JPA gets from
743+
`@MappedSuperclass` and Django documents as the `db_table`-on-abstract trap; ADR-0028
744+
amendment 2026-08-06.)
745+
734746
**Origin vocabulary (#195).** `origin.aggregate @agg` takes `count`/`sum`/`avg`/`min`/`max`
735747
(numeric reduces over `@of`), `any`/`all` (predicate quantifiers over a `@filter`; `@of`
736748
forbidden; empty set → `any=false`, `all=true`), and `collect` (an array rollup of `@of`

‎fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,18 @@ declared field set IS the exposure (fail-closed). Give it a read-only `source.rd
731731
projection detection + view DDL off that read-only source, so without it `meta gen`
732732
emits nothing for the projection.
733733

734+
**A CONCRETE projection declares its OWN source — never inherits one.** A
735+
projection may `extends` another projection to reuse shape, but the child must
736+
declare its own `source.rdb`; inheriting the parent's is
737+
`ERR_PROJECTION_INHERITED_SOURCE`. `extends` only ADDS fields, so an inherited view
738+
cannot provide the child's extra columns, and two objects would claim one physical
739+
view with different exposures. The sanctioned pattern is an **abstract, sourceless**
740+
projection base carrying shared shape, with each concrete projection declaring its
741+
own view — so a versioned successor (`CustomersV2 extends CustomersV1`) declares
742+
`v_customers_v2` rather than silently sharing V1's view. (Same rule JPA gets from
743+
`@MappedSuperclass` and Django documents as the `db_table`-on-abstract trap; ADR-0028
744+
amendment 2026-08-06.)
745+
734746
**Origin vocabulary (#195).** `origin.aggregate @agg` takes `count`/`sum`/`avg`/`min`/`max`
735747
(numeric reduces over `@of`), `any`/`all` (predicate quantifiers over a `@filter`; `@of`
736748
forbidden; empty set → `any=false`, `all=true`), and `collect` (an array rollup of `@of`

‎fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,18 @@ declared field set IS the exposure (fail-closed). Give it a read-only `source.rd
731731
projection detection + view DDL off that read-only source, so without it `meta gen`
732732
emits nothing for the projection.
733733

734+
**A CONCRETE projection declares its OWN source — never inherits one.** A
735+
projection may `extends` another projection to reuse shape, but the child must
736+
declare its own `source.rdb`; inheriting the parent's is
737+
`ERR_PROJECTION_INHERITED_SOURCE`. `extends` only ADDS fields, so an inherited view
738+
cannot provide the child's extra columns, and two objects would claim one physical
739+
view with different exposures. The sanctioned pattern is an **abstract, sourceless**
740+
projection base carrying shared shape, with each concrete projection declaring its
741+
own view — so a versioned successor (`CustomersV2 extends CustomersV1`) declares
742+
`v_customers_v2` rather than silently sharing V1's view. (Same rule JPA gets from
743+
`@MappedSuperclass` and Django documents as the `db_table`-on-abstract trap; ADR-0028
744+
amendment 2026-08-06.)
745+
734746
**Origin vocabulary (#195).** `origin.aggregate @agg` takes `count`/`sum`/`avg`/`min`/`max`
735747
(numeric reduces over `@of`), `any`/`all` (predicate quantifiers over a `@filter`; `@of`
736748
forbidden; empty set → `any=false`, `all=true`), and `collect` (an array rollup of `@of`

‎fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,18 @@ declared field set IS the exposure (fail-closed). Give it a read-only `source.rd
731731
projection detection + view DDL off that read-only source, so without it `meta gen`
732732
emits nothing for the projection.
733733

734+
**A CONCRETE projection declares its OWN source — never inherits one.** A
735+
projection may `extends` another projection to reuse shape, but the child must
736+
declare its own `source.rdb`; inheriting the parent's is
737+
`ERR_PROJECTION_INHERITED_SOURCE`. `extends` only ADDS fields, so an inherited view
738+
cannot provide the child's extra columns, and two objects would claim one physical
739+
view with different exposures. The sanctioned pattern is an **abstract, sourceless**
740+
projection base carrying shared shape, with each concrete projection declaring its
741+
own view — so a versioned successor (`CustomersV2 extends CustomersV1`) declares
742+
`v_customers_v2` rather than silently sharing V1's view. (Same rule JPA gets from
743+
`@MappedSuperclass` and Django documents as the `db_table`-on-abstract trap; ADR-0028
744+
amendment 2026-08-06.)
745+
734746
**Origin vocabulary (#195).** `origin.aggregate @agg` takes `count`/`sum`/`avg`/`min`/`max`
735747
(numeric reduces over `@of`), `any`/`all` (predicate quantifiers over a `@filter`; `@of`
736748
forbidden; empty set → `any=false`, `all=true`), and `collect` (an array rollup of `@of`

‎spec/decisions/ADR-0028-object-taxonomy-projection-value-purity.md‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
Accepted (2026-06-12). Defined by FR-024
66
(`docs/superpowers/specs/2026-06-12-fr-024-entity-surfaces-projections-design.md`).
7+
**Amended 2026-08-06** — see *Amendment: a concrete projection owns its source*
8+
below (`ERR_PROJECTION_INHERITED_SOURCE`).
79

810
## Context
911

@@ -49,6 +51,56 @@ semantic lie.
4951
Lineage, exposure, read-only-ness, and keys all remain computed; only the
5052
licensing is declared.
5153

54+
## Amendment (2026-08-06) — a concrete projection owns its source
55+
56+
A **concrete** `object.projection` must declare its own `source.*`; inheriting one
57+
through `extends` is `ERR_PROJECTION_INHERITED_SOURCE`. An **abstract** projection
58+
base carries shape only — a source on one is inert until a concrete child extends
59+
it, at which point the error fires on the child.
60+
61+
**Why.** A projection's `extends` is *shape lineage*, not a shared-storage
62+
hierarchy. `extends` only ADDS members, so a child inheriting the parent's view
63+
gains fields that view cannot provide, and both objects then claim one physical
64+
view while declaring different exposures — which contradicts decision 6's
65+
fail-closed rule that the declared field set IS the exposure, and gives one view
66+
two DDL owners.
67+
68+
The shape was also the one place two source predicates disagreed. "Which source am
69+
I bound to" resolves through the super chain — an entity legitimately inherits its
70+
table (TPH / a `BaseEntity`), and making that lookup resolving fixed a real
71+
"inherited source emitted nothing" bug. "What KIND of source am I" is own-only,
72+
because projection-ness is a property of the declaring object (ADR-0039 sanctioned
73+
own; see `codegen-ts` `projection-detector.ts`). Both readings are correct for what
74+
they were designed for; only their intersection was incoherent, and it produced no
75+
working artifact in any port — TypeScript mounted writable CRUD over a read-only
76+
view, Java and Kotlin skipped on their subtype gate, Python on the resolved kind,
77+
and C# emitted nothing. Guarding the shape makes the predicates agree without
78+
flipping either.
79+
80+
**Prior art.** The split matches how mature ORMs divide the two inheritance
81+
regimes. Shared-storage inheritance inherits binding AND writability together —
82+
Hibernate's `@Immutable` "may be applied only to the root entity, and is inherited
83+
by entity subclasses"; EF Core keyless `ToView` types; SQLAlchemy single-table.
84+
Shape-reuse inheritance does not inherit the binding at all — JPA
85+
`@MappedSuperclass` "has no separate table defined for it", and Django documents
86+
inheriting `db_table` from an abstract base as a trap: "all the child classes …
87+
would use the same database table, which is almost certainly not what you want".
88+
A projection is the second kind. Systems that expose views also derive writability
89+
structurally per object rather than splitting it from the binding (jOOQ's
90+
`TableRecord` vs `UpdatableRecord`; Prisma disables mutations on views outright).
91+
92+
**The sanctioned pattern** (already in the corpus as
93+
`fixtures/conformance/projection-extends-projection`): an abstract, sourceless
94+
projection base carries the shared field shape; each concrete projection declares
95+
its own read-only source. A versioned successor therefore declares its own view
96+
rather than silently sharing its predecessor's.
97+
98+
Enforced at the concrete level, mirroring #236's abstract-exemption precedent. The
99+
check is skipped when the super is not a legal projection, so a projection
100+
extending an entity still reports one error at its root cause rather than two.
101+
Gated by `fixtures/conformance/error-projection-inherited-source` across all five
102+
ports.
103+
52104
## Consequences
53105

54106
- The two legacy spellings are REMOVED outright — hard cutover, no deprecation

0 commit comments

Comments
 (0)