Skip to content

gate-57: the groupfolder-storage-backend capability is inert end-to-end — Task 5 was ticked but never done #87

Description

@rubenvdlinde

What the gate found

The full-scope workflow_dispatch run reports:

[gate-57] orphaned-write-capability: FAIL — 1 orphaned write-capability method(s)
lib/Service/DashboardService.php:3189 method=writeDashboardContent rule=orphaned-write-capability class=DashboardService

This is a true positive, and the real scope is much larger than one method.

Measured scope: no producer and no consumer

symbol callers outside its own definition
DashboardService::writeDashboardContent() 0
DashboardService::readDashboardContent() 0
DashboardService::deleteDashboardContent() 0
DashboardContentStorageFactory::getStorage() 0 (only the three methods above)
Dashboard::setContent() only DbContentStorage::write() (unreachable, per above) and MigrateStorageToGroupFolder (which sets it to null)

Consequences that follow from that table:

  • Nothing in the application ever populates oc_launchpad_dashboards.content. Widgets live in WidgetPlacement rows. Dashboard::jsonSerialize() emits 'content' => $this->decodeContent(), which is therefore always null.
  • launchpad.content_storage is a no-op setting. An admin can switch it to groupfolder via the admin UI, launchpad:storage:toggle-backend, or the setup wizard, and nothing changes — content was never routed through the factory at either end.
  • launchpad:storage:migrate-to-groupfolder migrates nothing. It reads $dashboard->getContent() (always null), decodes that, and writes the result to the GroupFolder. With --prune-source it then nulls a column that was already null.

Root cause: a phantom-ticked task

openspec/changes/archive/2026-06-14-groupfolder-storage-backend/tasks.md line 9:

- [x] Task 5: Wire `DashboardContentStorageFactory` into `DashboardService` so
      `get/create/update/delete` route through the active backend; catch storage
      exceptions and rethrow with user-friendly messages

Task 5 is ticked. The three facade methods were written; the wiring named in the task was not. Tasks 1–4 and 6–12 did land, which is why the capability looks complete from every angle except the one that matters.

openspec/specs/groupfolder-storage-backend/spec.md carries status: done and REQ-GFSB-001 states that DashboardService::getDashboard() MUST delegate to getStorage()->read(). There is no getDashboard() method anywhere in lib/ — the spec's read scenario names a method that does not exist.

Why this is not being fixed unilaterally

Completing Task 5 requires answering a genuine architectural question that the spec and the code currently answer differently:

  • The spec treats the content blob as the source of truth for a dashboard's widgets/layout (REQ-GFSB-002: "content MUST be read from and written to the content field").
  • The code treats WidgetPlacement rows as the source of truth, and has done since long before this capability existed.

Wiring the blob in as a second source of truth invites divergence between the two. Wiring it in as a projection (write-through for human-readable admin inspection, reads still from placements) contradicts REQ-GFSB-001's read scenario. Picking between those is a product/architecture decision, not a mechanical gate fix, so it belongs in an ADR rather than in a quality-sweep PR.

Suggested resolution paths

  1. Complete the capability — decide the source-of-truth question in an ADR, add content to the create/update API contract, wire get/create/update/delete through the factory, and land the deferred Task 13 Playwright coverage once the dev stack ships groupfolders.
  2. Retire the capability — remove the storage layer, the factory, the admin setting, the two CLI commands, and the content column, and archive the spec as withdrawn.

Either way the archived tasks.md Task 5 tick and the spec's status: done should be corrected, because right now the project record asserts a wiring that does not exist.

Until this is decided, gate-57 stays red on development and is the only non-coverage gate finding left in this repo.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions