diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index e2cc12b..96010b7 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,5 +1,7 @@ # Retrofit annotation commit (opsx-annotate, 2026-05-24) 770ffed41a22f3a45e00e98a8484ca72330eefc9 +# Retrofit reverse-spec annotations (opsx-reverse-spec, 2026-05-24, app-icon-management UUID) +01a2862 # Retrofit reverse-spec annotations (opsx-reverse-spec, 2026-05-24, openbuilt-runtime MCP) 8af6efa diff --git a/lib/Service/IconService.php b/lib/Service/IconService.php index 4fb496f..edfa991 100644 --- a/lib/Service/IconService.php +++ b/lib/Service/IconService.php @@ -30,6 +30,7 @@ * @spec openspec/changes/retrofit-2026-05-24-annotate-openbuilt/tasks.md#task-1 * @spec openspec/changes/retrofit-2026-05-24-annotate-openbuilt/tasks.md#task-2 * @spec openspec/changes/retrofit-2026-05-24-annotate-openbuilt/tasks.md#task-3 + * @spec openspec/changes/retrofit-2026-05-24-app-icon-management-uuid/tasks.md#task-1 */ declare(strict_types=1); diff --git a/openspec/changes/archive/retrofit-2026-05-24-app-icon-management-uuid/design.md b/openspec/changes/archive/retrofit-2026-05-24-app-icon-management-uuid/design.md new file mode 100644 index 0000000..9fe08e4 --- /dev/null +++ b/openspec/changes/archive/retrofit-2026-05-24-app-icon-management-uuid/design.md @@ -0,0 +1,39 @@ +# Design — Retrofit app-icon-management UUID resolution + +> Retrofit change. Tasks describe retroactive annotation, not new implementation +> work. The code already exists at HEAD. + +## Context + +The 2026-05-24 coverage scan dropped `IconService::extractUuid` into +Bucket 2a (`app-icon-management`) because the capability's existing +REQs (REQ-OBICON-001..004) specify the icon-serving endpoints, the +top-level `icon`/`iconDark` fields on the Application schema, and the +upload UX — but none of them name the helper that turns an OR +Application array into the UUID handed to `FileService::getFile`. + +## Decisions + +- **Extend not cluster.** The helper is plumbing for the icon endpoints + already covered by REQ-OBICON-002..003. A new capability for a + 3-fallback accessor would be REQ inflation. +- **One REQ, not three.** The three fallback locations (`@self.id`, + `@self.uuid`, top-level `uuid`) are one observable behaviour — "give + me the UUID for this Application, however OR happens to have shaped + it". Split into three REQs would inflate without adding testable + surface. +- **Codify the order.** The spec pins the fallback order. The order + matters: `@self.id` is the modern OR shape, top-level `uuid` is the + legacy shape; flipping the order would let stale legacy fields + shadow a present-day `@self.id` when both are set. +- **Null is part of the contract.** Returning `null` (vs throwing) is + observable and load-bearing — the icon-serving endpoint's fallback + chain (Decision 2 in design.md of the original change) relies on it + to step through to the filesystem fallbacks. The REQ pins this so + it can't be refactored into an exception without a spec change. + +## Out of scope + +- Caching the resolved UUID — out of scope; the call is microseconds. +- Validating UUID shape (RFC 4122) — out of scope; OR is the source + of truth for what counts as a valid UUID. diff --git a/openspec/changes/archive/retrofit-2026-05-24-app-icon-management-uuid/proposal.md b/openspec/changes/archive/retrofit-2026-05-24-app-icon-management-uuid/proposal.md new file mode 100644 index 0000000..206d207 --- /dev/null +++ b/openspec/changes/archive/retrofit-2026-05-24-app-icon-management-uuid/proposal.md @@ -0,0 +1,25 @@ +# Retrofit — app-icon-management (UUID resolution) + +Describes the observed behaviour of `IconService::extractUuid` as 1 new +REQ added to the `app-icon-management` capability. Code already exists — +this change retroactively specifies it. + +## Affected code units + +- lib/Service/IconService.php::extractUuid + +## Approach + +- Single helper, single REQ. The method governs how `IconService` + derives the OR object UUID it then hands to `FileService::getFile` + when pulling the icon attachment off the Application record. +- Folded into `app-icon-management` (extend) because the helper exists + exclusively in service of the icon-serving endpoints already + specified by REQ-OBICON-002..003. A standalone capability for a + 3-fallback UUID accessor would be REQ inflation. +- Scenarios codify the documented fallback order (`@self.id` → + `@self.uuid` → top-level `uuid`) so future refactors can't quietly + drop a fallback step without changing the spec. + +Source: `openspec/coverage-report.md` generated 2026-05-24. See +[retrofit playbook](../../../../hydra/.github/docs/claude/retrofit.md). diff --git a/openspec/changes/archive/retrofit-2026-05-24-app-icon-management-uuid/specs/app-icon-management/spec.md b/openspec/changes/archive/retrofit-2026-05-24-app-icon-management-uuid/specs/app-icon-management/spec.md new file mode 100644 index 0000000..fcf7a28 --- /dev/null +++ b/openspec/changes/archive/retrofit-2026-05-24-app-icon-management-uuid/specs/app-icon-management/spec.md @@ -0,0 +1,63 @@ +--- +retrofit_extensions: + - REQ-OBICON-005 +--- + +# app-icon-management Specification Delta (Retrofit — UUID resolution) + +## Requirements + +### Requirement: Application UUID resolution for icon attachment lookup + +`IconService` SHALL derive the OR object UUID it uses for icon +attachment lookups (`FileService::getFile`) from a normalised +Application array. The derivation SHALL walk three fallback locations +in this order: `@self.id`, `@self.uuid`, then top-level `uuid`. Each +candidate SHALL be accepted only when it is a non-empty string; +anything else (missing key, null, empty string, non-string scalar) +SHALL be skipped without raising. When no candidate produces a usable +value the helper SHALL return `null`, and the calling +`fetchAttachedFileStream` SHALL surface that as a short-circuit +fallback (no OR call, downstream fallback chain runs) — not as an +exception. + +**ID:** REQ-OBICON-005 + +#### Scenario: UUID lifted from @self.id + +- **GIVEN** an Application array of shape + `{ '@self': { id: 'abc-123' }, ... }` +- **WHEN** `IconService::extractUuid` is called +- **THEN** the returned UUID is `'abc-123'` + +#### Scenario: UUID lifted from @self.uuid when @self.id is missing + +- **GIVEN** an Application array of shape + `{ '@self': { uuid: 'def-456' }, ... }` (no `@self.id`) +- **WHEN** `extractUuid` is called +- **THEN** the returned UUID is `'def-456'` + +#### Scenario: UUID lifted from top-level uuid when @self is absent + +- **GIVEN** an Application array of shape + `{ uuid: 'ghi-789' }` (no `@self`) +- **WHEN** `extractUuid` is called +- **THEN** the returned UUID is `'ghi-789'` + +#### Scenario: Empty string candidates are skipped + +- **GIVEN** an Application array of shape + `{ '@self': { id: '' }, uuid: 'fallback-uuid' }` +- **WHEN** `extractUuid` is called +- **THEN** the empty `@self.id` is skipped and the returned UUID is + `'fallback-uuid'` + +#### Scenario: No usable UUID returns null + +- **GIVEN** an Application array of shape `{ name: 'x' }` (no UUID + anywhere) +- **WHEN** `extractUuid` is called +- **THEN** the helper returns `null` and the downstream + `fetchAttachedFileStream` SHALL return `null` without invoking + `FileService::getFile`, so the icon-serving endpoint cascades to + the next fallback (Decision 2 chain in design.md) diff --git a/openspec/changes/archive/retrofit-2026-05-24-app-icon-management-uuid/tasks.md b/openspec/changes/archive/retrofit-2026-05-24-app-icon-management-uuid/tasks.md new file mode 100644 index 0000000..56f7692 --- /dev/null +++ b/openspec/changes/archive/retrofit-2026-05-24-app-icon-management-uuid/tasks.md @@ -0,0 +1,3 @@ +# Tasks + +- [x] task-1: app-icon-management#REQ-OBICON-005 — Application UUID resolution for icon attachment lookup (retroactive annotation) diff --git a/openspec/specs/app-icon-management/spec.md b/openspec/specs/app-icon-management/spec.md index 7721d6b..99b61dc 100644 --- a/openspec/specs/app-icon-management/spec.md +++ b/openspec/specs/app-icon-management/spec.md @@ -1,3 +1,8 @@ +--- +retrofit_extensions: + - REQ-OBICON-005 +--- + # app-icon-management Specification ## Purpose @@ -156,3 +161,58 @@ goes through OR's existing files-attached-to-object endpoint (ADR-001). - **WHEN** a user attempts to upload a file with a non-`.svg` extension in either icon slot - **THEN** the uploader displays an inline error message and does not submit the file to OR + +### Requirement: Application UUID resolution for icon attachment lookup + +`IconService` SHALL derive the OR object UUID it uses for icon +attachment lookups (`FileService::getFile`) from a normalised +Application array. The derivation SHALL walk three fallback locations +in this order: `@self.id`, `@self.uuid`, then top-level `uuid`. Each +candidate SHALL be accepted only when it is a non-empty string; +anything else (missing key, null, empty string, non-string scalar) +SHALL be skipped without raising. When no candidate produces a usable +value the helper SHALL return `null`, and the calling +`fetchAttachedFileStream` SHALL surface that as a short-circuit +fallback (no OR call, downstream fallback chain runs) — not as an +exception. + +**ID:** REQ-OBICON-005 + +#### Scenario: UUID lifted from @self.id + +- **GIVEN** an Application array of shape + `{ '@self': { id: 'abc-123' }, ... }` +- **WHEN** `IconService::extractUuid` is called +- **THEN** the returned UUID is `'abc-123'` + +#### Scenario: UUID lifted from @self.uuid when @self.id is missing + +- **GIVEN** an Application array of shape + `{ '@self': { uuid: 'def-456' }, ... }` (no `@self.id`) +- **WHEN** `extractUuid` is called +- **THEN** the returned UUID is `'def-456'` + +#### Scenario: UUID lifted from top-level uuid when @self is absent + +- **GIVEN** an Application array of shape + `{ uuid: 'ghi-789' }` (no `@self`) +- **WHEN** `extractUuid` is called +- **THEN** the returned UUID is `'ghi-789'` + +#### Scenario: Empty string candidates are skipped + +- **GIVEN** an Application array of shape + `{ '@self': { id: '' }, uuid: 'fallback-uuid' }` +- **WHEN** `extractUuid` is called +- **THEN** the empty `@self.id` is skipped and the returned UUID is + `'fallback-uuid'` + +#### Scenario: No usable UUID returns null + +- **GIVEN** an Application array of shape `{ name: 'x' }` (no UUID + anywhere) +- **WHEN** `extractUuid` is called +- **THEN** the helper returns `null` and the downstream + `fetchAttachedFileStream` SHALL return `null` without invoking + `FileService::getFile`, so the icon-serving endpoint cascades to + the next fallback (Decision 2 chain in design.md)