From d6e250f36464539ba8976d38a9ea822af4e220e3 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Thu, 6 Aug 2026 08:20:29 -0400 Subject: [PATCH] =?UTF-8?q?docs(adr):=20amend=20ADR-0028=20=E2=80=94=20a?= =?UTF-8?q?=20concrete=20projection=20owns=20its=20source?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the doctrine behind ERR_PROJECTION_INHERITED_SOURCE (bdeb4765): a concrete object.projection declares its own source.*; an abstract projection base carries shape only. The rationale is worth writing down because the rule looks arbitrary until you see which of two inheritance regimes a projection belongs to. Shared-storage inheritance inherits binding AND writability together — Hibernate's @Immutable 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 table of its own, and Django documents inheriting db_table from an abstract base as a trap, in almost these words. A projection is the second kind, so the concrete class must bind its own storage. Also records why the fix guards the shape instead of flipping a predicate: "which source am I bound to" is resolving because entities legitimately inherit their table, "what KIND of source am I" is own-only because projection-ness belongs to the declaring object, and both are right for what they were designed for. Only their intersection was incoherent, and it produced no working artifact in any of the five ports. The authoring skill gains the rule with its sanctioned pattern (abstract sourceless base, concrete projections declaring their own views) so an agent authoring a versioned projection declares v_customers_v2 rather than silently sharing V1's view. Bundled into the sdk copy and the four agent-context-conformance goldens. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01S3msoGxjRMwx94PhKSLDuE --- .../skills/metaobjects-authoring/SKILL.md | 12 +++++ .../skills/metaobjects-authoring/SKILL.md | 12 +++++ .../skills/metaobjects-authoring/SKILL.md | 12 +++++ .../skills/metaobjects-authoring/SKILL.md | 12 +++++ .../skills/metaobjects-authoring/SKILL.md | 12 +++++ ...object-taxonomy-projection-value-purity.md | 52 +++++++++++++++++++ 6 files changed, 112 insertions(+) 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