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,6 +1,8 @@
# Retrofit annotation commit (opsx-annotate, 2026-05-24)
770ffed41a22f3a45e00e98a8484ca72330eefc9

# Retrofit reverse-spec annotations (opsx-reverse-spec, 2026-05-24, deep-link-registration)
c72f99a
# 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)
Expand Down
3 changes: 3 additions & 0 deletions lib/Listener/DeepLinkRegistrationListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
* @version GIT: <git-id>
*
* @link https://conduction.nl
*
* @spec openspec/changes/retrofit-2026-05-24-deep-link-registration/tasks.md#task-1
* @spec openspec/changes/retrofit-2026-05-24-deep-link-registration/tasks.md#task-2
*/

declare(strict_types=1);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Design — Retrofit deep-link-registration

> Retrofit change. Tasks describe retroactive annotation, not new implementation
> work. The code already exists at HEAD.

## Context

`lib/Listener/DeepLinkRegistrationListener.php` (and its companion
wiring in `lib/AppInfo/Application.php::register`) implement
OpenBuilt's hook into OpenRegister's unified-search deep-link surface.
The 2026-05-24 coverage scan dropped these into Bucket 2b
(`deep-link-registration`) because no existing openbuilt capability
spec names this listener or the event it consumes.

## Decisions

- **New capability, not extend.** The closest existing spec is
`openbuilt-runtime`, but that's a runtime/serving capability — the
listener is a search-integration capability, not a runtime mount.
ADR-019 is the closest org-wide concept, but it does not specify
per-app listener wiring. So this is a brand-new app-local capability.
- **Two REQs, not one.** Wiring (REQ-OBDL-001) vs payload
(REQ-OBDL-002). Splitting them lets future PRs add more
`$event->register(...)` calls under REQ-OBDL-002 without touching
REQ-OBDL-001's wiring contract.
- **Capture the stub honestly.** The current handler body registers a
single placeholder entry for `schemaSlug: 'example'` — clearly
template-stub leftover (matches the comment "Update the register
slug, schema slug, and URL template to match your app's actual
schemas"). REQ-OBDL-002 stays minimal ("at least one entry, with
`{uuid}` placeholder") and the Note block names the gap as TODO
rather than silently spec'ing the placeholder as production
behaviour.
- **No hard OR dependency.** The wiring is "if OR is installed and
dispatches the event, OpenBuilt responds". REQ-OBDL-001's scenario
pins this as an observable invariant — the app must not crash when
OR is absent.

## Out of scope

- Replacing the placeholder `example` schema with real OpenBuilt
schemas (`application`, `application-version`, `built-app-route`) —
separate PR, will extend REQ-OBDL-002 with additional scenarios.
- ADR-019 integration-registry surface — orthogonal.
- Unified-search result rendering / icon — OR concern.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Retrofit — deep-link-registration (new capability)

Describes the observed behaviour of
`DeepLinkRegistrationListener::handle` as 2 REQs in a brand-new
capability spec. Code already exists — this change retroactively
specifies it.

## Affected code units

- lib/AppInfo/Application.php::register (event-listener wiring only)
- lib/Listener/DeepLinkRegistrationListener.php::handle

## Approach

- New capability (`--cluster`) rather than `--extend`: the existing
openbuilt specs (runtime, RBAC, exporter, versioning, etc.) all
describe authoring or runtime serving of virtual apps. None covers
the listener that hooks OpenRegister's unified-search deep-link
registration event. ADR-019 (integration registry) is the closest
org-wide concept, but it does not specify per-app event-listener
wiring as a capability.
- Two REQs: one for the event-listener wiring (REQ-OBDL-001) and one
for the registration payload that the handler emits (REQ-OBDL-002).
Splitting the wiring from the payload lets future schema additions
(more `$event->register(...)` calls) extend REQ-OBDL-002 without
re-litigating the wiring posture.
- The current `handle` body registers a single deep link for a
placeholder schema slug `example` (template-stub left over from
scaffold) — Notes block flags this as observed-but-incomplete TODO,
not silently spec'd as production behaviour.

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,100 @@
---
retrofit: true
---

# deep-link-registration Specification

## Purpose

OpenBuilt opts in to Nextcloud's unified-search deep-link integration
by listening for OpenRegister's `DeepLinkRegistrationEvent` and
registering per-schema URL templates. When OR resolves a search hit on
a registered schema, the unified-search result row links straight to
the matching OpenBuilt detail view — no double-click through OR.

This capability is event-driven: the contract is "if OR is installed
and dispatches the event, OpenBuilt provides its deep-link table". If
OR is absent the event never fires and OpenBuilt silently no-ops, so
the integration is optional and adds zero hard dependency.

## Requirements

### Requirement: Event listener wired at app registration

The app SHALL register a listener for
`OCA\OpenRegister\Event\DeepLinkRegistrationEvent` during
`Application::register` via
`IRegistrationContext::registerEventListener`. The listener class
SHALL be `OCA\OpenBuilt\Listener\DeepLinkRegistrationListener` and
SHALL implement `OCP\EventDispatcher\IEventListener<Event>`. The
listener SHALL be idempotent at the Nextcloud DI level — re-running
`register()` SHALL NOT result in duplicate registrations. The wiring
SHALL NOT introduce a hard dependency on OpenRegister: if OR is not
installed, no event fires and the listener is never invoked.

**ID:** REQ-OBDL-001

#### Scenario: Listener is registered exactly once

- **WHEN** Nextcloud bootstraps the OpenBuilt app and calls
`Application::register`
- **THEN** `IRegistrationContext::registerEventListener` is invoked
with `DeepLinkRegistrationEvent::class` and
`DeepLinkRegistrationListener::class`

#### Scenario: OpenRegister absent → no-op

- **WHEN** OpenRegister is not installed and OpenBuilt boots
- **THEN** the wiring registers without raising
- **AND** no deep-link entries are emitted because the event never
fires

#### Scenario: Handler short-circuits on non-matching events

- **WHEN** the listener is invoked with an event that is not an
instance of `DeepLinkRegistrationEvent`
- **THEN** `handle()` returns immediately without calling
`$event->register(...)` and without raising

### Requirement: Per-schema deep-link entries registered against the event

When `handle()` receives a `DeepLinkRegistrationEvent`, it SHALL
register one or more deep-link entries by calling
`$event->register(appId, registerSlug, schemaSlug, urlTemplate)`.
Each entry SHALL declare `appId: 'openbuilt'` as the host Nextcloud
app id. The `urlTemplate` SHALL be a relative path of shape
`/apps/openbuilt/#/{schemaPath}/{uuid}` with `{uuid}` as the canonical
placeholder OR substitutes with the matching object's UUID. The
registration SHALL be additive — calling `$event->register(...)` once
per schema; the listener SHALL NOT mutate or remove previously
registered entries on the event.

**ID:** REQ-OBDL-002

#### Scenario: At least one deep-link entry is registered

- **WHEN** OR dispatches `DeepLinkRegistrationEvent` and the listener
handles it
- **THEN** `$event->register(...)` is called at least once with
`appId: 'openbuilt'`

#### Scenario: URL template carries the {uuid} placeholder

- **WHEN** the listener registers a deep-link entry
- **THEN** the `urlTemplate` string contains the literal placeholder
`{uuid}` that OR substitutes per result row

#### Note

The handler currently registers only a single placeholder entry for
schema slug `example` (left over from the
`nextcloud-app-template` scaffold — see `lib/Resources/template/lib/
Listener/DeepLinkRegistrationListener.php`). This is observed-but-
incomplete: the real OpenBuilt schemas (`application`,
`application-version`, `built-app-route`, etc.) have no deep-link
entries yet, so unified-search hits on those rows still land on OR's
generic detail page. TODO (filed separately): replace the placeholder
with one `$event->register(...)` call per OpenBuilt-owned schema. This
retrofit spec captures the wiring contract; the catalogue is
intentionally left to a follow-up so future PRs can extend
REQ-OBDL-002 without re-litigating REQ-OBDL-001.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Tasks

- [x] task-1: deep-link-registration#REQ-OBDL-001 — Event listener wired at app registration (retroactive annotation)
- [x] task-2: deep-link-registration#REQ-OBDL-002 — Per-schema deep-link entries registered against the event (retroactive annotation)
100 changes: 100 additions & 0 deletions openspec/specs/deep-link-registration/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
retrofit: true
---

# deep-link-registration Specification

## Purpose

OpenBuilt opts in to Nextcloud's unified-search deep-link integration
by listening for OpenRegister's `DeepLinkRegistrationEvent` and
registering per-schema URL templates. When OR resolves a search hit on
a registered schema, the unified-search result row links straight to
the matching OpenBuilt detail view — no double-click through OR.

This capability is event-driven: the contract is "if OR is installed
and dispatches the event, OpenBuilt provides its deep-link table". If
OR is absent the event never fires and OpenBuilt silently no-ops, so
the integration is optional and adds zero hard dependency.

## Requirements

### Requirement: Event listener wired at app registration

The app SHALL register a listener for
`OCA\OpenRegister\Event\DeepLinkRegistrationEvent` during
`Application::register` via
`IRegistrationContext::registerEventListener`. The listener class
SHALL be `OCA\OpenBuilt\Listener\DeepLinkRegistrationListener` and
SHALL implement `OCP\EventDispatcher\IEventListener<Event>`. The
listener SHALL be idempotent at the Nextcloud DI level — re-running
`register()` SHALL NOT result in duplicate registrations. The wiring
SHALL NOT introduce a hard dependency on OpenRegister: if OR is not
installed, no event fires and the listener is never invoked.

**ID:** REQ-OBDL-001

#### Scenario: Listener is registered exactly once

- **WHEN** Nextcloud bootstraps the OpenBuilt app and calls
`Application::register`
- **THEN** `IRegistrationContext::registerEventListener` is invoked
with `DeepLinkRegistrationEvent::class` and
`DeepLinkRegistrationListener::class`

#### Scenario: OpenRegister absent → no-op

- **WHEN** OpenRegister is not installed and OpenBuilt boots
- **THEN** the wiring registers without raising
- **AND** no deep-link entries are emitted because the event never
fires

#### Scenario: Handler short-circuits on non-matching events

- **WHEN** the listener is invoked with an event that is not an
instance of `DeepLinkRegistrationEvent`
- **THEN** `handle()` returns immediately without calling
`$event->register(...)` and without raising

### Requirement: Per-schema deep-link entries registered against the event

When `handle()` receives a `DeepLinkRegistrationEvent`, it SHALL
register one or more deep-link entries by calling
`$event->register(appId, registerSlug, schemaSlug, urlTemplate)`.
Each entry SHALL declare `appId: 'openbuilt'` as the host Nextcloud
app id. The `urlTemplate` SHALL be a relative path of shape
`/apps/openbuilt/#/{schemaPath}/{uuid}` with `{uuid}` as the canonical
placeholder OR substitutes with the matching object's UUID. The
registration SHALL be additive — calling `$event->register(...)` once
per schema; the listener SHALL NOT mutate or remove previously
registered entries on the event.

**ID:** REQ-OBDL-002

#### Scenario: At least one deep-link entry is registered

- **WHEN** OR dispatches `DeepLinkRegistrationEvent` and the listener
handles it
- **THEN** `$event->register(...)` is called at least once with
`appId: 'openbuilt'`

#### Scenario: URL template carries the {uuid} placeholder

- **WHEN** the listener registers a deep-link entry
- **THEN** the `urlTemplate` string contains the literal placeholder
`{uuid}` that OR substitutes per result row

#### Note

The handler currently registers only a single placeholder entry for
schema slug `example` (left over from the
`nextcloud-app-template` scaffold — see `lib/Resources/template/lib/
Listener/DeepLinkRegistrationListener.php`). This is observed-but-
incomplete: the real OpenBuilt schemas (`application`,
`application-version`, `built-app-route`, etc.) have no deep-link
entries yet, so unified-search hits on those rows still land on OR's
generic detail page. TODO (filed separately): replace the placeholder
with one `$event->register(...)` call per OpenBuilt-owned schema. This
retrofit spec captures the wiring contract; the catalogue is
intentionally left to a follow-up so future PRs can extend
REQ-OBDL-002 without re-litigating REQ-OBDL-001.
Loading