diff --git a/agent-context/skills/metaobjects-authoring/SKILL.md b/agent-context/skills/metaobjects-authoring/SKILL.md index cd825b2f4..90322f46a 100644 --- a/agent-context/skills/metaobjects-authoring/SKILL.md +++ b/agent-context/skills/metaobjects-authoring/SKILL.md @@ -731,6 +731,18 @@ declared field set IS the exposure (fail-closed). Give it a read-only `source.rd projection detection + view DDL off that read-only source, so without it `meta gen` emits nothing for the projection. +**A CONCRETE projection declares its OWN source — never inherits one.** A +projection may `extends` another projection to reuse shape, but the child must +declare its own `source.rdb`; inheriting the parent's is +`ERR_PROJECTION_INHERITED_SOURCE`. `extends` only ADDS fields, so an inherited view +cannot provide the child's extra columns, and two objects would claim one physical +view with different exposures. The sanctioned pattern is an **abstract, sourceless** +projection base carrying shared shape, with each concrete projection declaring its +own view — so a versioned successor (`CustomersV2 extends CustomersV1`) declares +`v_customers_v2` rather than silently sharing V1's view. (Same rule JPA gets from +`@MappedSuperclass` and Django documents as the `db_table`-on-abstract trap; ADR-0028 +amendment 2026-08-06.) + **Origin vocabulary (#195).** `origin.aggregate @agg` takes `count`/`sum`/`avg`/`min`/`max` (numeric reduces over `@of`), `any`/`all` (predicate quantifiers over a `@filter`; `@of` forbidden; empty set → `any=false`, `all=true`), and `collect` (an array rollup of `@of` diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md index cd825b2f4..90322f46a 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -731,6 +731,18 @@ declared field set IS the exposure (fail-closed). Give it a read-only `source.rd projection detection + view DDL off that read-only source, so without it `meta gen` emits nothing for the projection. +**A CONCRETE projection declares its OWN source — never inherits one.** A +projection may `extends` another projection to reuse shape, but the child must +declare its own `source.rdb`; inheriting the parent's is +`ERR_PROJECTION_INHERITED_SOURCE`. `extends` only ADDS fields, so an inherited view +cannot provide the child's extra columns, and two objects would claim one physical +view with different exposures. The sanctioned pattern is an **abstract, sourceless** +projection base carrying shared shape, with each concrete projection declaring its +own view — so a versioned successor (`CustomersV2 extends CustomersV1`) declares +`v_customers_v2` rather than silently sharing V1's view. (Same rule JPA gets from +`@MappedSuperclass` and Django documents as the `db_table`-on-abstract trap; ADR-0028 +amendment 2026-08-06.) + **Origin vocabulary (#195).** `origin.aggregate @agg` takes `count`/`sum`/`avg`/`min`/`max` (numeric reduces over `@of`), `any`/`all` (predicate quantifiers over a `@filter`; `@of` forbidden; empty set → `any=false`, `all=true`), and `collect` (an array rollup of `@of` diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md index cd825b2f4..90322f46a 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -731,6 +731,18 @@ declared field set IS the exposure (fail-closed). Give it a read-only `source.rd projection detection + view DDL off that read-only source, so without it `meta gen` emits nothing for the projection. +**A CONCRETE projection declares its OWN source — never inherits one.** A +projection may `extends` another projection to reuse shape, but the child must +declare its own `source.rdb`; inheriting the parent's is +`ERR_PROJECTION_INHERITED_SOURCE`. `extends` only ADDS fields, so an inherited view +cannot provide the child's extra columns, and two objects would claim one physical +view with different exposures. The sanctioned pattern is an **abstract, sourceless** +projection base carrying shared shape, with each concrete projection declaring its +own view — so a versioned successor (`CustomersV2 extends CustomersV1`) declares +`v_customers_v2` rather than silently sharing V1's view. (Same rule JPA gets from +`@MappedSuperclass` and Django documents as the `db_table`-on-abstract trap; ADR-0028 +amendment 2026-08-06.) + **Origin vocabulary (#195).** `origin.aggregate @agg` takes `count`/`sum`/`avg`/`min`/`max` (numeric reduces over `@of`), `any`/`all` (predicate quantifiers over a `@filter`; `@of` forbidden; empty set → `any=false`, `all=true`), and `collect` (an array rollup of `@of` diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md index cd825b2f4..90322f46a 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -731,6 +731,18 @@ declared field set IS the exposure (fail-closed). Give it a read-only `source.rd projection detection + view DDL off that read-only source, so without it `meta gen` emits nothing for the projection. +**A CONCRETE projection declares its OWN source — never inherits one.** A +projection may `extends` another projection to reuse shape, but the child must +declare its own `source.rdb`; inheriting the parent's is +`ERR_PROJECTION_INHERITED_SOURCE`. `extends` only ADDS fields, so an inherited view +cannot provide the child's extra columns, and two objects would claim one physical +view with different exposures. The sanctioned pattern is an **abstract, sourceless** +projection base carrying shared shape, with each concrete projection declaring its +own view — so a versioned successor (`CustomersV2 extends CustomersV1`) declares +`v_customers_v2` rather than silently sharing V1's view. (Same rule JPA gets from +`@MappedSuperclass` and Django documents as the `db_table`-on-abstract trap; ADR-0028 +amendment 2026-08-06.) + **Origin vocabulary (#195).** `origin.aggregate @agg` takes `count`/`sum`/`avg`/`min`/`max` (numeric reduces over `@of`), `any`/`all` (predicate quantifiers over a `@filter`; `@of` forbidden; empty set → `any=false`, `all=true`), and `collect` (an array rollup of `@of` diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md index cd825b2f4..90322f46a 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -731,6 +731,18 @@ declared field set IS the exposure (fail-closed). Give it a read-only `source.rd projection detection + view DDL off that read-only source, so without it `meta gen` emits nothing for the projection. +**A CONCRETE projection declares its OWN source — never inherits one.** A +projection may `extends` another projection to reuse shape, but the child must +declare its own `source.rdb`; inheriting the parent's is +`ERR_PROJECTION_INHERITED_SOURCE`. `extends` only ADDS fields, so an inherited view +cannot provide the child's extra columns, and two objects would claim one physical +view with different exposures. The sanctioned pattern is an **abstract, sourceless** +projection base carrying shared shape, with each concrete projection declaring its +own view — so a versioned successor (`CustomersV2 extends CustomersV1`) declares +`v_customers_v2` rather than silently sharing V1's view. (Same rule JPA gets from +`@MappedSuperclass` and Django documents as the `db_table`-on-abstract trap; ADR-0028 +amendment 2026-08-06.) + **Origin vocabulary (#195).** `origin.aggregate @agg` takes `count`/`sum`/`avg`/`min`/`max` (numeric reduces over `@of`), `any`/`all` (predicate quantifiers over a `@filter`; `@of` forbidden; empty set → `any=false`, `all=true`), and `collect` (an array rollup of `@of` diff --git a/spec/decisions/ADR-0028-object-taxonomy-projection-value-purity.md b/spec/decisions/ADR-0028-object-taxonomy-projection-value-purity.md index 1538deb05..4c417b4a7 100644 --- a/spec/decisions/ADR-0028-object-taxonomy-projection-value-purity.md +++ b/spec/decisions/ADR-0028-object-taxonomy-projection-value-purity.md @@ -4,6 +4,8 @@ Accepted (2026-06-12). Defined by FR-024 (`docs/superpowers/specs/2026-06-12-fr-024-entity-surfaces-projections-design.md`). +**Amended 2026-08-06** — see *Amendment: a concrete projection owns its source* +below (`ERR_PROJECTION_INHERITED_SOURCE`). ## Context @@ -49,6 +51,56 @@ semantic lie. Lineage, exposure, read-only-ness, and keys all remain computed; only the licensing is declared. +## Amendment (2026-08-06) — a concrete projection owns its source + +A **concrete** `object.projection` must declare its own `source.*`; inheriting one +through `extends` is `ERR_PROJECTION_INHERITED_SOURCE`. An **abstract** projection +base carries shape only — a source on one is inert until a concrete child extends +it, at which point the error fires on the child. + +**Why.** A projection's `extends` is *shape lineage*, not a shared-storage +hierarchy. `extends` only ADDS members, so a child inheriting the parent's view +gains fields that view cannot provide, and both objects then claim one physical +view while declaring different exposures — which contradicts decision 6's +fail-closed rule that the declared field set IS the exposure, and gives one view +two DDL owners. + +The shape was also the one place two source predicates disagreed. "Which source am +I bound to" resolves through the super chain — an entity legitimately inherits its +table (TPH / a `BaseEntity`), and making that lookup resolving fixed a real +"inherited source emitted nothing" bug. "What KIND of source am I" is own-only, +because projection-ness is a property of the declaring object (ADR-0039 sanctioned +own; see `codegen-ts` `projection-detector.ts`). Both readings are correct for what +they were designed for; only their intersection was incoherent, and it produced no +working artifact in any port — TypeScript mounted writable CRUD over a read-only +view, Java and Kotlin skipped on their subtype gate, Python on the resolved kind, +and C# emitted nothing. Guarding the shape makes the predicates agree without +flipping either. + +**Prior art.** The split matches how mature ORMs divide the two inheritance +regimes. Shared-storage inheritance inherits binding AND writability together — +Hibernate's `@Immutable` "may be applied only to the root entity, and is inherited +by entity subclasses"; EF Core keyless `ToView` types; SQLAlchemy single-table. +Shape-reuse inheritance does not inherit the binding at all — JPA +`@MappedSuperclass` "has no separate table defined for it", and Django documents +inheriting `db_table` from an abstract base as a trap: "all the child classes … +would use the same database table, which is almost certainly not what you want". +A projection is the second kind. Systems that expose views also derive writability +structurally per object rather than splitting it from the binding (jOOQ's +`TableRecord` vs `UpdatableRecord`; Prisma disables mutations on views outright). + +**The sanctioned pattern** (already in the corpus as +`fixtures/conformance/projection-extends-projection`): an abstract, sourceless +projection base carries the shared field shape; each concrete projection declares +its own read-only source. A versioned successor therefore declares its own view +rather than silently sharing its predecessor's. + +Enforced at the concrete level, mirroring #236's abstract-exemption precedent. The +check is skipped when the super is not a legal projection, so a projection +extending an entity still reports one error at its root cause rather than two. +Gated by `fixtures/conformance/error-projection-inherited-source` across all five +ports. + ## Consequences - The two legacy spellings are REMOVED outright — hard cutover, no deprecation