From 20558d6e6efbed774198cf9eb9b0296df430eb16 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Mon, 14 Sep 2026 10:43:05 -0600 Subject: [PATCH 1/5] docs(spec): GraphQL access to inherited-subtype properties on Image and File fields (#34540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Image and File fields are typed in the GraphQL schema as DotFileasset, a flat six-property object built once in a static block, instead of the real type of the asset they point at. Customer fields on DOTASSET/FILEASSET-extended content types, tags, and even the asset's own identifier are therefore unreachable, and a client cannot tell an image-style asset from a file-style one. Two decisions are recorded in the spec: - Breaking existing queries is accepted (FR-012). Five of the six properties the current view exposes are synthesized for image-style assets and have no equivalent on the real type, so preserving them would mean preserving the misdescription permanently. FR-009a additionally requires every break to be visible: `description` survives as a valid name but changes meaning, so it must be renamed rather than repurposed. - PR #35363 is superseded rather than merged (FR-013). Its benefit — sparing the client one level of nesting to reach an asset's binary properties — does not survive this feature, because the real asset type already exposes a fully described binary carrying those properties. Issue #34540 stays open afterwards, carrying the second stage, which must be designed on the shape this feature establishes. Co-Authored-By: Claude Opus 5 (1M context) --- .../spec.md | 324 ++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 specs/34540-graphql-asset-subtype-fields/spec.md diff --git a/specs/34540-graphql-asset-subtype-fields/spec.md b/specs/34540-graphql-asset-subtype-fields/spec.md new file mode 100644 index 000000000000..0f317f071168 --- /dev/null +++ b/specs/34540-graphql-asset-subtype-fields/spec.md @@ -0,0 +1,324 @@ +# Feature Specification: GraphQL Access to Inherited-Subtype Properties on Image and File Fields + +**Feature Branch**: `34540-graphql-asset-subtype-fields` + +**Created**: 2026-09-14 + +**Status**: Draft + +**Type**: New Feature + +**Issue**: dotCMS/core#34540 — "GraphQL Support for dotAssets and FileAssets in Image/File Fields" + +**Input**: User description: "A client querying an Image or File field over GraphQL must be able to select the properties of the content type the field actually points at — including fields the customer defined on their own content types that extend the DOTASSET or FILEASSET base types — and must be able to tell which type it received." + +## Overview + +dotCMS lets a customer model their own kinds of assets by extending the two asset base types: a +banner image that also carries a campaign name and an ad size, a PDF that also carries a category +and a download count. The authoring side of that works today — the customer defines the fields, +editors fill them in, the values are stored and versioned. + +The reading side does not. When another piece of content points at one of those assets through an +Image or a File field, a GraphQL client can only ever see a fixed, six-property view of it. The +customer's own fields are not offered by the schema at all, so they cannot be requested. Neither +can the asset's own identity — a client cannot ask an Image field for the asset's identifier or +its site. And because every asset comes back described the same way, a client cannot tell whether +it received an image-style asset or a file-style one. + +For customers who have adopted GraphQL as their data-consumption API — and GraphQL is a +first-class delivery API in dotCMS, not a side channel — this makes a whole category of their own +content unreadable through the API they standardized on. There is no workaround for the custom +fields. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Read the customer's own asset properties (Priority: P1) + +A customer has defined their own image-style content type carrying a campaign name and an ad size, +and their own file-style content type carrying a category and a download count. Content elsewhere +in the site points at those assets through an Image field and a File field. The customer's +front-end asks for a page of that content and expects to receive the campaign name alongside the +image, and the category alongside the document, in the same request. + +**Why this priority**: This is the capability that does not exist in any form today and has no +workaround. It is what blocks the AI tagging workflow: the tags an asset carries cannot be read +back. Everything else in this spec is an improvement to something partially possible; this is the +difference between the customer's data being reachable and unreachable. + +**Independent Test**: Define an asset type with a custom property, create an asset of it, point an +Image field at that asset, and request the custom property through GraphQL. Delivering only this +story already unblocks the tagging workflow and custom asset metadata. + +**Acceptance Scenarios**: + +1. **Given** a customer-defined image-style asset type with a campaign-name property, and content + whose Image field points at an asset of that type, **When** a client requests the campaign name + through that Image field, **Then** the stored campaign name is returned. +2. **Given** a customer-defined file-style asset type with a category property, and content whose + File field points at a document of that type, **When** a client requests the category through + that File field, **Then** the stored category is returned. +3. **Given** an asset that carries tags, **When** a client requests the tags through an Image + field pointing at it, **Then** the tags are returned. +4. **Given** a customer adds a new property to an existing asset type, **When** a client requests + that property through an Image or File field without any administrative action in between, + **Then** the new property is available and returns its stored value. + +--- + +### User Story 2 - Know which kind of asset came back (Priority: P2) + +A customer's front-end renders a mixed feed in which the same field may point at an image-style +asset or a file-style one, and each needs different treatment. The client needs the response to +say which kind of asset it received so it can branch on that, rather than guessing from which +properties happen to be populated. + +**Why this priority**: Without it, a client that supports more than one asset type has to infer +the type from the shape of the data, which is fragile and breaks as soon as a customer adds a +type. It matters, but a customer with a single asset type per field is unblocked by Story 1 alone. + +**Independent Test**: Point one Image field at an image-style asset and another at a file-style +asset, request both in one query, and confirm the response names a different, accurate type for +each. + +**Acceptance Scenarios**: + +1. **Given** content whose Image field points at an image-style asset and whose File field points + at a file-style asset, **When** a client requests type information for both in one query, + **Then** each reports the name of the specific content type it is, and the two differ. +2. **Given** an asset of a customer-defined type, **When** a client requests type information, + **Then** the reported name is the customer's own type, not a generic asset label. + +--- + +### User Story 3 - Reach the referenced asset's own identity (Priority: P3) + +A customer's front-end needs to build a link to an asset, cache it by identity, or check whether +it is published — so it needs the asset's identifier, its site, its URL mapping and its published +state, requested through the same field that points at it. + +**Why this priority**: Real value, and it removes a class of follow-up requests, but a client can +work around it today by issuing a second query against the asset's own content type. Story 1 has +no such escape. + +**Independent Test**: Request the asset's identifier and published state through an Image field +and confirm they match the asset's own record. + +**Acceptance Scenarios**: + +1. **Given** content whose Image field points at a published asset, **When** a client requests the + asset's identifier and published state through that field, **Then** both are returned and match + the asset's own record. +2. **Given** an asset that lives on a specific site, **When** a client requests the site through + the pointing field, **Then** the correct site is reported. + +--- + +### Edge Cases + +- An Image or File field that is empty must return an explicit empty result, not an error. +- A field that points at an asset that has since been archived or deleted must degrade + predictably rather than failing the whole query. +- A field pointing at an asset in a language the request did not ask for must follow the same + language-fallback behavior the rest of content delivery uses. +- A customer asset type whose property name collides with one of the general asset properties must + resolve to a single, documented meaning — a client must never silently receive one when it + asked for the other. +- A customer who deletes a property that a live client query still asks for must get a clear + error naming the missing property. +- An asset type the requesting user has no permission to read must not leak its property names or + values. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: A client MUST be able to request, through an Image or File field, any property the + customer defined on the content type of the asset that field points at. +- **FR-002**: A client MUST be able to request the tags carried by an asset through the Image or + File field that points at it. +- **FR-003**: The response MUST identify the specific content type of the asset that was returned, + distinguishably for two different asset types. +- **FR-004**: A client MUST be able to request the referenced asset's own identity and state — + its identifier, its internal version reference, its site, its URL mapping, its published state + and its title — through the field that points at it. +- **FR-005**: Properties a customer adds to, or removes from, an asset type MUST become available + or unavailable to clients without any administrative step, restart, or manual registration. +- **FR-006**: The API MUST remain self-documenting: every property a client can request MUST be + discoverable by inspecting the API's own published description of itself, with no reliance on an + untyped escape hatch. +- **FR-007**: An empty Image or File field MUST return an explicit empty result rather than an + error, and MUST NOT fail the rest of the query. +- **FR-008**: Asset properties MUST be subject to the same read-permission rules as the asset + itself; a user who cannot read an asset MUST NOT obtain its property values through a pointing + field. +- **FR-009**: Where a property name could refer either to the referenced asset or to the binary + file it carries, the API MUST give that name exactly one documented meaning, and MUST offer an + unambiguous way to reach the other. +- **FR-009a**: No property name may silently change what it returns. Where this feature changes + the meaning of an existing name, that name MUST be changed too, so the client receives an + explicit failure rather than a different value. A break a client can see is required; a break a + client cannot see is not acceptable. +- **FR-010**: The delivered behavior MUST be covered by an automated API-level test that a + customer-defined property on an extended asset type is readable through both an Image field and + a File field. +- **FR-011**: Reading N properties of one referenced asset MUST NOT cost N times the work of + reading one; per-asset work MUST be performed once per asset per request. +- **FR-012**: Existing customer queries MAY break. This feature is not required to keep the + current asset view working. The break MUST be announced ahead of the release and MUST ship with + migration guidance naming, for each property available today, its replacement or the fact that + it has none. See "Decision: breaking change accepted" below. +- **FR-013**: This feature supersedes PR dotCMS/core#35363. That PR MUST be closed as superseded + rather than merged, and the convenience it aimed at — reading an asset's binary properties + without descending a level — MUST be re-raised as a second, separately tracked stage of issue + #34540, delivered after this one. Issue #34540 MUST remain open after this feature ships, + carrying that remaining scope, so it is not lost. +- **FR-014**: The existing automated check that locks the current shape of an asset-pointing field + MUST be corrected as part of this work. It currently asserts that the asset view exposes *no + property other than* the six it has today, while being named as though it asserts those six are + present. It must assert what it claims. + +### Key Entities + +- **Asset base types**: the two built-in kinds of asset content in dotCMS — one image-oriented, + one file-oriented. Customers extend these to define their own asset types. +- **Customer-defined asset type**: a content type a customer created by extending one of the asset + base types, carrying both the inherited asset properties and the customer's own. +- **Asset-pointing field**: a field on some other content type (Image or File) whose value refers + to one asset contentlet. +- **Published API description**: the machine-readable description of the delivery API that clients + and tooling inspect to learn what can be requested. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 100% of the properties a customer defines on an extended asset type are readable + through an Image or File field that points at content of that type. +- **SC-002**: A customer can retrieve an asset together with its custom properties in a **single** + request, where today the custom properties are unobtainable at any number of requests. +- **SC-003**: A client can correctly determine which of two different asset types it received in + 100% of responses, without inspecting which properties are populated. +- **SC-004**: A property added to an asset type is readable by a client within the same session, + with zero administrative actions performed in between. +- **SC-005**: Every property a client can request appears in the API's published self-description + — no property is reachable only through an untyped escape hatch. +- **SC-006**: Requesting all available properties of a referenced asset performs the same amount + of per-asset work as requesting one, measured as a constant rather than a per-property cost. +- **SC-007**: The AI tagging workflow, which cannot read an asset's tags today, completes + end-to-end. + +## Legacy Considerations *(dotCMS-specific — mandatory)* + +- **Existing behavior touched**: GraphQL content delivery — specifically how a field that points + at an asset is described to clients. This is an actively used delivery surface, not a dormant + legacy corner: customers run production front-ends against it today. The asset base types + themselves are long-standing product surface. + +- **Backward-compatibility expectations**: Customers already query the six properties the current + asset view exposes. Changing how an asset-pointing field is described invalidates those queries + on deploy. **This is accepted** (FR-012): the current view is a facade that reports asset + content using property names borrowed from the other base type, and preserving it would mean + preserving the misdescription permanently. The break ships with announcement and migration + guidance, and FR-009a requires every break to be visible. + +### What breaks, and whether the client can see it + +Measured against a running instance, for the six properties the current view exposes: + +- **Five are invented for image-style assets** — `fileName`, `fileAsset`, `metaData`, + `showOnMenu`, `sortOrder` do not exist on that content at all; the current view synthesizes + them. Removing the facade removes them. A client asking for one gets an outright request + failure, which is loud and immediately visible. +- **One is real but currently masked** — `description`. The current view does not return the + asset's stored description; it returns the asset's title, which for an image is the file name. + The asset's own description is unreachable. After the change the same name returns the stored + value instead. Measured on a real instance: the current view returns a value for 57 of 57 + images, while only 2 of those 57 have a stored description — so 55 of 57 would go from a + populated string to empty, **with no error of any kind**. This is the one case a client cannot + detect, and it is why FR-009a requires renaming rather than silently repurposing it. +- **File-style assets fare better**: their properties are real and merely move from the shared + view to the specific type, so a client recovers all of them by naming the type. + +- **Known related decisions**: Issue dotCMS/core#34540 asks for two things: (a) customer-defined + properties on extended asset types must be queryable, and (b) the ~20 general asset properties + reachable today only through the untyped `_map` escape hatch must be properly described. **This + spec covers (a).** Limb (b) is addressed by in-flight work described below. The plan phase will + formally consult `dotCMS/platform-adrs`. + +### Relationship to in-flight work + +Pull request dotCMS/core#35363 is open and unmerged, and states that it closes #34540. It delivers +limb (b): a shared description of the binary properties an asset carries, so a client can ask for +things like size, MIME type and dimensions directly instead of descending a level. It does **not** +deliver limb (a) — customer-defined properties remain unreachable after it. + +That PR is an **input to be reviewed, not a settled foundation.** The plan phase must evaluate it +on its merits before deciding how this work relates to it, and specifically must reach a position +on each of the following, all of which are visible in the PR as it stands: + +1. **Scope versus claim.** It closes #34540 while delivering only limb (b). Decide whether it + should close the issue at all. +2. **An overloaded property name.** It introduces `title` and `modDate` on the asset-pointing + view meaning the *binary file's* title and date. Everywhere else in the delivery API those + names mean the *content item's* title and date. A customer asking an Image field for `title` + would get the file's. This ambiguity originates in that PR, independently of this feature, and + is what FR-009 exists to resolve. +3. **How the returned type is chosen.** It selects which description to apply by inspecting the + runtime shape of the value being resolved. Assess how robust that is; it is also the exact + decision point this feature must change in order to satisfy FR-003. +4. **Cost per property.** Its resolution of each binary property re-derives the asset's binary + metadata from scratch, with no reuse across the properties of the same asset in the same + request. Selecting all twelve therefore repeats that derivation twelve times per asset, per row + of a result set. Quantify it against FR-011 and decide whether reuse is required. +5. **A hardcoded assumption.** It assumes the binary-carrying property of an image-style asset is + always named `asset`. Confirm that holds for every customer-extended image-style type. +6. **Unfinished verification.** Its own test plan leaves two items unchecked: a manual end-to-end + check through the GraphQL console, and API-level test coverage for these queries. Treat both as + outstanding. + +**Structural tension**: that PR attaches the twelve binary properties to the single fixed asset +view. This feature replaces that fixed view with per-type descriptions, which is how FR-001 and +FR-003 get satisfied — so those twelve properties do not come along, and the queries #35363 +enables would stop working unless carried over. + +### Decision: this feature supersedes PR #35363 + +Recorded as FR-013. The reasoning, verified rather than assumed: + +- **Its benefit does not survive this feature.** #35363 exists to spare the client one level of + nesting: `image { size }` instead of `image { fileAsset { size } }`. But once an asset-pointing + field returns the asset's real type, that type's binary property is already a fully described + binary carrying all twelve of those properties plus a focal point — verified against a running + instance. The client still descends exactly one level, just to the correctly named property. The + saving the PR offers is gone. +- **Its deliverable would have to be rebuilt anyway.** The twelve properties are attached to the + view this feature removes. +- **It is not in a mergeable state.** Its automated checks have been failing since it was last + updated, on a check in its own area, tripped by its own change and left unaddressed. It carries + no review. See FR-014: the check in question is itself wrong and must be corrected regardless. +- **What to keep from it**: its recognition of the problem, and the corrected version of the check + it trips. + +**The remaining scope is not dropped.** Issue #34540 stays open after this feature ships, carrying +the second stage: exposing an asset's binary properties conveniently on the new per-type shape. +That is deliberate sequencing, not an oversight — this feature must land first because the second +stage's design depends on the shape this one establishes. + +## Assumptions + +- The permission model for reading an asset through a pointing field is unchanged: it follows the + asset's own read permissions (FR-008), not those of the content that points at it. +- Language and version resolution for the referenced asset keeps the behavior clients see today; + this feature widens *which properties* are readable, not *which asset version* is selected. +- Write operations are untouched. This is a read/delivery capability only. +- The dynamic registration of customer content types into the delivery API already works and is + not part of this scope — verified against a running instance, where a customer-defined asset + type created moments earlier was already described by the API, carrying its own properties, + with no administrative action. FR-005 therefore records an existing guarantee that must be + preserved, not new work. +- Limb (b) of issue #34540 — conveniently exposing the general asset/binary properties — is out + of scope for this stage. It is **not** tracked through PR #35363, which is superseded (FR-013); + it stays on issue #34540, which remains open after this feature ships. +- "Single request" in SC-002 means one GraphQL query from the client's perspective; it makes no + claim about server-side work. From 775fd560ac17cd7f6211130c622c9d8d13c2c60b Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Mon, 14 Sep 2026 13:27:20 -0600 Subject: [PATCH 2/5] docs(spec): pin down how narrowing clauses behave on asset fields (#34540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four requirements the first draft left implicit, all verified by introspection against a running instance: - Shared asset properties are selectable both directly on the field and inside a clause that narrows to a specific type (FR-015). - Narrowing to a type the asset does not happen to be must not fail the request; it contributes nothing, and a non-fatal warning names the clause that matched nothing, so "wasn't that type" is distinguishable from "wrong type name" (FR-016). There is no cast here to fail — the clause is a condition. - Narrowing to a type that does not exist must still fail loudly (FR-017). - Clauses for the base kind and for the specific type merge into one result object (FR-018). Also records that the hierarchy is exactly two levels deep and cannot grow: a content type may only extend one of the fixed base types, never another content type. A chain of narrowing clauses therefore does not arise. Co-Authored-By: Claude Opus 5 (1M context) --- .../spec.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/specs/34540-graphql-asset-subtype-fields/spec.md b/specs/34540-graphql-asset-subtype-fields/spec.md index 0f317f071168..b5f99c1769e3 100644 --- a/specs/34540-graphql-asset-subtype-fields/spec.md +++ b/specs/34540-graphql-asset-subtype-fields/spec.md @@ -117,6 +117,10 @@ and confirm they match the asset's own record. ### Edge Cases - An Image or File field that is empty must return an explicit empty result, not an error. +- A client narrowing to a type the asset is not must receive the rest of its data plus a warning, + never a failed request (FR-016) — whereas naming a type that does not exist must fail (FR-017). +- A client narrowing to both the base kind and the specific type in one request must receive one + merged object, not two partial ones (FR-018). - A field that points at an asset that has since been archived or deleted must degrade predictably rather than failing the whole query. - A field pointing at an asset in a language the request did not ask for must follow the same @@ -173,6 +177,18 @@ and confirm they match the asset's own record. without descending a level — MUST be re-raised as a second, separately tracked stage of issue #34540, delivered after this one. Issue #34540 MUST remain open after this feature ships, carrying that remaining scope, so it is not lost. +- **FR-015**: The properties shared by every asset MUST be selectable **both** directly on an + asset-pointing field and inside a clause that narrows to a specific type, without the client + having to repeat itself or choose one place over the other. +- **FR-016**: A clause that narrows to a type the returned asset does not happen to be MUST NOT + fail the request. It contributes nothing and the rest of the response is delivered normally. + The response MUST additionally carry a non-fatal warning naming the clause that matched nothing, + so a client can tell "this asset wasn't that type" apart from "I named the wrong type". +- **FR-017**: A clause that narrows to a type that does not exist at all MUST fail the request. + This is a client mistake with no valid reading, and failing loudly is correct. +- **FR-018**: When more than one clause applies to the same returned asset — one narrowing to its + base kind and another to its specific type — their properties MUST merge into a single result + object, with no precedence rule needed and no duplication. - **FR-014**: The existing automated check that locks the current shape of an asset-pointing field MUST be corrected as part of this work. It currently asserts that the asset view exposes *no property other than* the six it has today, while being named as though it asserts those six are @@ -282,6 +298,19 @@ view. This feature replaces that fixed view with per-type descriptions, which is FR-003 get satisfied — so those twelve properties do not come along, and the queries #35363 enables would stop working unless carried over. +### Depth of the type hierarchy + +Verified by introspection against a running instance: the hierarchy an asset-pointing field +exposes is exactly **two levels deep, always** — the asset's base kind, then its specific content +type. It cannot grow deeper, because a dotCMS content type may only extend one of the fixed base +types; it can never extend another content type. The base kind is a plain shared description that +itself sits under nothing, and a specific type names its shared descriptions side by side, not +nested. + +So a client never faces a chain of narrowing clauses. It faces at most one clause for the base +kind and one for the specific type, and per FR-018 those merge. A deeper case does not arise and +the design need not account for one. + ### Decision: this feature supersedes PR #35363 Recorded as FR-013. The reasoning, verified rather than assumed: From 92f5317c870f2c62a5b2c0b886541be8e0f43912 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Mon, 14 Sep 2026 13:33:40 -0600 Subject: [PATCH 3/5] docs(spec): generalize beyond the examples, and span both asset base types (#34540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses @nollymar's change request. Her inline note is about framing, but checking the second half of it turned up a real gap in the proposed shape. Framing (her inline comment on the Overview): - The Overview now leads with "any content type extending either asset base type" and says outright that the banner/PDF cases are illustrations. - Added two acceptance scenarios to User Story 1 stating the general case explicitly, and noting that passing the two example scenarios does not satisfy it. Substance ("expose any dotAsset/fileAsset content type so I can cast any binary to that type"): - FR-001a: the reachable set is every content type extending either base type, including ones created later, and may not be satisfied by enumeration. - FR-001b: an asset-pointing field must span BOTH base types regardless of the field's own kind. Verified on a running instance — an Image field accepts and resolves a reference to file-style content (a plain-text file came back through one). The earlier "shape of the solution" typed each field by its own kind, which would have silently dropped content those fields already hold today. That section is corrected. - FR-016a: the warning behavior holds across a whole result set. Assets that match a clause come back populated, assets that do not still come back, and one non-matching asset must not suppress the matching ones. Also fixes the requirement ordering (FR-014 was listed last). Co-Authored-By: Claude Opus 5 (1M context) --- .../spec.md | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/specs/34540-graphql-asset-subtype-fields/spec.md b/specs/34540-graphql-asset-subtype-fields/spec.md index b5f99c1769e3..cadbd1a1b1c3 100644 --- a/specs/34540-graphql-asset-subtype-fields/spec.md +++ b/specs/34540-graphql-asset-subtype-fields/spec.md @@ -14,10 +14,14 @@ ## Overview -dotCMS lets a customer model their own kinds of assets by extending the two asset base types: a -banner image that also carries a campaign name and an ad size, a PDF that also carries a category -and a download count. The authoring side of that works today — the customer defines the fields, -editors fill them in, the values are stored and versioned. +dotCMS lets a customer model their own kinds of assets by extending either of the two asset base +types. **Any** content type built that way is in scope here — there is no privileged set. The +authoring side works today: the customer defines the fields, editors fill them in, the values are +stored and versioned. + +*(Throughout this document, a banner image carrying a campaign name and a PDF carrying a category +appear as illustrations only. Nothing in the requirements is specific to them; wherever an example +is named, read it as "any content type extending that base type".)* The reading side does not. When another piece of content points at one of those assets through an Image or a File field, a GraphQL client can only ever see a fixed, six-property view of it. The @@ -63,6 +67,14 @@ story already unblocks the tagging workflow and custom asset metadata. 4. **Given** a customer adds a new property to an existing asset type, **When** a client requests that property through an Image or File field without any administrative action in between, **Then** the new property is available and returns its stored value. +5. **Given** *any* content type extending either asset base type — not only the two used as + illustrations above — and any property the customer defined on it, **When** a client requests + that property through a field pointing at content of that type, **Then** the stored value is + returned. This scenario is the general case; scenarios 1-2 are instances of it and passing + them alone does not satisfy it. +6. **Given** an Image field pointing at file-style content, or a File field pointing at + image-style content, **When** a client narrows to the actual type of what is referenced, + **Then** that type is offered and its properties are returned. --- @@ -139,6 +151,15 @@ and confirm they match the asset's own record. - **FR-001**: A client MUST be able to request, through an Image or File field, any property the customer defined on the content type of the asset that field points at. +- **FR-001a**: Every content type extending either asset base type MUST be reachable through an + asset-pointing field — the whole open-ended set, including types a customer creates later. No + subset may be privileged, and the capability MUST NOT be satisfiable by enumerating known types. +- **FR-001b**: An asset-pointing field MUST be able to return, and a client MUST be able to narrow + to, content of **either** base type regardless of which kind of field is doing the pointing. + Verified against a running instance: an Image field accepts and resolves a reference to + file-style content (a plain-text file was returned through one), so a client narrowing on that + field MUST be offered file-style types as well as image-style ones. Typing the field by the + field's own kind would silently drop content the field can already hold today. - **FR-002**: A client MUST be able to request the tags carried by an asset through the Image or File field that points at it. - **FR-003**: The response MUST identify the specific content type of the asset that was returned, @@ -177,6 +198,10 @@ and confirm they match the asset's own record. without descending a level — MUST be re-raised as a second, separately tracked stage of issue #34540, delivered after this one. Issue #34540 MUST remain open after this feature ships, carrying that remaining scope, so it is not lost. +- **FR-014**: The existing automated check that locks the current shape of an asset-pointing field + MUST be corrected as part of this work. It currently asserts that the asset view exposes *no + property other than* the six it has today, while being named as though it asserts those six are + present. It must assert what it claims. - **FR-015**: The properties shared by every asset MUST be selectable **both** directly on an asset-pointing field and inside a clause that narrows to a specific type, without the client having to repeat itself or choose one place over the other. @@ -184,16 +209,17 @@ and confirm they match the asset's own record. fail the request. It contributes nothing and the rest of the response is delivered normally. The response MUST additionally carry a non-fatal warning naming the clause that matched nothing, so a client can tell "this asset wasn't that type" apart from "I named the wrong type". +- **FR-016a**: This MUST hold across a whole result set, not just a single asset. When one request + returns many assets of differing types, every asset that matches a clause MUST be returned with + those properties populated, every asset that does not MUST still be returned without them, and + the request as a whole MUST succeed. A single non-matching asset MUST NOT suppress the matching + ones. Warnings MUST identify which clause matched nothing rather than being a single opaque + flag on the response. - **FR-017**: A clause that narrows to a type that does not exist at all MUST fail the request. This is a client mistake with no valid reading, and failing loudly is correct. - **FR-018**: When more than one clause applies to the same returned asset — one narrowing to its base kind and another to its specific type — their properties MUST merge into a single result object, with no precedence rule needed and no duplication. -- **FR-014**: The existing automated check that locks the current shape of an asset-pointing field - MUST be corrected as part of this work. It currently asserts that the asset view exposes *no - property other than* the six it has today, while being named as though it asserts those six are - present. It must assert what it claims. - ### Key Entities - **Asset base types**: the two built-in kinds of asset content in dotCMS — one image-oriented, From 3f3465fb4c860d66d810d4291e63e22eae4b378b Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Wed, 16 Sep 2026 08:07:21 -0600 Subject: [PATCH 4/5] docs(spec): follow expand/retire instead of breaking the contract (#34540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ADR consultation that gates planning turned up a conflict with the decision this spec had already recorded, so the decision is reversed. Two accepted decisions in platform-adrs govern a published contract: - ADR-0020 deprecated a core REST endpoint and kept it functional, annotated for removal, existing integrations explicitly unaffected. - ADR-0022 states that "a URL, parameter, or response field is a promise to callers in exactly the same way a database column is a promise to queries", and names why it bites here: dotCMS is self-hosted and upgraded on the customer's schedule, so "nobody is calling the old endpoint right now" is not the same question as "it's safe to remove." It prescribes expand -> adopt -> bake -> retire, gates removal on a supported-version floor plus a confirmed zero-use window, and requires the deprecation to be marked in the schema itself rather than only in code. So instead of replacing the asset view in place: - FR-012 keeps every existing selection working and returning what it returns today; the new capability ships alongside. - FR-012a marks the current view as superseded in the published API description, naming each property's replacement. - FR-012b puts removal out of scope for this feature, behind the same gate, and requires the retirement ticket to be opened up front. - FR-009a is reframed: rather than renaming the one property that would have changed meaning, the corrected value is reached through the new surface and the existing name is left alone. - SC-008 and SC-009 make the guarantee measurable. The decisive case is unchanged and now argues the other way: the asset description would have kept returning a string while changing which string — 57 of 57 images populated today, 2 of 57 afterwards. A self-hosted customer who had not upgraded would have seen no failure, just different data. Adds an ADR Alignment section. No exception to either ADR is requested. Co-Authored-By: Claude Opus 5 (1M context) --- .../spec.md | 144 +++++++++--------- 1 file changed, 73 insertions(+), 71 deletions(-) diff --git a/specs/34540-graphql-asset-subtype-fields/spec.md b/specs/34540-graphql-asset-subtype-fields/spec.md index cadbd1a1b1c3..0101848bdb64 100644 --- a/specs/34540-graphql-asset-subtype-fields/spec.md +++ b/specs/34540-graphql-asset-subtype-fields/spec.md @@ -180,19 +180,30 @@ and confirm they match the asset's own record. - **FR-009**: Where a property name could refer either to the referenced asset or to the binary file it carries, the API MUST give that name exactly one documented meaning, and MUST offer an unambiguous way to reach the other. -- **FR-009a**: No property name may silently change what it returns. Where this feature changes - the meaning of an existing name, that name MUST be changed too, so the client receives an - explicit failure rather than a different value. A break a client can see is required; a break a - client cannot see is not acceptable. +- **FR-009a**: No existing property name may change what it returns — not visibly, and above all + not silently. Where this feature exposes a value that differs from what a name returns today, + it MUST do so through a **new** surface and leave the existing one alone. The worked example is + the asset description: today that name returns the asset's title, and the asset's own stored + description is unreachable. Both MUST be available afterwards, under names that cannot be + confused, and the existing name MUST keep returning what it returns today. - **FR-010**: The delivered behavior MUST be covered by an automated API-level test that a customer-defined property on an extended asset type is readable through both an Image field and a File field. - **FR-011**: Reading N properties of one referenced asset MUST NOT cost N times the work of reading one; per-asset work MUST be performed once per asset per request. -- **FR-012**: Existing customer queries MAY break. This feature is not required to keep the - current asset view working. The break MUST be announced ahead of the release and MUST ship with - migration guidance naming, for each property available today, its replacement or the fact that - it has none. See "Decision: breaking change accepted" below. +- **FR-012**: Existing customer queries MUST keep working unchanged. Every property the current + asset view exposes MUST continue to be selectable and MUST continue to return exactly what it + returns today. The new capability is delivered **alongside** the current view, not by replacing + it. +- **FR-012a**: The current asset view MUST be marked as superseded **in the published API + description itself**, not only in code or release notes, so that a client's own tooling surfaces + the warning without anyone reading a changelog. The marking MUST say what replaces each property. +- **FR-012b**: Removal of the current asset view is **out of scope for this feature** and MUST NOT + happen in the same release that introduces the replacement. It is a later, separately tracked + step, gated on an explicit floor: the oldest still-supported release no longer depending on it, + **and** a confirmed observation window with no remaining use. A tracking item for that removal + MUST be opened when this feature ships, naming the surface to be retired — not deferred to a + later cleanup pass. - **FR-013**: This feature supersedes PR dotCMS/core#35363. That PR MUST be closed as superseded rather than merged, and the convenience it aimed at — reading an asset's binary properties without descending a level — MUST be re-raised as a second, separately tracked stage of issue @@ -249,6 +260,11 @@ and confirm they match the asset's own record. of per-asset work as requesting one, measured as a constant rather than a per-property cost. - **SC-007**: The AI tagging workflow, which cannot read an asset's tags today, completes end-to-end. +- **SC-008**: Zero existing customer queries stop working. Every selection valid against the + current asset view is still valid after this feature ships and returns the same value it + returned before. +- **SC-009**: A client inspecting the API's own published description sees the current asset view + marked as superseded, with its replacement named — without reading release notes. ## Legacy Considerations *(dotCMS-specific — mandatory)* @@ -258,71 +274,51 @@ and confirm they match the asset's own record. themselves are long-standing product surface. - **Backward-compatibility expectations**: Customers already query the six properties the current - asset view exposes. Changing how an asset-pointing field is described invalidates those queries - on deploy. **This is accepted** (FR-012): the current view is a facade that reports asset - content using property names borrowed from the other base type, and preserving it would mean - preserving the misdescription permanently. The break ships with announcement and migration - guidance, and FR-009a requires every break to be visible. + asset view exposes. **Those queries must keep working, unchanged** (FR-012). The new capability + ships alongside the current view; the current view is marked as superseded in the published API + description (FR-012a) and retired later, as a separately gated step (FR-012b). -### What breaks, and whether the client can see it + An earlier draft of this spec accepted breaking those queries outright. That was reversed after + consulting the accepted architecture decisions — see ADR Alignment below. -Measured against a running instance, for the six properties the current view exposes: +### ADR Alignment -- **Five are invented for image-style assets** — `fileName`, `fileAsset`, `metaData`, - `showOnMenu`, `sortOrder` do not exist on that content at all; the current view synthesizes - them. Removing the facade removes them. A client asking for one gets an outright request - failure, which is loud and immediately visible. -- **One is real but currently masked** — `description`. The current view does not return the +Two accepted decisions in `dotCMS/platform-adrs` govern how a published contract may change, and +both were consulted before this spec was finalized: + +- **ADR-0020** (accepted) deprecated a core REST endpoint and kept it **functional**, annotated for + removal, with existing integrations explicitly unaffected. +- **ADR-0022** (accepted) states the principle directly — *"a URL, parameter, or response field is + a promise to callers in exactly the same way a database column is a promise to queries"* — and + names why it bites here: dotCMS is self-hosted and upgraded by customers on their own schedule, + so *"'nobody is calling the old endpoint right now' is not the same question as 'it's safe to + remove.'"* It prescribes **expand → adopt → bake → retire**, gates removal on both a + supported-version floor and a confirmed zero-use window, and requires the deprecation to be + marked **in the schema itself**, not only in code. + +This feature follows that pattern: FR-012 expands, FR-012a marks, FR-012b defers retirement behind +the same gate and requires the tracking item to be opened up front rather than left to a later +cleanup pass. **No exception to either ADR is requested.** + +### Why breaking was rejected, and what the facade actually hides + +Measured against a running instance, the six properties the current view exposes split in two: + +- **Five are invented for image-style assets** — the file name, the binary, the metadata, the menu + flag and the sort order do not exist on that content at all; the current view synthesizes them, + borrowing names from the other base type. Removing the view would remove them outright, and a + client asking for one would get an immediate request failure. +- **One is real but currently masked** — the description. The current view does not return the asset's stored description; it returns the asset's title, which for an image is the file name. - The asset's own description is unreachable. After the change the same name returns the stored - value instead. Measured on a real instance: the current view returns a value for 57 of 57 - images, while only 2 of those 57 have a stored description — so 55 of 57 would go from a - populated string to empty, **with no error of any kind**. This is the one case a client cannot - detect, and it is why FR-009a requires renaming rather than silently repurposing it. -- **File-style assets fare better**: their properties are real and merely move from the shared - view to the specific type, so a client recovers all of them by naming the type. - -- **Known related decisions**: Issue dotCMS/core#34540 asks for two things: (a) customer-defined - properties on extended asset types must be queryable, and (b) the ~20 general asset properties - reachable today only through the untyped `_map` escape hatch must be properly described. **This - spec covers (a).** Limb (b) is addressed by in-flight work described below. The plan phase will - formally consult `dotCMS/platform-adrs`. - -### Relationship to in-flight work - -Pull request dotCMS/core#35363 is open and unmerged, and states that it closes #34540. It delivers -limb (b): a shared description of the binary properties an asset carries, so a client can ask for -things like size, MIME type and dimensions directly instead of descending a level. It does **not** -deliver limb (a) — customer-defined properties remain unreachable after it. - -That PR is an **input to be reviewed, not a settled foundation.** The plan phase must evaluate it -on its merits before deciding how this work relates to it, and specifically must reach a position -on each of the following, all of which are visible in the PR as it stands: - -1. **Scope versus claim.** It closes #34540 while delivering only limb (b). Decide whether it - should close the issue at all. -2. **An overloaded property name.** It introduces `title` and `modDate` on the asset-pointing - view meaning the *binary file's* title and date. Everywhere else in the delivery API those - names mean the *content item's* title and date. A customer asking an Image field for `title` - would get the file's. This ambiguity originates in that PR, independently of this feature, and - is what FR-009 exists to resolve. -3. **How the returned type is chosen.** It selects which description to apply by inspecting the - runtime shape of the value being resolved. Assess how robust that is; it is also the exact - decision point this feature must change in order to satisfy FR-003. -4. **Cost per property.** Its resolution of each binary property re-derives the asset's binary - metadata from scratch, with no reuse across the properties of the same asset in the same - request. Selecting all twelve therefore repeats that derivation twelve times per asset, per row - of a result set. Quantify it against FR-011 and decide whether reuse is required. -5. **A hardcoded assumption.** It assumes the binary-carrying property of an image-style asset is - always named `asset`. Confirm that holds for every customer-extended image-style type. -6. **Unfinished verification.** Its own test plan leaves two items unchecked: a manual end-to-end - check through the GraphQL console, and API-level test coverage for these queries. Treat both as - outstanding. - -**Structural tension**: that PR attaches the twelve binary properties to the single fixed asset -view. This feature replaces that fixed view with per-type descriptions, which is how FR-001 and -FR-003 get satisfied — so those twelve properties do not come along, and the queries #35363 -enables would stop working unless carried over. + The stored description is unreachable. Had the view been replaced in place, that name would have + kept working and kept returning a string, but a different one: measured on a real instance, the + current view returns a value for 57 of 57 images while only 2 of those 57 have a stored + description. So 55 of 57 would have gone from a populated string to empty, **with no error of + any kind** — undetectable by the client. + +That second case is decisive, and it is why FR-012 keeps the current view intact and FR-009a +routes the real value through a new surface instead. A self-hosted customer who has not upgraded +would not have seen a failure; they would have seen different data. ### Depth of the type hierarchy @@ -347,8 +343,10 @@ Recorded as FR-013. The reasoning, verified rather than assumed: binary carrying all twelve of those properties plus a focal point — verified against a running instance. The client still descends exactly one level, just to the correctly named property. The saving the PR offers is gone. -- **Its deliverable would have to be rebuilt anyway.** The twelve properties are attached to the - view this feature removes. +- **Its deliverable lands on a view that is now on a retirement path.** The twelve properties + would be attached to the current asset view, which FR-012a marks as superseded and FR-012b + schedules for removal — so the work would be spent extending a surface already being retired, + and would have to be redone on the replacement. - **It is not in a mergeable state.** Its automated checks have been failing since it was last updated, on a check in its own area, tripped by its own change and left unaddressed. It carries no review. See FR-014: the check in question is itself wrong and must be corrected regardless. @@ -377,3 +375,7 @@ stage's design depends on the shape this one establishes. it stays on issue #34540, which remains open after this feature ships. - "Single request" in SC-002 means one GraphQL query from the client's perspective; it makes no claim about server-side work. +- The current asset view keeps its exact present behavior for the whole of its remaining life, + including the two synthesized property values it derives rather than stores. Correcting them + would be a silent change to a live contract, which FR-009a forbids; the corrected values are + reached through the new surface instead. From 5e00776a990937bb2647e91f4556858941c5e25e Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Fri, 18 Sep 2026 20:49:39 -0600 Subject: [PATCH 5/5] =?UTF-8?q?docs(spec):=20the=20flat=20view=20is=20repl?= =?UTF-8?q?aced,=20not=20kept=20alongside=20=E2=80=94=20a=20product=20deci?= =?UTF-8?q?sion=20(#34540)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings this branch's spec.md in line with what was actually built. The version approved here described a non-breaking design; the implementation took the breaking one, and the approved spec must be the spec that shipped. What changed, and why it needs re-approval: - FR-012 superseded. Five of the six flat properties survive unchanged; `description` and the non-asset-content case do not. Both fail VISIBLY. - FR-012a and FR-012b no longer apply — there is no surviving surface to mark as superseded, and retirement is not deferred. - FR-012c added: the break ships with announcement and migration guidance. - FR-009a survives intact and is why the breaks take the shape they do: nothing may change value silently. A removal a client can see is acceptable; a name that keeps working and returns something else is not. - SC-008/SC-009 now measure five-of-six correct, and ZERO selections that keep working while returning different data. - ADR Alignment asks for an exception to ADR-0022 knowingly, rather than claiming none is needed. It records that the compliant expand-phase design was built and verified green before being set aside, and that the constraint forcing the choice is not negotiable: a GraphQL field has one type and a resolved value one runtime type, so the flat view and the asset itself cannot share a position. The decisive measurement is unchanged and now argues the other way: the asset description would have kept returning a string while changing WHICH string — populated for 57 of 57 images before, 2 of 57 after. Removing the name makes that visible instead. Co-Authored-By: Claude Opus 5 (1M context) --- .../spec.md | 169 +++++++++++------- 1 file changed, 109 insertions(+), 60 deletions(-) diff --git a/specs/34540-graphql-asset-subtype-fields/spec.md b/specs/34540-graphql-asset-subtype-fields/spec.md index 0101848bdb64..a7d3de0af7cf 100644 --- a/specs/34540-graphql-asset-subtype-fields/spec.md +++ b/specs/34540-graphql-asset-subtype-fields/spec.md @@ -180,30 +180,35 @@ and confirm they match the asset's own record. - **FR-009**: Where a property name could refer either to the referenced asset or to the binary file it carries, the API MUST give that name exactly one documented meaning, and MUST offer an unambiguous way to reach the other. -- **FR-009a**: No existing property name may change what it returns — not visibly, and above all - not silently. Where this feature exposes a value that differs from what a name returns today, - it MUST do so through a **new** surface and leave the existing one alone. The worked example is - the asset description: today that name returns the asset's title, and the asset's own stored - description is unreachable. Both MUST be available afterwards, under names that cannot be - confused, and the existing name MUST keep returning what it returns today. +- **FR-009a**: No property name may **silently** change what it returns. This requirement survives + the decision to break: where a value differs from what a name returns today, that name MUST be + removed rather than repurposed, so the client receives an explicit failure instead of different + data. The worked example is the asset description — removed from the asset-pointing field rather + than left in place answering with a new value. - **FR-010**: The delivered behavior MUST be covered by an automated API-level test that a customer-defined property on an extended asset type is readable through both an Image field and a File field. - **FR-011**: Reading N properties of one referenced asset MUST NOT cost N times the work of reading one; per-asset work MUST be performed once per asset per request. -- **FR-012**: Existing customer queries MUST keep working unchanged. Every property the current - asset view exposes MUST continue to be selectable and MUST continue to return exactly what it - returns today. The new capability is delivered **alongside** the current view, not by replacing - it. -- **FR-012a**: The current asset view MUST be marked as superseded **in the published API - description itself**, not only in code or release notes, so that a client's own tooling surfaces - the warning without anyone reading a changelog. The marking MUST say what replaces each property. -- **FR-012b**: Removal of the current asset view is **out of scope for this feature** and MUST NOT - happen in the same release that introduces the replacement. It is a later, separately tracked - step, gated on an explicit floor: the oldest still-supported release no longer depending on it, - **and** a confirmed observation window with no remaining use. A tracking item for that removal - MUST be opened when this feature ships, naming the surface to be retired — not deferred to a - later cleanup pass. +- **FR-012**: *(Superseded — see "Decision: the flat view is replaced, not kept alongside" below.)* + ~~Existing customer queries MUST keep working unchanged.~~ The flat asset view is **replaced**. + Two selections stop working, and both MUST fail **visibly** rather than return a different value: + - `description` on an asset-pointing field. It is the one property whose meaning differs between + the flat view (the contentlet title) and the content answering it (a stored description), so it + cannot be carried over without changing what a live query returns. + - An asset-pointing field aimed at content that is not an asset now resolves to nothing rather + than to the flat view. + + Every other property the flat view exposed — `fileName`, `fileAsset`, `metaData`, `showOnMenu`, + `sortOrder` — MUST remain selectable **and** return exactly what it returns today, on every + surface a client can narrow through. +- **FR-012a**: *(No longer applicable.)* There is no surviving surface to mark as superseded. What + replaces it is documentation and release communication, not an in-schema deprecation. +- **FR-012b**: *(No longer applicable.)* Retirement is not deferred; it happens in this feature. + The tracking item this requirement called for is therefore not opened. +- **FR-012c**: The break MUST be announced ahead of the release and MUST ship with migration + guidance naming, for each removed selection, its replacement — `description` through a narrowing + clause on the concrete content type, where it returns the stored value rather than the title. - **FR-013**: This feature supersedes PR dotCMS/core#35363. That PR MUST be closed as superseded rather than merged, and the convenience it aimed at — reading an asset's binary properties without descending a level — MUST be re-raised as a second, separately tracked stage of issue @@ -260,11 +265,12 @@ and confirm they match the asset's own record. of per-asset work as requesting one, measured as a constant rather than a per-property cost. - **SC-007**: The AI tagging workflow, which cannot read an asset's tags today, completes end-to-end. -- **SC-008**: Zero existing customer queries stop working. Every selection valid against the - current asset view is still valid after this feature ships and returns the same value it - returned before. -- **SC-009**: A client inspecting the API's own published description sees the current asset view - marked as superseded, with its replacement named — without reading release notes. +- **SC-008**: Of the six properties the flat asset view exposed, **five** are still selectable + after this feature ships and return the same values they returned before. The sixth, + `description`, fails explicitly rather than returning a different value. +- **SC-009**: Every removed selection fails visibly. Zero selections keep working while returning + different data — measured by querying each removed name and confirming an error rather than a + value. ## Legacy Considerations *(dotCMS-specific — mandatory)* @@ -273,13 +279,14 @@ and confirm they match the asset's own record. legacy corner: customers run production front-ends against it today. The asset base types themselves are long-standing product surface. -- **Backward-compatibility expectations**: Customers already query the six properties the current - asset view exposes. **Those queries must keep working, unchanged** (FR-012). The new capability - ships alongside the current view; the current view is marked as superseded in the published API - description (FR-012a) and retired later, as a separately gated step (FR-012b). +- **Backward-compatibility expectations**: Customers already query the six properties the flat + asset view exposes. **Five keep working and return the same values; one does not** (FR-012), and + an asset field aimed at non-asset content stops resolving. Both breaks are visible. - An earlier draft of this spec accepted breaking those queries outright. That was reversed after - consulting the accepted architecture decisions — see ADR Alignment below. + This spec changed position twice. It first accepted breaking, then reversed to a non-breaking + design after consulting the accepted architecture decisions, and has now returned to breaking — + **as a product decision, not a technical one**. See "Decision: the flat view is replaced" and + ADR Alignment below. ### ADR Alignment @@ -296,29 +303,73 @@ both were consulted before this spec was finalized: supported-version floor and a confirmed zero-use window, and requires the deprecation to be marked **in the schema itself**, not only in code. -This feature follows that pattern: FR-012 expands, FR-012a marks, FR-012b defers retirement behind -the same gate and requires the tracking item to be opened up front rather than left to a later -cleanup pass. **No exception to either ADR is requested.** - -### Why breaking was rejected, and what the facade actually hides - -Measured against a running instance, the six properties the current view exposes split in two: - -- **Five are invented for image-style assets** — the file name, the binary, the metadata, the menu - flag and the sort order do not exist on that content at all; the current view synthesizes them, - borrowing names from the other base type. Removing the view would remove them outright, and a - client asking for one would get an immediate request failure. -- **One is real but currently masked** — the description. The current view does not return the - asset's stored description; it returns the asset's title, which for an image is the file name. - The stored description is unreachable. Had the view been replaced in place, that name would have - kept working and kept returning a string, but a different one: measured on a real instance, the - current view returns a value for 57 of 57 images while only 2 of those 57 have a stored - description. So 55 of 57 would have gone from a populated string to empty, **with no error of - any kind** — undetectable by the client. - -That second case is decisive, and it is why FR-012 keeps the current view intact and FR-009a -routes the real value through a new surface instead. A self-hosted customer who has not upgraded -would not have seen a failure; they would have seen different data. +**An exception to ADR-0022 IS requested, knowingly.** This feature does not expand, adopt, bake and +retire — it retires now. The expand phase was designed, implemented and verified green (a companion +field beside each asset field, breaking nothing), and was then set aside because it could not put +the narrowing clauses in the same block as the flat properties. That ergonomic difference — one +block instead of two — was judged by product to be worth the break. + +The technical constraint is not negotiable and is worth recording, because it is what makes the +compliant option unable to deliver the requested shape: a GraphQL field has exactly one type and a +resolved value has exactly one runtime type, so the flat view and the asset itself — two +descriptions of the same content — cannot occupy the same position. Keeping both means keeping them +at different positions, which is precisely what the expand-phase design did. + +What the exception preserves from the ADR's intent: + +- The break is **visible**, never silent. ADR-0022's concern is a self-hosted customer who upgrades + on their own schedule and cannot tell that data changed; every removal here fails loudly + (FR-009a). +- Five of the six properties are carried over unchanged, so the blast radius is one property plus + one edge case, not the whole surface. +- FR-012c requires the announcement and migration guidance the ADR's process would otherwise have + provided through the bake window. + +**Sign-off needed**: @fmontes as an author of ADR-0020, and @nollymar who approved the spec in its +non-breaking form (PR #37537). Neither has agreed to this exception yet — it is recorded here as +requested, not granted. + +### Decision: the flat view is replaced, not kept alongside + +**This is a product decision, and it overrides what the rest of this section originally argued.** +Recorded in full because the reasoning ran both ways and a later reader will otherwise assume the +compliant option was never available. + +**What was built and set aside.** A non-breaking design was implemented and verified green: a +companion field beside each asset field (`image` gaining `imageContent`), typed by the asset +interface. Nothing broke, the spec needed no change, and no ADR exception was required. It was set +aside for one reason — the narrowing clauses lived in a second block rather than beside the flat +properties: + +``` +imageContent { ... on Images { tags } } # what the compliant option offered +image { fileName ... on Images { tags } } # what was asked for +``` + +**Why the compliant option could not deliver the requested shape.** A GraphQL field has exactly one +type, and a resolved value has exactly one runtime type. The flat view and the asset itself are two +descriptions of the same content, so they cannot occupy the same position — keeping both means +keeping them at different positions. That is a property of GraphQL, not of this implementation, and +no amount of work removes it. + +**What the break actually costs**, measured rather than estimated: + +- **Five of the six properties survive unchanged** — `fileName`, `fileAsset`, `metaData`, + `showOnMenu`, `sortOrder`. They are synthesized onto DOTASSET-derived types using the very same + fetchers the flat view used, so they answer identically. Notably `fileName`, which was never a + stored value for that content. +- **`description` does not.** It is the one property whose meaning differs: the flat view answered + with the contentlet title, while the content answering it stores something else. On a real + instance the flat view returned a value for 57 of 57 images while only 2 of those 57 have a + stored description. Carrying the name over would have returned different data **without + failing** — so it is removed instead, and fails loudly. +- **An asset field aimed at non-asset content** now resolves to nothing rather than to the flat + view. Forced: a contentlet outside the interface cannot be handed on, and doing so fails the + entire request with `UnresolvedTypeException` rather than just that field. + +**What is preserved from the earlier position.** FR-009a survives intact and is the reason the two +breaks take the shape they do: nothing may change value silently. A removal a client can see is +acceptable; a name that keeps working and returns something else is not. ### Depth of the type hierarchy @@ -344,9 +395,8 @@ Recorded as FR-013. The reasoning, verified rather than assumed: instance. The client still descends exactly one level, just to the correctly named property. The saving the PR offers is gone. - **Its deliverable lands on a view that is now on a retirement path.** The twelve properties - would be attached to the current asset view, which FR-012a marks as superseded and FR-012b - schedules for removal — so the work would be spent extending a surface already being retired, - and would have to be redone on the replacement. + would be attached to the flat asset view, which this feature removes — so the work would be + spent extending a surface that no longer exists and would have to be redone on the interface. - **It is not in a mergeable state.** Its automated checks have been failing since it was last updated, on a check in its own area, tripped by its own change and left unaddressed. It carries no review. See FR-014: the check in question is itself wrong and must be corrected regardless. @@ -375,7 +425,6 @@ stage's design depends on the shape this one establishes. it stays on issue #34540, which remains open after this feature ships. - "Single request" in SC-002 means one GraphQL query from the client's perspective; it makes no claim about server-side work. -- The current asset view keeps its exact present behavior for the whole of its remaining life, - including the two synthesized property values it derives rather than stores. Correcting them - would be a silent change to a live contract, which FR-009a forbids; the corrected values are - reached through the new surface instead. +- The five surviving flat properties keep their exact present behaviour, including the synthesized + `fileName` that derives rather than stores its value. Correcting it would be a silent change to a + live contract, which FR-009a forbids.