Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions lib/Service/IconService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Tasks

- [x] task-1: app-icon-management#REQ-OBICON-005 — Application UUID resolution for icon attachment lookup (retroactive annotation)
60 changes: 60 additions & 0 deletions openspec/specs/app-icon-management/spec.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
retrofit_extensions:
- REQ-OBICON-005
---

# app-icon-management Specification

## Purpose
Expand Down Expand Up @@ -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)
Loading