-
1.1 Declare
Applicationschema inlib/Settings/openbuilt_register.json- spec_ref: REQ-OBA-001, REQ-OBA-002
- files:
lib/Settings/openbuilt_register.json - acceptance_criteria: Schema declares
uuid,slug(kebab-case pattern),name(required),description,manifest(object, required, with a$refor inline reference to the canonical app-manifest schema),version(semver pattern, required),status(enum draft|published|archived, default draft, required). Validates against OpenAPI 3.0.0. - Implement: declarative — no PHP service class.
- Test: integration test creates an Application via OR REST, asserts schema validation kicks in on a malformed manifest.
-
1.2 Add
x-openregister-lifecycleto theApplicationschema (canonical ADR-031 example)- spec_ref: REQ-OBA-003
- files:
lib/Settings/openbuilt_register.json(NOT a new PHP service) - acceptance_criteria: Declares states
draft,published,archivedand transitionsdraft → published,published → archived,archived → draft. Each transition emits an OR audit event. NoApplicationLifecycleService.phpfile is created. - Implement: declarative schema patch only.
- Test: integration test transitions a seeded Application through every allowed state, asserts audit-trail entries exist, asserts a disallowed transition (
draft → archived) returns 4xx.
-
1.3 Declare
BuiltAppRouteschema and slug uniqueness- spec_ref: REQ-OBA-004
- files:
lib/Settings/openbuilt_register.json - acceptance_criteria: Schema declares
slug(kebab-case, required) andapplicationUuid(UUID-format, required); slug uniqueness scoped to organisation (declarative if the engine supports it; otherwise documented in design.md OQ-1 as a thin-glue fallback). - Implement: declarative schema patch (and, only if necessary per design.md OQ-1, a single
BuiltAppRouteSyncListener.phpsubscribed to OR's lifecycle event). - Test: integration test publishes two Applications with the same slug in the same organisation, asserts the second is rejected.
-
1.4 Wire BuiltAppRoute upkeep to the Application lifecycle
- spec_ref: REQ-OBA-004
- files:
lib/Settings/openbuilt_register.json(preferred); only if OR's engine is missing the hook,lib/Listener/BuiltAppRouteSyncListener.php - acceptance_criteria: Transitioning an Application to
publishedcreates / refreshes its BuiltAppRoute; transitioning toarchivedremoves (or marks inactive) the BuiltAppRoute. Behaviour is identical whether the action is declarative (x-openregister-lifecycle.on_published) or listener-based. - Implement: prefer the declarative path; record the chosen path in
hydra.jsonunderdecisions[]for self-learning. - Test: integration test asserts the BuiltAppRoute row appears on publish and disappears on archive.
-
1.5 Confirm multi-tenant scoping via OR
organisation- spec_ref: REQ-OBA-005
- files:
lib/Settings/openbuilt_register.json(no changes if OR defaults already apply) - acceptance_criteria: Cross-organisation reads return empty / 403 per OR's standard contract. No app-local RBAC code introduced (ADR-022).
- Implement: rely on OR's existing organisation scoping; no PHP added.
- Test: integration test runs as user-A in org-A, asserts org-B Applications are not returned.
-
2.1 Register the manifest endpoint route in
appinfo/routes.php(ADR-016)- spec_ref: REQ-OBR-001
- files:
appinfo/routes.php - acceptance_criteria: Route
GET /api/applications/{slug}/manifestmaps toapplications#getManifestwith#[NoAdminRequired]. Only registration path isroutes.php— no attribute-only registration. - Implement: ~5 LOC route declaration.
- Test: Newman + Playwright network-request capture verifies the route resolves.
-
2.2 Add
ApplicationsController::getManifest(thin-glue code per ADR-032)- spec_ref: REQ-OBR-001
- files:
lib/Controller/ApplicationsController.php - acceptance_criteria:
getManifest(string $slug): JSONResponseresolves slug → Application via OR's ObjectService and theBuiltAppRouteindex, returns themanifestblob unwrapped (no OR envelope), 200 on hit, 404 on miss. ~15 LOC; carries SPDX + EUPL-1.2 docblock (per memory rule).#[NoAdminRequired]attribute is set so route-auth gate-5 passes. - Implement: single method, no service class.
- Test: PHPUnit asserts 404 on unknown slug + 200+payload on known slug.
-
2.3 Build
BuilderHost.vuemounting a nestedCnAppRoot- spec_ref: REQ-OBR-002, REQ-OBR-003
- files:
src/views/BuilderHost.vue,src/router/index.js(route registration),src/manifests/placeholder.json - acceptance_criteria: Vue route
/builder/:slug(.*)mountsBuilderHost.vue; the host renders<CnAppRoot :app-id="\openbuilt-${slug}`" :bundled-manifest="placeholder" :key="slug" :options="{ fetcher: redirectingFetcher }" />. Inner-router path forwarding is verified by inspecting$route.params.pathMatch`. - Implement: ~25 LOC across the SFC
<script>+<template>. - Test: Playwright navigates
/builder/hello-worldand asserts the seeded index page renders; then navigates to/builder/hello-world/messages/<uuid>and asserts the detail page renders.
-
2.4 Build the textarea-based
ApplicationEditor.vue- spec_ref: REQ-OBR-005
- files:
src/views/ApplicationEditor.vue,src/store/applications.js - acceptance_criteria: Editor lists Applications via OR REST, opens a JSON textarea bound to the
manifestblob, validates on Save viavalidateManifestfrom@conduction/nextcloud-vue, surfaces the failing JSON path on validation error, PUTs the blob back to OR REST on success. - Implement: Options API; no custom Pinia store layered over
useObjectStore(memory rule: usecreateObjectStore). - Test: Playwright pastes a malformed manifest (missing
pages), asserts the save button stays disabled / surfaces an error; then pastes a valid manifest and asserts the PUT goes through.
-
3.1 Declare
hello-messageschema inlib/Settings/openbuilt_register.json- spec_ref: REQ-OBR-004
- files:
lib/Settings/openbuilt_register.json - acceptance_criteria: Schema declares
uuid(UUID-format) plustitle(required, string) andbody(string). - Implement: declarative schema patch.
-
3.2 Ship the seed repair step
lib/Repair/SeedHelloWorld.php- spec_ref: REQ-OBR-004
- files:
lib/Repair/SeedHelloWorld.php,appinfo/info.xml(<repair-steps>already declaresInitializeSettings; addSeedHelloWorldas a<post-migration>step) - acceptance_criteria: Seeds one
Applicationwithslug: hello-world,status: published, the manifest declared in design.md "Seed Data", plus threehello-messagesample objects. Idempotent: guarded by an existing-slug check; re-running the repair step on a seeded install is a no-op. The seeded manifest validates against the canonical schema; the repair step callsConfigurationService::importFromApp()(memory rule) for schema registration. - Implement: PHP repair step; no scripting / sed / awk to modify code.
- Test: PHPUnit runs the repair step twice, asserts exactly one
hello-worldApplication + three sample messages exist after each run.
- 4.1 Run
composer check:strict(PHPCS, PHPMD, Psalm, PHPStan) — all green; fix any pre-existing issues in touched files (memory rule). - 4.2 Run
npm run lint/ ESLint flat config — clean on the new SFC. - 4.3 Run
npm run check:manifest(ADR-024) on the seededhello-worldmanifest blob in tests — passes against the canonical schema pinned inpackage.json. - 4.4 Visually verify on a fresh
docker compose upthat/index.php/apps/openbuilt/builder/hello-worldrenders the seeded virtual app. - 4.5 Confirm no
ApplicationLifecycleService.php/ApplicationStateMachine.php/ similar service class exists underlib/Service/— ADR-031 review gate.
- 5.1 PHPUnit —
tests/Unit/Controller/ApplicationsControllerTest.phpcoversgetManifest(404 + 200 + organisation scoping). - 5.2 PHPUnit —
tests/Integration/ApplicationLifecycleTest.phpwalks the Application throughdraft → published → archived → draft, asserts audit entries on each transition, asserts a disallowed transition is rejected, asserts BuiltAppRoute upkeep. - 5.3 Newman —
tests/api/openbuilt.postman_collection.jsoncoversGET /api/applications/{slug}/manifest(200, 404) plus standard OR-REST CRUD on Applications. - 5.4 Playwright —
tests/e2e/builder-host.spec.tsopens the OpenBuilt shell, navigates to/builder/hello-world, asserts the seeded index page renders three messages, opens a detail page, opens the form page, and round-trips a manifest edit through the textarea editor.
- 6.1 Add
docs/openbuilt-runtime.mddescribing the nested-CnAppRootmount pattern, the manifest endpoint contract, and the workaround per design.md Decision 4. - 6.2 Add a "How to author a virtual app" walkthrough in
docs/integrator-guide.mdcovering the textarea editor + the seededhello-worldexample. - 6.3 NL Design (ADR-010) — confirm the new views use Nextcloud CSS variables only (no hardcoded colours); document any new variables added.
- 6.4 Update
openspec/app-config.jsonto listopenbuilt-application-registerandopenbuilt-runtimeunder capabilities.
- 7.1 Add English translations for every new string in
l10n/en.json(top-bar entry already declared ininfo.xml; addopenbuilt.builder.*,openbuilt.editor.*,openbuilt.helloworld.*keys). - 7.2 Add Dutch translations for the same keys in
l10n/nl.json. - 7.3 Confirm the seeded
hello-worldmanifest uses translation keys for everylabelandtitle(per ADR-024 §6).