Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Commit b5c0fcb

Browse files
authored
retrofit: reverse-spec deep-link-registration (2 REQs / 1 method, new capability) (#142)
* retrofit: draft deep-link-registration spec + annotate DeepLinkRegistrationListener Drafts 2 REQs (REQ-OBDL-001..002) in a new deep-link-registration capability spec covering the DeepLinkRegistrationListener wiring + handle() method (Bucket 2b from the 2026-05-24 coverage scan). Adds 2 @SPEC tags to the listener's file docblock. REQ-OBDL-001: Event listener wired at app registration - codifies the IRegistrationContext::registerEventListener wiring and the short-circuit on non-matching events. Includes the no-hard-dependency invariant: if OR isn't installed, the listener never fires. REQ-OBDL-002: Per-schema deep-link entries registered against the event - codifies the additive $event->register(appId, registerSlug, schemaSlug, urlTemplate) shape and the {uuid} placeholder. Notes block flags the placeholder schema slug 'example' (scaffold leftover) as TODO - real OpenBuilt schemas (application, application-version, built-app-route) have no deep-link entries yet. Future PRs will extend REQ-OBDL-002 with one register() call per OpenBuilt-owned schema. Refs #137 * retrofit: archive deep-link-registration change Creates openspec/specs/deep-link-registration/spec.md (new capability) from the change's spec delta and archives the change. No code logic changes. Refs #137 * chore: record reverse-spec annotation SHA in .git-blame-ignore-revs Refs #137
1 parent 2a60018 commit b5c0fcb

7 files changed

Lines changed: 287 additions & 0 deletions

File tree

.git-blame-ignore-revs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Retrofit annotation commit (opsx-annotate, 2026-05-24)
22
770ffed41a22f3a45e00e98a8484ca72330eefc9
33

4+
# Retrofit reverse-spec annotations (opsx-reverse-spec, 2026-05-24, deep-link-registration)
5+
c72f99a
46
# Retrofit reverse-spec annotations (opsx-reverse-spec, 2026-05-24, app-icon-management UUID)
57
01a2862
68
# Retrofit reverse-spec annotations (opsx-reverse-spec, 2026-05-24, openbuilt-runtime MCP)

lib/Listener/DeepLinkRegistrationListener.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
* @version GIT: <git-id>
1616
*
1717
* @link https://conduction.nl
18+
*
19+
* @spec openspec/changes/retrofit-2026-05-24-deep-link-registration/tasks.md#task-1
20+
* @spec openspec/changes/retrofit-2026-05-24-deep-link-registration/tasks.md#task-2
1821
*/
1922

2023
declare(strict_types=1);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Design — Retrofit deep-link-registration
2+
3+
> Retrofit change. Tasks describe retroactive annotation, not new implementation
4+
> work. The code already exists at HEAD.
5+
6+
## Context
7+
8+
`lib/Listener/DeepLinkRegistrationListener.php` (and its companion
9+
wiring in `lib/AppInfo/Application.php::register`) implement
10+
OpenBuilt's hook into OpenRegister's unified-search deep-link surface.
11+
The 2026-05-24 coverage scan dropped these into Bucket 2b
12+
(`deep-link-registration`) because no existing openbuilt capability
13+
spec names this listener or the event it consumes.
14+
15+
## Decisions
16+
17+
- **New capability, not extend.** The closest existing spec is
18+
`openbuilt-runtime`, but that's a runtime/serving capability — the
19+
listener is a search-integration capability, not a runtime mount.
20+
ADR-019 is the closest org-wide concept, but it does not specify
21+
per-app listener wiring. So this is a brand-new app-local capability.
22+
- **Two REQs, not one.** Wiring (REQ-OBDL-001) vs payload
23+
(REQ-OBDL-002). Splitting them lets future PRs add more
24+
`$event->register(...)` calls under REQ-OBDL-002 without touching
25+
REQ-OBDL-001's wiring contract.
26+
- **Capture the stub honestly.** The current handler body registers a
27+
single placeholder entry for `schemaSlug: 'example'` — clearly
28+
template-stub leftover (matches the comment "Update the register
29+
slug, schema slug, and URL template to match your app's actual
30+
schemas"). REQ-OBDL-002 stays minimal ("at least one entry, with
31+
`{uuid}` placeholder") and the Note block names the gap as TODO
32+
rather than silently spec'ing the placeholder as production
33+
behaviour.
34+
- **No hard OR dependency.** The wiring is "if OR is installed and
35+
dispatches the event, OpenBuilt responds". REQ-OBDL-001's scenario
36+
pins this as an observable invariant — the app must not crash when
37+
OR is absent.
38+
39+
## Out of scope
40+
41+
- Replacing the placeholder `example` schema with real OpenBuilt
42+
schemas (`application`, `application-version`, `built-app-route`) —
43+
separate PR, will extend REQ-OBDL-002 with additional scenarios.
44+
- ADR-019 integration-registry surface — orthogonal.
45+
- Unified-search result rendering / icon — OR concern.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Retrofit — deep-link-registration (new capability)
2+
3+
Describes the observed behaviour of
4+
`DeepLinkRegistrationListener::handle` as 2 REQs in a brand-new
5+
capability spec. Code already exists — this change retroactively
6+
specifies it.
7+
8+
## Affected code units
9+
10+
- lib/AppInfo/Application.php::register (event-listener wiring only)
11+
- lib/Listener/DeepLinkRegistrationListener.php::handle
12+
13+
## Approach
14+
15+
- New capability (`--cluster`) rather than `--extend`: the existing
16+
openbuilt specs (runtime, RBAC, exporter, versioning, etc.) all
17+
describe authoring or runtime serving of virtual apps. None covers
18+
the listener that hooks OpenRegister's unified-search deep-link
19+
registration event. ADR-019 (integration registry) is the closest
20+
org-wide concept, but it does not specify per-app event-listener
21+
wiring as a capability.
22+
- Two REQs: one for the event-listener wiring (REQ-OBDL-001) and one
23+
for the registration payload that the handler emits (REQ-OBDL-002).
24+
Splitting the wiring from the payload lets future schema additions
25+
(more `$event->register(...)` calls) extend REQ-OBDL-002 without
26+
re-litigating the wiring posture.
27+
- The current `handle` body registers a single deep link for a
28+
placeholder schema slug `example` (template-stub left over from
29+
scaffold) — Notes block flags this as observed-but-incomplete TODO,
30+
not silently spec'd as production behaviour.
31+
32+
Source: `openspec/coverage-report.md` generated 2026-05-24. See
33+
[retrofit playbook](../../../../hydra/.github/docs/claude/retrofit.md).
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
retrofit: true
3+
---
4+
5+
# deep-link-registration Specification
6+
7+
## Purpose
8+
9+
OpenBuilt opts in to Nextcloud's unified-search deep-link integration
10+
by listening for OpenRegister's `DeepLinkRegistrationEvent` and
11+
registering per-schema URL templates. When OR resolves a search hit on
12+
a registered schema, the unified-search result row links straight to
13+
the matching OpenBuilt detail view — no double-click through OR.
14+
15+
This capability is event-driven: the contract is "if OR is installed
16+
and dispatches the event, OpenBuilt provides its deep-link table". If
17+
OR is absent the event never fires and OpenBuilt silently no-ops, so
18+
the integration is optional and adds zero hard dependency.
19+
20+
## Requirements
21+
22+
### Requirement: Event listener wired at app registration
23+
24+
The app SHALL register a listener for
25+
`OCA\OpenRegister\Event\DeepLinkRegistrationEvent` during
26+
`Application::register` via
27+
`IRegistrationContext::registerEventListener`. The listener class
28+
SHALL be `OCA\OpenBuilt\Listener\DeepLinkRegistrationListener` and
29+
SHALL implement `OCP\EventDispatcher\IEventListener<Event>`. The
30+
listener SHALL be idempotent at the Nextcloud DI level — re-running
31+
`register()` SHALL NOT result in duplicate registrations. The wiring
32+
SHALL NOT introduce a hard dependency on OpenRegister: if OR is not
33+
installed, no event fires and the listener is never invoked.
34+
35+
**ID:** REQ-OBDL-001
36+
37+
#### Scenario: Listener is registered exactly once
38+
39+
- **WHEN** Nextcloud bootstraps the OpenBuilt app and calls
40+
`Application::register`
41+
- **THEN** `IRegistrationContext::registerEventListener` is invoked
42+
with `DeepLinkRegistrationEvent::class` and
43+
`DeepLinkRegistrationListener::class`
44+
45+
#### Scenario: OpenRegister absent → no-op
46+
47+
- **WHEN** OpenRegister is not installed and OpenBuilt boots
48+
- **THEN** the wiring registers without raising
49+
- **AND** no deep-link entries are emitted because the event never
50+
fires
51+
52+
#### Scenario: Handler short-circuits on non-matching events
53+
54+
- **WHEN** the listener is invoked with an event that is not an
55+
instance of `DeepLinkRegistrationEvent`
56+
- **THEN** `handle()` returns immediately without calling
57+
`$event->register(...)` and without raising
58+
59+
### Requirement: Per-schema deep-link entries registered against the event
60+
61+
When `handle()` receives a `DeepLinkRegistrationEvent`, it SHALL
62+
register one or more deep-link entries by calling
63+
`$event->register(appId, registerSlug, schemaSlug, urlTemplate)`.
64+
Each entry SHALL declare `appId: 'openbuilt'` as the host Nextcloud
65+
app id. The `urlTemplate` SHALL be a relative path of shape
66+
`/apps/openbuilt/#/{schemaPath}/{uuid}` with `{uuid}` as the canonical
67+
placeholder OR substitutes with the matching object's UUID. The
68+
registration SHALL be additive — calling `$event->register(...)` once
69+
per schema; the listener SHALL NOT mutate or remove previously
70+
registered entries on the event.
71+
72+
**ID:** REQ-OBDL-002
73+
74+
#### Scenario: At least one deep-link entry is registered
75+
76+
- **WHEN** OR dispatches `DeepLinkRegistrationEvent` and the listener
77+
handles it
78+
- **THEN** `$event->register(...)` is called at least once with
79+
`appId: 'openbuilt'`
80+
81+
#### Scenario: URL template carries the {uuid} placeholder
82+
83+
- **WHEN** the listener registers a deep-link entry
84+
- **THEN** the `urlTemplate` string contains the literal placeholder
85+
`{uuid}` that OR substitutes per result row
86+
87+
#### Note
88+
89+
The handler currently registers only a single placeholder entry for
90+
schema slug `example` (left over from the
91+
`nextcloud-app-template` scaffold — see `lib/Resources/template/lib/
92+
Listener/DeepLinkRegistrationListener.php`). This is observed-but-
93+
incomplete: the real OpenBuilt schemas (`application`,
94+
`application-version`, `built-app-route`, etc.) have no deep-link
95+
entries yet, so unified-search hits on those rows still land on OR's
96+
generic detail page. TODO (filed separately): replace the placeholder
97+
with one `$event->register(...)` call per OpenBuilt-owned schema. This
98+
retrofit spec captures the wiring contract; the catalogue is
99+
intentionally left to a follow-up so future PRs can extend
100+
REQ-OBDL-002 without re-litigating REQ-OBDL-001.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Tasks
2+
3+
- [x] task-1: deep-link-registration#REQ-OBDL-001 — Event listener wired at app registration (retroactive annotation)
4+
- [x] task-2: deep-link-registration#REQ-OBDL-002 — Per-schema deep-link entries registered against the event (retroactive annotation)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
retrofit: true
3+
---
4+
5+
# deep-link-registration Specification
6+
7+
## Purpose
8+
9+
OpenBuilt opts in to Nextcloud's unified-search deep-link integration
10+
by listening for OpenRegister's `DeepLinkRegistrationEvent` and
11+
registering per-schema URL templates. When OR resolves a search hit on
12+
a registered schema, the unified-search result row links straight to
13+
the matching OpenBuilt detail view — no double-click through OR.
14+
15+
This capability is event-driven: the contract is "if OR is installed
16+
and dispatches the event, OpenBuilt provides its deep-link table". If
17+
OR is absent the event never fires and OpenBuilt silently no-ops, so
18+
the integration is optional and adds zero hard dependency.
19+
20+
## Requirements
21+
22+
### Requirement: Event listener wired at app registration
23+
24+
The app SHALL register a listener for
25+
`OCA\OpenRegister\Event\DeepLinkRegistrationEvent` during
26+
`Application::register` via
27+
`IRegistrationContext::registerEventListener`. The listener class
28+
SHALL be `OCA\OpenBuilt\Listener\DeepLinkRegistrationListener` and
29+
SHALL implement `OCP\EventDispatcher\IEventListener<Event>`. The
30+
listener SHALL be idempotent at the Nextcloud DI level — re-running
31+
`register()` SHALL NOT result in duplicate registrations. The wiring
32+
SHALL NOT introduce a hard dependency on OpenRegister: if OR is not
33+
installed, no event fires and the listener is never invoked.
34+
35+
**ID:** REQ-OBDL-001
36+
37+
#### Scenario: Listener is registered exactly once
38+
39+
- **WHEN** Nextcloud bootstraps the OpenBuilt app and calls
40+
`Application::register`
41+
- **THEN** `IRegistrationContext::registerEventListener` is invoked
42+
with `DeepLinkRegistrationEvent::class` and
43+
`DeepLinkRegistrationListener::class`
44+
45+
#### Scenario: OpenRegister absent → no-op
46+
47+
- **WHEN** OpenRegister is not installed and OpenBuilt boots
48+
- **THEN** the wiring registers without raising
49+
- **AND** no deep-link entries are emitted because the event never
50+
fires
51+
52+
#### Scenario: Handler short-circuits on non-matching events
53+
54+
- **WHEN** the listener is invoked with an event that is not an
55+
instance of `DeepLinkRegistrationEvent`
56+
- **THEN** `handle()` returns immediately without calling
57+
`$event->register(...)` and without raising
58+
59+
### Requirement: Per-schema deep-link entries registered against the event
60+
61+
When `handle()` receives a `DeepLinkRegistrationEvent`, it SHALL
62+
register one or more deep-link entries by calling
63+
`$event->register(appId, registerSlug, schemaSlug, urlTemplate)`.
64+
Each entry SHALL declare `appId: 'openbuilt'` as the host Nextcloud
65+
app id. The `urlTemplate` SHALL be a relative path of shape
66+
`/apps/openbuilt/#/{schemaPath}/{uuid}` with `{uuid}` as the canonical
67+
placeholder OR substitutes with the matching object's UUID. The
68+
registration SHALL be additive — calling `$event->register(...)` once
69+
per schema; the listener SHALL NOT mutate or remove previously
70+
registered entries on the event.
71+
72+
**ID:** REQ-OBDL-002
73+
74+
#### Scenario: At least one deep-link entry is registered
75+
76+
- **WHEN** OR dispatches `DeepLinkRegistrationEvent` and the listener
77+
handles it
78+
- **THEN** `$event->register(...)` is called at least once with
79+
`appId: 'openbuilt'`
80+
81+
#### Scenario: URL template carries the {uuid} placeholder
82+
83+
- **WHEN** the listener registers a deep-link entry
84+
- **THEN** the `urlTemplate` string contains the literal placeholder
85+
`{uuid}` that OR substitutes per result row
86+
87+
#### Note
88+
89+
The handler currently registers only a single placeholder entry for
90+
schema slug `example` (left over from the
91+
`nextcloud-app-template` scaffold — see `lib/Resources/template/lib/
92+
Listener/DeepLinkRegistrationListener.php`). This is observed-but-
93+
incomplete: the real OpenBuilt schemas (`application`,
94+
`application-version`, `built-app-route`, etc.) have no deep-link
95+
entries yet, so unified-search hits on those rows still land on OR's
96+
generic detail page. TODO (filed separately): replace the placeholder
97+
with one `$event->register(...)` call per OpenBuilt-owned schema. This
98+
retrofit spec captures the wiring contract; the catalogue is
99+
intentionally left to a follow-up so future PRs can extend
100+
REQ-OBDL-002 without re-litigating REQ-OBDL-001.

0 commit comments

Comments
 (0)