From 94e826bf71669a96a1ee1a16877f2a2bd35f1af9 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:33:17 +0200 Subject: [PATCH 01/15] docs(openspec): connections-catalogue-pages, connection index, detail and application-page lists --- .../.openspec.yaml | 2 + .../connections-catalogue-pages/design.md | 59 +++++++++++++++ .../connections-catalogue-pages/proposal.md | 52 +++++++++++++ .../specs/catalogue-connection-pages/spec.md | 74 +++++++++++++++++++ .../connections-catalogue-pages/tasks.md | 47 ++++++++++++ 5 files changed, 234 insertions(+) create mode 100644 openspec/changes/connections-catalogue-pages/.openspec.yaml create mode 100644 openspec/changes/connections-catalogue-pages/design.md create mode 100644 openspec/changes/connections-catalogue-pages/proposal.md create mode 100644 openspec/changes/connections-catalogue-pages/specs/catalogue-connection-pages/spec.md create mode 100644 openspec/changes/connections-catalogue-pages/tasks.md diff --git a/openspec/changes/connections-catalogue-pages/.openspec.yaml b/openspec/changes/connections-catalogue-pages/.openspec.yaml new file mode 100644 index 000000000..7f2ad572a --- /dev/null +++ b/openspec/changes/connections-catalogue-pages/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-27 diff --git a/openspec/changes/connections-catalogue-pages/design.md b/openspec/changes/connections-catalogue-pages/design.md new file mode 100644 index 000000000..261ea9e59 --- /dev/null +++ b/openspec/changes/connections-catalogue-pages/design.md @@ -0,0 +1,59 @@ +# Design: connections-catalogue-pages + +Read at development `49e65cb4`. Every path and line below was opened for this design. + +## Context + +A connection (`koppeling`) is a catalogue object: an application A talks to an application B, or to a national provision, over a transport, in a direction. The schema is complete (`lib/Settings/softwarecatalogus_register.json:3565`) and the ArchiMate import fills it, but the app shows it nowhere. The pages are declarative manifest pages over OpenRegister (ADR-001, ADR-024), so no PHP controller or service is added. + +## D1. Pages live in a manifest fragment + +New file `src/manifest.d/connections.json` (ADR-037), merged at build like `src/manifest.d/connection-registry.json`. It holds: + +- `Koppelingen`, route `/koppelingen`, `type: index`, `register: @resolve:voorzieningen_register`, `schema: connection`. Columns: `name`, `type`, `status`, `moduleA`, `moduleB`, `nonMunicipalProvision`, `dataExchangeDirection`. `filterMenu: true`, so the table header lists the values of every enum column as toggleable filters (`CnIndexPage.vue:2236` in `@conduction/nextcloud-vue` 2.57.1). `quickFilters` on status: All, In use, In development, End of support, Withdrawn, the way `Contracten` does it (`src/manifest.json:540`). +- `KoppelingDetail`, route `/koppelingen/:id`, `type: detail`. Widgets: `kp-data` (type `data`, all visible fields), `kp-files` (files integration, the schema already allows files), `kp-related` (type `related`), and a History tab in the sidebar like every other detail page. `lifecycleActions` on, so `CnLifecycleActions` (`CnDetailPage.vue:158`) offers release, sunset and withdraw once D3 lands. + +Rejected: adding the pages to `src/manifest.json` directly. That file is already 1,000+ lines, and ADR-037 puts a change's pages in its own fragment so two changes do not conflict on one file. + +## D2. The application page gets two connection lists + +`ModuleDetail` (`src/manifest.json:491`) gains two `object-list` widgets: + +| id | filter | title | +|---|---|---| +| `md-connections-out` | `{ "moduleA": "@objectId" }` | Connections from this application | +| `md-connections-in` | `{ "moduleB": "@objectId" }` | Connections to this application | + +Both set `rowRoute: KoppelingDetail`, `viewAllRoute: Koppelingen` with the same filter in `viewAllQuery`, and columns `type`, `status`, the other side of the connection. `CnObjectListWidget` takes one filter object with AND semantics (`CnObjectListWidget.vue:503`), so one list with "A or B" is not possible declaratively; two lists also say which way the data flows. + +Rejected: a custom widget that calls `GET /api/koppelingen-gebruik/{uuid}` (`AangebodenGebruikController.php:208`). That endpoint is public, mixes usages into the answer, and would add the first caller of a custom endpoint where OpenRegister's own list already serves the need (ADR-022). + +## D3. Register fixes in the same change + +All in `lib/Settings/softwarecatalogus_register.json`, schema `connection`: + +1. **Lifecycle states.** Replace the Dutch states in `x-openregister-lifecycle` (:3926) with the enum values: initial `in development`, final `withdrawn`, transitions release (`in development` to `in use`), sunset (`in use` to `end of support`), withdraw (`in use`, `end of support` to `withdrawn`). The rows already hold the English values (`lib/Repair/RenameDutchCatalogValues.php:87-88`). +2. **Picker.** Change `objectConfiguration.queryParams` on `nonMunicipalProvision` (:3720) to `gemmaType=Buitengemeentelijke voorziening`, the spelling the GEMMA model uses. +3. **Name template.** `objectNameField` names `gegevensuitwisselingRichting` and `buitengemeentelijkVoorziening`, keys the schema renamed to `dataExchangeDirection` and `nonMunicipalProvision`, and maps `AnaarB`, `BnaarA`, `bi-directioneel` where the enum holds `AtoB`, `BtoA`, `bi-directional`. Rewrite it on the current keys and values, so a connection reads "Application A to Application B" in lists and pickers. +4. **Facets.** Set `facetable: true` on `type`, `status` and `dataExchangeDirection`, so the index page can count and filter them. +5. **Version.** Bump the `connection` schema version to 0.3.2 and the register version, and add a changelog line. The register changelog entry 2.4.4 (`register.json:7`) records why: OpenRegister skips an import whose deployed version is not lower, and its content check ignores `configuration`. + +## D4. Menu + +Add `Connections` as a child of the `Modules` (Applications) menu entry, in the fragment. `CnAppNav` supports one level of `children[]` (`CnAppNav.vue:6`). ADR-097 decision 1 caps the main menu at six top-level entries and asks an amendment for more. Stackiq already carries fifteen, so this change adds none. + +Rejected: a top-level `Connections` entry. It would need an ADR-097 amendment that this change has no standing to make. + +## Declarative versus imperative + +Everything here is declarative: manifest pages, `object-list` widgets, the schema's own lifecycle and facets (ADR-031). No service, controller or route is added. Access follows the schema's existing `authorization` block (`register.json:3565` area): organisation-scoped read and public read after `publicationDate`. + +## Seed data + +No new schema. The demo register `lib/Settings/stackiq_mock_register.json` gains three connections (an API between two demo applications, a file transfer, and one to a national provision) so the pages show rows on a fresh install. + +## Risks + +- `ModuleDetail` is also edited by `landscape-application-page`. Both add rows to the same grid. The second change to land moves its widgets below the first. +- Existing connections whose `nonMunicipalProvision` points at an element still resolve; only the picker's query changes. +- A connection readable by the public group shows on the public frontend already; the pages add no new read path. diff --git a/openspec/changes/connections-catalogue-pages/proposal.md b/openspec/changes/connections-catalogue-pages/proposal.md new file mode 100644 index 000000000..11c63bb0b --- /dev/null +++ b/openspec/changes/connections-catalogue-pages/proposal.md @@ -0,0 +1,52 @@ +--- +kind: code +depends_on: [] +--- + +# Connections get their own pages + +## Summary + +Stackiq stores the connections (koppelingen) between applications, but no page lists them, opens one, or shows them on the application they belong to. This change adds a Connections index page and a connection detail page, a filter on transport type, a connections section on the application page, and a working picker for the national provision a connection reaches. + +## Why + +Rows from the stackiq matrix (`openspec/parity/capabilities.json`): + +- `stackiq:conn-list-page`, "Browse all connections in the catalogue in one list and open each one." Rated no and marked specified with no change directory, so this is the missing change. GEMMA Softwarecatalogus rates yes: https://www.softwarecatalogus.nl/node/13683, "Alle koppelingen ... staan de koppelingen van alle gemeenten en samenwerkingsverbanden". SAP LeanIX rates yes: https://help.sap.com/docs/leanix/ea/interface-modeling-guidelines, interfaces are fact sheets listed in the inventory. +- `stackiq:conn-type-filter`, "Filter connections by type, such as an API, a file exchange or a message." Rated no, marked specified with no change directory. SAP LeanIX rates yes on the same page: interface subtypes and transfer type are filterable in the inventory. Core area (connections). +- `stackiq:conn-per-application`, "See every connection an application has, from that application's own page." Rated partial, built. Three competitors rate yes: SAP LeanIX (https://help.sap.com/docs/leanix/ea/circle-map-report, relations on the fact sheet), BlueDolphin (https://help.bluedolphin.io/en/articles/11967550-working-with-object-relationships, the Relationships tab) and GLPI (source read at 11.0.9, `src/Impact.php:91`, an impact analysis tab listing related items in both directions). The missing half is a connections section on the application page that opens each connection. +- `stackiq:conn-external-provision`, "Record a connection from an application to a national provision such as a basisregistratie." Rated partial, built, one competitor yes: GEMMA Softwarecatalogus (https://www.softwarecatalogus.nl/Opvoeren%20koppeling%20iJw%20en%20iWmo). It rides with `conn-list-page`: its missing half is a page to fill the field, and the connection form on the new page shows it. + +No tender, feature request or roadmap row names these rows. `conn-list-page` and `conn-type-filter` sit in the product's core area. + +## What stackiq has today + +- The `connection` schema (`lib/Settings/softwarecatalogus_register.json:3565`, version 0.3.1) holds name, transport `type`, `status`, four lifecycle dates, `dataExchangeDirection` (:3676), `moduleA` (:3689), `moduleB` (:3705), `nonMunicipalProvision` (:3720), `standardVersions` and `realisedWithIntermediaryModule`. +- No manifest page uses this schema. The Integrations page (`src/manifest.d/connection-registry.json:23`) lists integriq's `app_connection`, which its `_note` says is a different thing. +- The application page `ModuleDetail` (`src/manifest.json:491`) shows connections only as untyped entries in the generic related panel `md-related` (:502). +- `GET /api/koppelingen-gebruik/{uuid}` (`appinfo/routes.php:269`, `lib/Controller/AangebodenGebruikController.php:208`) returns an application's connections and usages. Nothing in `src/` calls it. +- Two register defects would break the pages even once they exist: + - The connection lifecycle (`register.json:3926`) names `in ontwikkeling`, `in gebruik`, `einde ondersteuning` and `teruggetrokken`. The status enum and the migrated rows (`lib/Repair/RenameDutchCatalogValues.php:87-88`) hold `in development`, `in use`, `end of support` and `withdrawn`, so no transition matches any row. + - The `nonMunicipalProvision` picker filters on `gemmaType=Buitengemeentenlijke voorziening` (`register.json:3720`). The GEMMA model spells it `Buitengemeentelijke voorziening` (59 times in `lib/Settings/GEMMA_release.xml`), so the picker finds nothing. + +## What this change builds + +1. A Connections index page (`Koppelingen`, `/koppelingen`) over the `connection` schema, with a filter on transport type and quick filters on status. +2. A connection detail page (`KoppelingDetail`, `/koppelingen/:id`) with the connection data, both applications, the national provision, standards, documents, history and the status transitions. +3. A connections section on the application page: the connections that start at the application and the ones that end there, each row opening the detail page. +4. Register fixes: lifecycle states that match the enum, the picker's GEMMA type, `type` and `status` marked facetable, and the schema version bumped so the edit deploys. +5. A menu entry for Connections under Applications, without a new top-level entry (ADR-097). + +## Out of scope + +- Drawing connections as a diagram and exporting the link graph: `connections-diagram-and-graph-export`. +- The APIs an application exposes: `connections-api-catalogue`. +- Deriving connections automatically: `connections-derived-dependencies`. +- integriq's `app_connection` and the Integrations page: open change `adopt-connection-registry`. +- The public frontend's own connection form (`README.md:273`, a separate repository). + +## Risks + +- `landscape-application-page` also edits the `ModuleDetail` grid. Whichever change lands second rebases the layout rows. +- A lifecycle edit without a schema version bump never deploys (register changelog 2.4.4, `register.json:7`). diff --git a/openspec/changes/connections-catalogue-pages/specs/catalogue-connection-pages/spec.md b/openspec/changes/connections-catalogue-pages/specs/catalogue-connection-pages/spec.md new file mode 100644 index 000000000..c19d9aca2 --- /dev/null +++ b/openspec/changes/connections-catalogue-pages/specs/catalogue-connection-pages/spec.md @@ -0,0 +1,74 @@ +# catalogue-connection-pages specification + +**Status**: proposed +**Scope**: stackiq +**OpenSpec changes**: +- connections-catalogue-pages + +## Purpose + +A connection records that one application exchanges data with another, or with a national provision. Users browse connections, filter them by transport, open one, and see an application's connections on its own page. Matrix rows `stackiq:conn-list-page`, `stackiq:conn-type-filter`, `stackiq:conn-per-application` and `stackiq:conn-external-provision`. + +## ADDED Requirements + +### Requirement: REQ-CCP-001 A user can browse every connection they may read in one list + +Stackiq SHALL offer an index page `Koppelingen` at `/koppelingen` over the `connection` schema, reached from the Applications menu entry. It SHALL list every connection the user may read under the schema's authorization, with its name, transport type, status, both applications and the national provision, and each row SHALL open the connection's detail page. + +#### Scenario: A municipal information manager opens the connections list +@e2e tests/e2e/workflows/connections.spec.ts + +- **GIVEN** an information manager of a municipality whose organisation registered two connections +- **WHEN** they open Applications, then Connections +- **THEN** the page `/koppelingen` lists both connections with type, status and the two applications +- **AND** clicking a row opens `/koppelingen/` + +#### Scenario: A connection of another municipality stays hidden +@e2e exclude The read rule is OpenRegister's schema RBAC; tests/Unit/Settings/SchemaRbacTest.php asserts the connection read rule. + +- **GIVEN** a connection owned by another organisation with no publication date +- **WHEN** the information manager opens the connections list +- **THEN** that connection is not listed + +### Requirement: REQ-CCP-002 A user can filter connections by transport type + +The connections list SHALL let the user narrow the rows to one or more transport types (`api`, `file transfer`, `digikoppeling`, `message que`, `upload to portal`, `webservices`, `n/a`) and to one status, and SHALL show how many rows each value holds. + +#### Scenario: Only API connections remain +@e2e tests/e2e/workflows/connections.spec.ts + +- **GIVEN** the connections list shows one `api` and one `file transfer` connection +- **WHEN** the information manager picks type `api` in the table's filter menu +- **THEN** only the `api` connection remains in the list + +### Requirement: REQ-CCP-003 The application page lists the connections that start and end there + +The application page `ModuleDetail` SHALL show the connections in which the application is application A, and separately the connections in which it is application B. Each row SHALL open the connection's detail page, and each list SHALL link to the connections list filtered on that application. + +#### Scenario: An application owner sees both directions +@e2e tests/e2e/workflows/connections.spec.ts + +- **GIVEN** application X is application A in connection 1 and application B in connection 2 +- **WHEN** the application owner opens the page of application X +- **THEN** "Connections from this application" lists connection 1 +- **AND** "Connections to this application" lists connection 2 +- **AND** clicking connection 2 opens its detail page + +### Requirement: REQ-CCP-004 The connection schema offers transitions and a picker that match its data + +The `connection` schema SHALL declare its lifecycle on the status values its rows hold (`in development`, `in use`, `end of support`, `withdrawn`), SHALL filter the national provision picker on the GEMMA type `Buitengemeentelijke voorziening`, and SHALL build a connection's display name from `moduleA`, `dataExchangeDirection` and `moduleB` or `nonMunicipalProvision`. + +#### Scenario: A supplier releases a connection from its detail page +@e2e tests/e2e/workflows/connections.spec.ts + +- **GIVEN** a connection with status `in development` +- **WHEN** a supplier with update rights opens its detail page +- **THEN** the page offers the release action +- **AND** after release the status reads `in use` + +#### Scenario: The national provision picker lists GEMMA provisions +@e2e exclude The picker is the library's related-object field; tests/Unit/Settings/ConnectionSchemaTest.php asserts the queryParams value and that GEMMA_release.xml uses the same spelling. + +- **GIVEN** the GEMMA model is imported +- **WHEN** a user edits a connection and opens the national provision field +- **THEN** it offers the GEMMA elements of type Buitengemeentelijke voorziening, such as a basisregistratie diff --git a/openspec/changes/connections-catalogue-pages/tasks.md b/openspec/changes/connections-catalogue-pages/tasks.md new file mode 100644 index 000000000..737f099ad --- /dev/null +++ b/openspec/changes/connections-catalogue-pages/tasks.md @@ -0,0 +1,47 @@ +# Tasks: connections-catalogue-pages + +## Implementation tasks + +### Task 1: Fix the connection schema +- **spec_ref**: openspec/changes/connections-catalogue-pages/specs/catalogue-connection-pages/spec.md#requirement-req-ccp-004-the-connection-schema-offers-transitions-and-a-picker-that-match-its-data +- **files**: `lib/Settings/softwarecatalogus_register.json`, `lib/Settings/stackiq_mock_register.json` +- **acceptance_criteria**: + - GIVEN the imported register WHEN a connection in `in development` is opened THEN the release transition is offered + - GIVEN the connection form WHEN the national provision picker opens THEN it lists GEMMA elements of type Buitengemeentelijke voorziening + - GIVEN the schema version bump WHEN the repair step imports the register THEN the new configuration is deployed +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Settings/ConnectionSchemaTest.php`: lifecycle states are enum values, queryParams spelling, name template keys exist) + +### Task 2: Connections index and detail pages +- **spec_ref**: openspec/changes/connections-catalogue-pages/specs/catalogue-connection-pages/spec.md#requirement-req-ccp-001-a-user-can-browse-every-connection-they-may-read-in-one-list +- **files**: `src/manifest.d/connections.json`, `l10n/en.json`, `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN connections exist WHEN the user opens Applications, Connections THEN the list shows them with type and status + - GIVEN the list WHEN the user filters on type api THEN only API connections remain + - GIVEN a row WHEN the user opens it THEN the detail page shows both applications and the national provision +- [ ] Implement +- [ ] Test (Playwright `tests/e2e/workflows/connections.spec.ts`, manifest validation in `npm run lint`) + +### Task 3: Connections on the application page +- **spec_ref**: openspec/changes/connections-catalogue-pages/specs/catalogue-connection-pages/spec.md#requirement-req-ccp-003-the-application-page-lists-the-connections-that-start-and-end-there +- **files**: `src/manifest.json` (ModuleDetail widgets and layout) +- **acceptance_criteria**: + - GIVEN an application that is A in one connection and B in another WHEN its page opens THEN each list shows its connection + - GIVEN a connection row WHEN the user clicks it THEN KoppelingDetail opens +- [ ] Implement +- [ ] Test (Playwright `tests/e2e/workflows/connections.spec.ts`, application page case) + +### Task 4: Documentation +- **spec_ref**: openspec/changes/connections-catalogue-pages/specs/catalogue-connection-pages/spec.md#requirement-req-ccp-001-a-user-can-browse-every-connection-they-may-read-in-one-list +- **files**: `docs/features/connections.md`, `docs/images/connections-index.png` +- **acceptance_criteria**: + - GIVEN the docs site WHEN a reader opens Connections THEN it explains the list, the filters and the application page section with a screenshot +- [ ] Implement +- [ ] Test (docs build `npm run build` in `docs/`, screenshot taken with Playwright) + +## Verification + +- `openspec validate connections-catalogue-pages --type change --strict` passes. +- `composer check:strict` and `npm run lint` pass; the new PHPUnit test and the Playwright spec pass. +- English and Dutch strings exist for every new label (ADR-005). +- Feature documentation with a screenshot is in `docs/features/` (ADR-010). From 113ecb3d7c5dd05005a24ef666d5656ffb858763 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:35:14 +0200 Subject: [PATCH 02/15] docs(openspec): connections-diagram-and-graph-export, application map, list diagram and connections in the ArchiMate export --- .../.openspec.yaml | 2 + .../design.md | 45 ++++++++++++++ .../proposal.md | 45 ++++++++++++++ .../connection-diagram-and-export/spec.md | 58 +++++++++++++++++++ .../tasks.md | 53 +++++++++++++++++ 5 files changed, 203 insertions(+) create mode 100644 openspec/changes/connections-diagram-and-graph-export/.openspec.yaml create mode 100644 openspec/changes/connections-diagram-and-graph-export/design.md create mode 100644 openspec/changes/connections-diagram-and-graph-export/proposal.md create mode 100644 openspec/changes/connections-diagram-and-graph-export/specs/connection-diagram-and-export/spec.md create mode 100644 openspec/changes/connections-diagram-and-graph-export/tasks.md diff --git a/openspec/changes/connections-diagram-and-graph-export/.openspec.yaml b/openspec/changes/connections-diagram-and-graph-export/.openspec.yaml new file mode 100644 index 000000000..7f2ad572a --- /dev/null +++ b/openspec/changes/connections-diagram-and-graph-export/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-27 diff --git a/openspec/changes/connections-diagram-and-graph-export/design.md b/openspec/changes/connections-diagram-and-graph-export/design.md new file mode 100644 index 000000000..caf3790b7 --- /dev/null +++ b/openspec/changes/connections-diagram-and-graph-export/design.md @@ -0,0 +1,45 @@ +# Design: connections-diagram-and-graph-export + +Read at development `49e65cb4`, with `@conduction/nextcloud-vue` 2.57.1 (`package.json:45`). + +## Context + +The connection data is complete in the `connection` schema (`lib/Settings/softwarecatalogus_register.json:3565`): `moduleA` (:3689), `moduleB` (:3705), `nonMunicipalProvision` (:3720), `type`, `dataExchangeDirection` (:3676) and `status`. `connections-catalogue-pages` gives it an index page, a detail page and two lists on the application page. This change draws and exports what those pages list. + +## D1. The application map uses CnRelationshipGraph + +A new custom widget `ConnectionMapWidget` (`src/components/connections/ConnectionMapWidget.vue`), registered in `src/customComponents.js` and placed on `ModuleDetail` (`src/manifest.json:491`) as a body widget. It reads the application's connections through the object store (two queries, `moduleA` and `moduleB` equal to the application, as `connections-catalogue-pages` D2 does) and hands nodes and edges to `CnRelationshipGraph` (`src/components/CnRelationshipGraph/CnRelationshipGraph.vue` in the library) with `layout: 'radial'` and the application as root. Edge labels carry the transport type; the arrow direction follows `dataExchangeDirection`. + +Rejected: a new graph library. `CnRelationshipGraph` covers one hop, which is what "what is this application linked to" asks (ADR-012). + +## D2. The list diagram uses CnGraphCanvas read-only + +The `Koppelingen` index page gets a "Diagram" toggle next to the table, rendered by a custom component `ConnectionsDiagram` (`src/components/connections/ConnectionsDiagram.vue`). It takes the rows the index page fetched (same filters, same page) and draws them with `CnGraphCanvas` with `interactive: false`. Node positions come from a small layered layout in `src/utils/connectionLayout.js`: applications that only send on the left, that only receive on the right, the rest in the middle, sorted by name. No new dependency: Vue Flow already ships with the library. + +Rejected: a force layout. It moves nodes on every render and needs a layout engine the library chose not to carry (`CnRelationshipGraph.vue:90`). + +## D3. Downloads are client-side + +The map widget's action menu offers "Download as SVG", "Download as PNG" and "Download links as CSV". SVG serialises the rendered `svg` element; PNG draws it on a canvas; CSV lists one line per connection with application A, direction, application B or national provision, type and status. Nothing goes to the server, so the download follows exactly what the user may read. + +## D4. Connections in the organisation ArchiMate export + +- `SettingsController::exportOrgArchiMate()` (`lib/Controller/SettingsController.php:1685`) reads a fourth option `connections` next to `modules`, `deelnames` and `usage` (:1698-1700), default false. +- `ArchiMateService::exportOrgArchiMate()` (`lib/Service/ArchiMateService.php:302`) passes it on. A new private method in `lib/Service/ArchiMateExportService.php` loads the connections whose `moduleA` or `moduleB` is an application of the organisation, and writes each as an ArchiMate `Flow` relationship in the model's `relationships` section (the section writer at `:1160` and `:1197`), source and target by the exported application component identifiers, with properties for transport type, direction and status. A bi-directional connection writes two flows. A connection to a national provision targets that element's identifier. +- A flow whose end is not in the exported model is skipped and counted in the export result. +- `src/views/settings/sections/ArchiMateImportExport.vue:571` gains a fourth checkbox, "Connections". + +Rejected: a new export endpoint for connections only. The GEMMA Softwarecatalogus exports "pakketten én koppelingen in 1 model", and one file is what Archi opens. + +## Declarative versus imperative + +The map and the diagram are views over data the schema already holds; they add no relation or lifecycle behaviour. The export is imperative because it is: the ArchiMate writer is a PHP service today. + +## Seed data + +No schema change. The three demo connections from `connections-catalogue-pages` are enough to draw a map. + +## Risks + +- `ArchiMateExportService.php` is large (the export path around `:2734`). The new method stays separate and is unit tested against a fixture. +- The list diagram draws only the fetched page; the toggle says so. diff --git a/openspec/changes/connections-diagram-and-graph-export/proposal.md b/openspec/changes/connections-diagram-and-graph-export/proposal.md new file mode 100644 index 000000000..71f803ec8 --- /dev/null +++ b/openspec/changes/connections-diagram-and-graph-export/proposal.md @@ -0,0 +1,45 @@ +--- +kind: code +depends_on: + - connections-catalogue-pages +--- + +# See connections as a diagram and export the link graph + +## Summary + +An application owner can see an application's connections drawn as a map on its page, and an information manager can see the filtered connections list as a diagram. The link graph leaves stackiq in two forms: the application map as an image and a CSV of its links, and the connections inside the organisation's ArchiMate export, so Archi and other modelling tools receive them. + +## Why + +Rows from the stackiq matrix: + +- `stackiq:conn-diagram`, "See the connections between applications drawn as a diagram." Rated no. Four competitors rate yes: SAP LeanIX (https://help.sap.com/docs/leanix/ea/data-flow, data flow diagrams "understand how applications are connected"), BlueDolphin (https://help.bluedolphin.io/en/articles/11967472-welcome-to-bluedolphin, "visualize chains and information flows"), GLPI (source read at 11.0.9, `src/Impact.php:252` displayGraphView draws the relation network) and TOPdesk (https://docs.topdesk.com/en/linking-assets-to-other-assets.html, "the graphical overview to see a visual representation of their relationship"). Core area (connections). +- `stackiq:conn-export-graph`, "Export the graph of what an application is linked to, for use elsewhere." Rated no. Two competitors rate yes: GEMMA Softwarecatalogus (https://www.softwarecatalogus.nl/Handleiding%20koppeling%20architectuurtools, "Pakketten én koppelingen worden in 1 model geëxporteerd ... AMEFF-export") and GLPI (source read at 11.0.9, CSV export in `front/impactcsv.php` and PNG or JPEG download at `js/impact.js:2528`). Core area. + +No tender, feature request or roadmap row names these rows. + +## What stackiq has today + +- No page draws connections. `src/store/modules/view.js` calls `GET /api/views` and nothing imports it. +- The organisation ArchiMate export (`GET /api/archimate/export/organization/{organizationUuid}`, `appinfo/routes.php:98`, `lib/Controller/SettingsController.php:1685`) takes the options `modules`, `deelnames` and `usage` (:1698-1700) and writes GEMMA view copies with the organisation's applications (`lib/Service/ArchiMateExportService.php:2734`). It writes no catalogue connection: the only `connection` handling in the export service is a diagram line inside a copied view (`:632-634`, `:742`). +- The export is reached from the admin settings section (`src/views/settings/sections/ArchiMateImportExport.vue:571`), with checkboxes Modules, Deelnames and Gebruik, for a Nextcloud admin or an organisation admin (`SettingsController.php:1737`). +- `@conduction/nextcloud-vue` 2.57.1 ships `CnRelationshipGraph` (an SVG graph with radial, grid and manual layouts) and `CnGraphCanvas` (a Vue Flow canvas). + +## What this change builds + +1. A connection map on the application page: the application in the centre, every application or national provision it connects to around it, each line labelled with transport and direction. Clicking a node opens that application or the connection. +2. A diagram view on the connections list (`Koppelingen`, from `connections-catalogue-pages`), drawing the rows the current filters return. +3. Downloads from the application map: the drawing as SVG and PNG, and the links as CSV. +4. A `connections` option on the organisation ArchiMate export, writing each connection of the organisation as an ArchiMate flow relationship between the application components, with transport type, direction and status as properties. + +## Out of scope + +- Drawing or editing architecture views: `architecture-views-editor`. +- Making the organisation export reachable outside admin settings (`stackiq:arch-export-org`, deferred as partial, built, no demand). +- Impact analysis before retiring an application (`stackiq:conn-impact-analysis`, owned by openregister). + +## Risks + +- A large landscape gives an unreadable diagram. The list diagram caps at the page size and asks the user to filter. +- Archi reads flow relationships only between elements it knows. The export writes a flow only when both ends are in the exported model, and counts the ones it skipped. diff --git a/openspec/changes/connections-diagram-and-graph-export/specs/connection-diagram-and-export/spec.md b/openspec/changes/connections-diagram-and-graph-export/specs/connection-diagram-and-export/spec.md new file mode 100644 index 000000000..97206822e --- /dev/null +++ b/openspec/changes/connections-diagram-and-graph-export/specs/connection-diagram-and-export/spec.md @@ -0,0 +1,58 @@ +# connection-diagram-and-export specification + +**Status**: proposed +**Scope**: stackiq +**OpenSpec changes**: +- connections-diagram-and-graph-export + +## Purpose + +Users see how applications connect as a picture, and take the link graph to other tools. Matrix rows `stackiq:conn-diagram` and `stackiq:conn-export-graph`. + +## ADDED Requirements + +### Requirement: REQ-CDX-001 The application page draws the application's connections as a map + +The application page `ModuleDetail` SHALL show a map with the application in the centre and every application or national provision it has a readable connection with around it. Each line SHALL carry the transport type and point in the connection's data exchange direction. Clicking a node SHALL open that application, and clicking a line SHALL open the connection. + +#### Scenario: An application owner reads the map +@e2e tests/e2e/workflows/connections.spec.ts + +- **GIVEN** application X has an `api` connection to application Y and a `file transfer` connection to a national provision +- **WHEN** the application owner opens the page of application X +- **THEN** the connection map shows X in the centre with Y and the national provision around it +- **AND** the line to Y reads `api` + +### Requirement: REQ-CDX-002 The connections list can be shown as a diagram of the filtered rows + +The connections list `Koppelingen` SHALL offer a diagram view that draws exactly the rows the current filters return, with a stable layout: the same rows SHALL give the same positions. + +#### Scenario: An information manager draws only the API connections +@e2e tests/e2e/workflows/connections.spec.ts + +- **GIVEN** the connections list filtered on transport type `api` +- **WHEN** the information manager switches to Diagram +- **THEN** the diagram shows only the `api` connections and their applications + +### Requirement: REQ-CDX-003 A user can download an application's map and its links + +The connection map SHALL let the user download the drawing as SVG and as PNG, and the links as a CSV with one line per connection: application A, direction, application B or national provision, transport type and status. The download SHALL hold only connections the user may read. + +#### Scenario: An architect takes the links to a spreadsheet +@e2e exclude The download is built client-side; tests/vitest/connectionExport.spec.js asserts the CSV columns, one line per connection and quoting. + +- **GIVEN** the map of application X with two connections +- **WHEN** the architect picks Download links as CSV +- **THEN** a CSV downloads with a header line and two connection lines + +### Requirement: REQ-CDX-004 The organisation ArchiMate export can carry the organisation's connections + +`GET /api/archimate/export/organization/{organizationUuid}` SHALL accept `connections=true`. The export SHALL then write every connection between applications in the export as an ArchiMate flow relationship, and a connection to a national provision as a flow to that element, with the transport type, direction and status as properties. A connection whose other end is not in the exported model SHALL be skipped and counted in the result. + +#### Scenario: An organisation admin exports applications and connections in one file +@e2e tests/e2e/org-archimate-export.spec.ts + +- **GIVEN** an organisation whose two applications share one `api` connection +- **WHEN** the organisation admin runs the organisation export with Applications and Connections ticked +- **THEN** the downloaded file holds a flow relationship between the two application components +- **AND** the relationship carries the property transport type `api` diff --git a/openspec/changes/connections-diagram-and-graph-export/tasks.md b/openspec/changes/connections-diagram-and-graph-export/tasks.md new file mode 100644 index 000000000..4eb83425e --- /dev/null +++ b/openspec/changes/connections-diagram-and-graph-export/tasks.md @@ -0,0 +1,53 @@ +# Tasks: connections-diagram-and-graph-export + +## Implementation tasks + +### Task 1: Connection map on the application page +- **spec_ref**: openspec/changes/connections-diagram-and-graph-export/specs/connection-diagram-and-export/spec.md#requirement-req-cdx-001-the-application-page-draws-the-applications-connections-as-a-map +- **files**: `src/components/connections/ConnectionMapWidget.vue`, `src/customComponents.js`, `src/manifest.json` (ModuleDetail bodyWidgets), `l10n/en.json`, `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN an application with three connections WHEN its page opens THEN the map shows the application in the centre and three linked nodes with transport labels + - GIVEN a node on the map WHEN the user clicks it THEN the linked application or connection opens +- [ ] Implement +- [ ] Test (vitest `tests/vitest/connectionMap.spec.js` for nodes and edges from connections; Playwright `tests/e2e/workflows/connections.spec.ts` map case) + +### Task 2: Diagram view on the connections list +- **spec_ref**: openspec/changes/connections-diagram-and-graph-export/specs/connection-diagram-and-export/spec.md#requirement-req-cdx-002-the-connections-list-can-be-shown-as-a-diagram-of-the-filtered-rows +- **files**: `src/components/connections/ConnectionsDiagram.vue`, `src/utils/connectionLayout.js`, `src/manifest.d/connections.json` +- **acceptance_criteria**: + - GIVEN the connections list filtered on type api WHEN the user switches to Diagram THEN only the api connections are drawn + - GIVEN the same rows WHEN the diagram renders twice THEN every node keeps its position +- [ ] Implement +- [ ] Test (vitest `tests/vitest/connectionLayout.spec.js` for a stable layered layout) + +### Task 3: Downloads from the map +- **spec_ref**: openspec/changes/connections-diagram-and-graph-export/specs/connection-diagram-and-export/spec.md#requirement-req-cdx-003-a-user-can-download-an-applications-map-and-its-links +- **files**: `src/components/connections/ConnectionMapWidget.vue`, `src/utils/connectionExport.js` +- **acceptance_criteria**: + - GIVEN the map of an application WHEN the user picks Download links as CSV THEN a CSV with one line per connection downloads + - GIVEN the same map WHEN the user picks Download as SVG THEN an SVG with the drawn nodes downloads +- [ ] Implement +- [ ] Test (vitest `tests/vitest/connectionExport.spec.js` for CSV columns and escaping) + +### Task 4: Connections in the organisation ArchiMate export +- **spec_ref**: openspec/changes/connections-diagram-and-graph-export/specs/connection-diagram-and-export/spec.md#requirement-req-cdx-004-the-organisation-archimate-export-can-carry-the-organisations-connections +- **files**: `lib/Controller/SettingsController.php`, `lib/Service/ArchiMateService.php`, `lib/Service/ArchiMateExportService.php`, `src/views/settings/sections/ArchiMateImportExport.vue` +- **acceptance_criteria**: + - GIVEN an organisation with two connections between its applications WHEN the export runs with connections on THEN the file holds two flow relationships with transport properties + - GIVEN a connection whose other end is outside the export WHEN the export runs THEN it is skipped and counted +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Service/ArchiMateExportConnectionsTest.php` against a fixture model; Playwright `tests/e2e/org-archimate-export.spec.ts` extended with the checkbox) + +### Task 5: Documentation +- **spec_ref**: openspec/changes/connections-diagram-and-graph-export/specs/connection-diagram-and-export/spec.md#requirement-req-cdx-001-the-application-page-draws-the-applications-connections-as-a-map +- **files**: `docs/features/connections.md`, `docs/images/connection-map.png` +- **acceptance_criteria**: + - GIVEN the docs site WHEN a reader opens Connections THEN the map, the diagram and the export option are explained with a screenshot +- [ ] Implement +- [ ] Test (docs build, screenshot with Playwright) + +## Verification + +- `openspec validate connections-diagram-and-graph-export --type change --strict` passes. +- `composer check:strict` and `npm run lint` pass; the PHPUnit, vitest and Playwright cases above pass. +- English and Dutch strings for every new label (ADR-005); docs with a screenshot (ADR-010). From 7d1f849452cbd6551753902ff92555f98a9f94b5 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:36:54 +0200 Subject: [PATCH 03/15] docs(openspec): connections-api-catalogue, record the APIs an application offers --- .../connections-api-catalogue/.openspec.yaml | 2 + .../connections-api-catalogue/design.md | 55 +++++++++++++++++++ .../connections-api-catalogue/proposal.md | 42 ++++++++++++++ .../specs/application-interfaces/spec.md | 46 ++++++++++++++++ .../connections-api-catalogue/tasks.md | 35 ++++++++++++ 5 files changed, 180 insertions(+) create mode 100644 openspec/changes/connections-api-catalogue/.openspec.yaml create mode 100644 openspec/changes/connections-api-catalogue/design.md create mode 100644 openspec/changes/connections-api-catalogue/proposal.md create mode 100644 openspec/changes/connections-api-catalogue/specs/application-interfaces/spec.md create mode 100644 openspec/changes/connections-api-catalogue/tasks.md diff --git a/openspec/changes/connections-api-catalogue/.openspec.yaml b/openspec/changes/connections-api-catalogue/.openspec.yaml new file mode 100644 index 000000000..7f2ad572a --- /dev/null +++ b/openspec/changes/connections-api-catalogue/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-27 diff --git a/openspec/changes/connections-api-catalogue/design.md b/openspec/changes/connections-api-catalogue/design.md new file mode 100644 index 000000000..218aeba42 --- /dev/null +++ b/openspec/changes/connections-api-catalogue/design.md @@ -0,0 +1,55 @@ +# Design: connections-api-catalogue + +Read at development `49e65cb4`. + +## Context + +Stackiq's catalogue schemas live in `lib/Settings/softwarecatalogus_register.json`. Per ADR-037 a change adds its schema in a fragment under `lib/Settings/register.d/`, which `SettingsService::loadSettings()` deep-merges into the monolith at load (`lib/Service/SettingsService.php:1653-1680`). Lists append in that merge (`deepMergeConfig`, :7338), so a fragment can add a schema to the register's list. + +## D1. A new schema in a fragment + +File `lib/Settings/register.d/application-interfaces.json` with: + +- `components.schemas.applicationInterface`, schema.org type `WebAPI`: + +| property | type | notes | +|---|---|---| +| `name` | string, required | | +| `shortDescription`, `longDescription` | string, markdown for the long one | | +| `module` | `$ref` module, required | the application that offers the API, `inversedBy: interfaces` | +| `style` | enum `REST`, `SOAP`, `GraphQL`, `event or message`, `file`, `other` | facetable | +| `version` | string | | +| `specificationUrl` | string, format uri | an OpenAPI, WSDL or AsyncAPI document | +| `documentationUrl` | string, format uri | | +| `standardVersions` | array of `$ref` element, `queryParams: gemmaType=standaardversie` | the same picker as `connection.standardVersions` | +| `status` | enum `in development`, `in use`, `end of support`, `withdrawn` | the connection's values, with an `x-openregister-lifecycle` on those exact values | +| `publicationDate`, `depublicationDate` | date-time | | + +- `authorization` copied from `module` (`register.json` module schema): organisation-scoped read for `aanbod-beheerder`, read for `gebruik-beheerder`, public read after `publicationDate`; create and update for the groups that may edit a module. +- `components.registers.stackiq.schemas: ["applicationInterface"]` and `components.registers.stackiq.configuration.schemas.applicationInterface: { "magicMapping": true, "autoCreateTable": true }`, like every other catalogue schema (`register.json:853` onwards). +- On `connection`, a new optional property `interface` (`$ref` applicationInterface, `x-relation-filter` on `module` equal to the connection's `moduleB`), added through the same fragment. + +Rejected: a `subtype` value on `connection`. An API exists before anyone connects to it, has its own version and specification, and serves many connections; LeanIX models it as its own fact sheet for that reason. + +## D2. Pages + +In `src/manifest.d/application-interfaces.json`: + +- `Apis`, route `/apis`, `type: index`, schema `applicationInterface`, columns name, module, style, version, status; `filterMenu: true`. +- `ApiDetail`, route `/apis/:id`, `type: detail`: data widget, files, related (connections that name it), history tab. +- A menu child `APIs` under the `Modules` (Applications) entry, next to Connections from `connections-catalogue-pages`. No new top-level entry (ADR-097). + +On `ModuleDetail` (`src/manifest.json:491`) an `object-list` widget `md-apis` with filter `{ "module": "@objectId" }`, `rowRoute: ApiDetail`, and `allowCreate: true` with the application prefilled. + +## Declarative versus imperative + +All declarative: a schema with a lifecycle and relations, manifest pages, one object-list widget (ADR-031). No PHP. + +## Seed data + +`lib/Settings/stackiq_mock_register.json` gains two demo APIs on one demo application: a REST API with a specification URL pointing at a placeholder `https://example.org/openapi.json`, and an event API. + +## Risks + +- `ModuleDetail` also changes in `connections-catalogue-pages` and `landscape-application-page`; the widgets stack below each other. +- A schema added through a fragment has never been tried for a brand-new schema in this app (the two existing fragments modify schemas). Task 1 proves the merge with a unit test before any page work. diff --git a/openspec/changes/connections-api-catalogue/proposal.md b/openspec/changes/connections-api-catalogue/proposal.md new file mode 100644 index 000000000..f7d84e850 --- /dev/null +++ b/openspec/changes/connections-api-catalogue/proposal.md @@ -0,0 +1,42 @@ +--- +kind: code +depends_on: + - connections-catalogue-pages +--- + +# Record the APIs an application exposes + +## Summary + +A supplier or an information manager records the APIs an application offers, next to that application: the style, the version, where the specification lives, the standards it follows and its status. A connection can name the API it calls. The application page lists its APIs, and an APIs list shows them across the catalogue. + +## Why + +Row from the stackiq matrix: + +- `stackiq:conn-api-catalogue`, "Keep the APIs an application exposes in the catalogue next to the application." Rated no. SAP LeanIX rates yes: https://help.sap.com/docs/leanix/ea/interface-modeling-guidelines, interface subtype "API ... APIs provide functionalities accessible to external applications", related to the providing application. The row sits in the product's core area (connections), which is why it is built with one competitor. + +No tender, feature request or roadmap row names it. + +## What stackiq has today + +- No schema describes an API. The register's catalogue schemas are listed at `lib/Settings/softwarecatalogus_register.json:817` onwards (sector, suite, module, catalogService, vulnerability, contactPerson, organization, usage, catalogContract, connection, software-review, compliancy, moduleVersion, sbomComponent, bioMeasure). +- `connection.type` has the value `api` (`register.json:3565` schema), but it only labels the transport of one connection; it says nothing about the API itself. +- Standards live as GEMMA elements with `gemmaType` standaard and standaardversie; `module.standardVersions` and `connection.standardVersions` already point at them. + +## What this change builds + +1. A schema `applicationInterface` (title "API") in a register fragment: name, descriptions, the providing application, style, version, specification URL, documentation URL, standard versions, status and publication dates. +2. An optional `interface` field on `connection`, so a connection names the API it calls. +3. An APIs list page and an API detail page, reached under Applications in the menu. +4. An APIs section on the application page, with an Add button that fills in the application. + +## Out of scope + +- A developer portal: keys, subscriptions, a try-it console. The matrix category says stackiq is not a developer portal; integriq's open change `access-developer-portal-and-subscriptions` covers that for its gateway. +- Importing APIs from an OpenAPI file or a gateway (integriq's `gateway-openapi-import-and-publish` covers publishing through integriq). +- Checking an API against the NLGov REST API design rules (integriq's `gateway-api-design-rules-check`). + +## Risks + +- Suppliers and municipalities may both register the same API. The detail page shows the providing application and its supplier, and `operations-record-reconciliation` covers merging duplicates. diff --git a/openspec/changes/connections-api-catalogue/specs/application-interfaces/spec.md b/openspec/changes/connections-api-catalogue/specs/application-interfaces/spec.md new file mode 100644 index 000000000..37d9cc689 --- /dev/null +++ b/openspec/changes/connections-api-catalogue/specs/application-interfaces/spec.md @@ -0,0 +1,46 @@ +# application-interfaces specification + +**Status**: proposed +**Scope**: stackiq +**OpenSpec changes**: +- connections-api-catalogue + +## Purpose + +The catalogue records the APIs an application offers, next to the application, so an architect sees what can be connected to and how. Matrix row `stackiq:conn-api-catalogue`. + +## ADDED Requirements + +### Requirement: REQ-AIF-001 A user can record an API that an application offers + +Stackiq SHALL store an API as an `applicationInterface` object with a name, the providing application, a style, a version, a specification URL, a documentation URL, the standard versions it follows and a status. The status SHALL move through `in development`, `in use`, `end of support` and `withdrawn` by declared transitions. + +#### Scenario: A supplier adds a REST API to its application +@e2e tests/e2e/workflows/application-interfaces.spec.ts + +- **GIVEN** a supplier with edit rights on application X +- **WHEN** they open the page of application X, click Add in the APIs section and save name "Zaken API", style `REST`, version `1.2` and a specification URL +- **THEN** the APIs section of application X lists "Zaken API" with style REST and version 1.2 +- **AND** the APIs list at `/apis` shows it too + +### Requirement: REQ-AIF-002 The application page lists its APIs + +The application page `ModuleDetail` SHALL list the APIs whose providing application is that application, and each row SHALL open the API's detail page. + +#### Scenario: An architect checks what an application offers +@e2e tests/e2e/workflows/application-interfaces.spec.ts + +- **GIVEN** application X offers a REST API and an event API +- **WHEN** an architect opens the page of application X +- **THEN** the APIs section lists both APIs with their style and status + +### Requirement: REQ-AIF-003 A connection can name the API it calls + +The `connection` schema SHALL carry an optional `interface` field that points at an API of the connection's application B, and the API's detail page SHALL list the connections that name it. + +#### Scenario: An information manager links a connection to an API +@e2e exclude The field is a related-object picker in the library form; tests/Unit/Settings/ApplicationInterfaceFragmentTest.php asserts the property and its relation filter. + +- **GIVEN** a connection from application A to application X, and X offers "Zaken API" +- **WHEN** the information manager sets the connection's API to "Zaken API" +- **THEN** the detail page of "Zaken API" lists that connection diff --git a/openspec/changes/connections-api-catalogue/tasks.md b/openspec/changes/connections-api-catalogue/tasks.md new file mode 100644 index 000000000..8abb73477 --- /dev/null +++ b/openspec/changes/connections-api-catalogue/tasks.md @@ -0,0 +1,35 @@ +# Tasks: connections-api-catalogue + +## Implementation tasks + +### Task 1: The applicationInterface schema +- **spec_ref**: openspec/changes/connections-api-catalogue/specs/application-interfaces/spec.md#requirement-req-aif-001-a-user-can-record-an-api-that-an-application-offers +- **files**: `lib/Settings/register.d/application-interfaces.json`, `lib/Settings/stackiq_mock_register.json` +- **acceptance_criteria**: + - GIVEN the merged register WHEN it is imported THEN the stackiq register lists applicationInterface and its table exists + - GIVEN a connection WHEN its interface field is set THEN it holds an API of the connection's application B +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Settings/ApplicationInterfaceFragmentTest.php`: the merged register carries the schema, the register list entry and the lifecycle on enum values) + +### Task 2: APIs pages and the application page section +- **spec_ref**: openspec/changes/connections-api-catalogue/specs/application-interfaces/spec.md#requirement-req-aif-002-the-application-page-lists-its-apis +- **files**: `src/manifest.d/application-interfaces.json`, `src/manifest.json` (ModuleDetail), `l10n/en.json`, `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN an application with two APIs WHEN its page opens THEN the APIs section lists both + - GIVEN the APIs list WHEN the user filters on style REST THEN only REST APIs remain +- [ ] Implement +- [ ] Test (Playwright `tests/e2e/workflows/application-interfaces.spec.ts`) + +### Task 3: Documentation +- **spec_ref**: openspec/changes/connections-api-catalogue/specs/application-interfaces/spec.md#requirement-req-aif-001-a-user-can-record-an-api-that-an-application-offers +- **files**: `docs/features/application-interfaces.md`, `docs/images/application-apis.png` +- **acceptance_criteria**: + - GIVEN the docs site WHEN a reader opens APIs THEN it explains how to record an API and link a connection to it, with a screenshot +- [ ] Implement +- [ ] Test (docs build, screenshot with Playwright) + +## Verification + +- `openspec validate connections-api-catalogue --type change --strict` passes. +- `composer check:strict` and `npm run lint` pass; the PHPUnit and Playwright cases above pass. +- English and Dutch strings for every new label (ADR-005); docs with a screenshot (ADR-010). From 7f6c7a660b883d7d54f5b7dfe32ebf215bd2a7b7 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:38:42 +0200 Subject: [PATCH 04/15] docs(openspec): connections-derived-dependencies, suggest known and carried-over connections --- .../.openspec.yaml | 2 + .../design.md | 55 +++++++++++++++++ .../proposal.md | 44 +++++++++++++ .../specs/derived-connections/spec.md | 61 +++++++++++++++++++ .../connections-derived-dependencies/tasks.md | 53 ++++++++++++++++ 5 files changed, 215 insertions(+) create mode 100644 openspec/changes/connections-derived-dependencies/.openspec.yaml create mode 100644 openspec/changes/connections-derived-dependencies/design.md create mode 100644 openspec/changes/connections-derived-dependencies/proposal.md create mode 100644 openspec/changes/connections-derived-dependencies/specs/derived-connections/spec.md create mode 100644 openspec/changes/connections-derived-dependencies/tasks.md diff --git a/openspec/changes/connections-derived-dependencies/.openspec.yaml b/openspec/changes/connections-derived-dependencies/.openspec.yaml new file mode 100644 index 000000000..7f2ad572a --- /dev/null +++ b/openspec/changes/connections-derived-dependencies/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-27 diff --git a/openspec/changes/connections-derived-dependencies/design.md b/openspec/changes/connections-derived-dependencies/design.md new file mode 100644 index 000000000..7d6ad0fdf --- /dev/null +++ b/openspec/changes/connections-derived-dependencies/design.md @@ -0,0 +1,55 @@ +# Design: connections-derived-dependencies + +Read at development `49e65cb4`. + +## Context + +A connection lives between two applications (`connection.moduleA`, `moduleB`, `lib/Settings/softwarecatalogus_register.json:3689`, `:3705`). What an organisation runs is a usage (`usage` schema, `register.json:2656`) with `module`, `moduleVersion`, `koppelingen` (the connections this usage uses) and `plannedReplacement` (a successor application). `landscape-usage-registration` gives usages their own index and detail pages; this change adds a panel to that detail page. + +## D1. A suggestion service, computed on demand + +New `lib/Service/ConnectionSuggestionService.php` (ADR-008: controller, service, OpenRegister's ObjectService as the mapper). `suggestFor(string $usageUuid): array` does: + +1. Load the usage and its organisation (`consumer`). +2. Load the organisation's other usages and collect their applications: the organisation's landscape. +3. Load the connections where `moduleA` or `moduleB` is the usage's application, that the caller may read (OpenRegister RBAC stays on). +4. Keep the connections whose other end is in the landscape, or is a national provision, and that are not already in `usage.koppelingen` and not dismissed for this usage. +5. Return each with a reason: `shared-landscape`. + +Carry-over (`land-version-carry-connections`) adds a second source in the same method: + +- If another usage of the organisation has the same `module` and an older `moduleVersion`, or has this usage's `module` as its `plannedReplacement`, its `koppelingen` are offered with reason `carry-over`. +- For a successor, each offered connection is a draft: same other end, same type and direction, application A or B set to the successor, status `in development`, and `longDescription` naming the connection it came from. + +Rejected: storing suggestions as objects. They go stale the moment someone adds a usage; computing them on each open is cheap because every query is scoped to one organisation. + +## D2. Endpoints + +In `appinfo/routes.php`, next to the offer routes (:202-204): + +| verb | url | controller method | +|---|---|---| +| GET | `/api/usages/{uuid}/connection-suggestions` | `ConnectionSuggestionController::index` | +| POST | `/api/usages/{uuid}/connection-suggestions/accept` | `::accept` (body: suggestion ids) | +| POST | `/api/usages/{uuid}/connection-suggestions/dismiss` | `::dismiss` (body: suggestion ids) | + +All `#[NoAdminRequired]` with a per-object guard: the caller must be allowed to update the usage (the usage's `consumer` or a participant is the active organisation), the same rule `AanbodService` applies before it changes a usage. Accept appends existing connections to `usage.koppelingen`, and for a draft creates the connection first. Dismiss records the connection id in a new usage property `dismissedConnectionSuggestions` (array of uuid, `hideOnForm: true`). + +Rejected: reusing the offer endpoints (`/api/aanbod/{uuid}/accept`). An offer is an object a supplier made; a suggestion is derived and has no owner to accept from. + +## D3. The panel + +A custom component `ConnectionSuggestionsPanel` (`src/components/connections/ConnectionSuggestionsPanel.vue`), registered in `src/customComponents.js`, placed on the usage detail page as a body widget. It lists suggestions with the other application, type, direction and reason, and offers Accept, Dismiss and Accept all. Empty state: "No suggestions. Every known connection of this application is already in your usage." + +## Declarative versus imperative + +Imperative: deriving suggestions joins three queries and a rule, which no `x-openregister-*` block describes (ADR-031 allows code where the dialect has no construct). The accepted result is plain data on the usage. + +## Seed data + +The usage schema gains `dismissedConnectionSuggestions` (array, hidden on the form), added in `lib/Settings/register.d/derived-connections.json`. The demo register gets two usages of one organisation whose applications share a demo connection, so the panel shows one suggestion. + +## Risks + +- The per-object guard must match the usage's update rule exactly; `hydra-gate-no-admin-idor` checks each method has one. +- Draft connections created for a successor are real objects; a dismissed draft is never created, only an accepted one. diff --git a/openspec/changes/connections-derived-dependencies/proposal.md b/openspec/changes/connections-derived-dependencies/proposal.md new file mode 100644 index 000000000..ab45a63f9 --- /dev/null +++ b/openspec/changes/connections-derived-dependencies/proposal.md @@ -0,0 +1,44 @@ +--- +kind: code +depends_on: + - connections-catalogue-pages + - landscape-usage-registration +--- + +# Suggest connections from what the catalogue already knows + +## Summary + +When an organisation records that it uses an application, stackiq suggests the connections that application already has with other applications the organisation uses, so nobody draws each link by hand. When a usage is replaced by a newer version or by its planned successor, stackiq offers to carry the old usage's connections over. The organisation accepts or dismisses each suggestion. + +## Why + +Rows from the stackiq matrix: + +- `stackiq:conn-auto-populate-dependencies`, "Fill in an application's dependencies automatically from what is already known about connected items, instead of drawing each link by hand." Rated no. Feature request: https://github.com/glpi-project/roadmap/discussions/336. Two competitors rate yes: SAP LeanIX (https://help.sap.com/docs/leanix/ea/jira-service-management-integration, "Dependencies and relationships identified in Jira Service Management are automatically documented", and discovered flows that suggest missing interfaces) and BlueDolphin (https://help.bluedolphin.io/en/articles/11967645-use-datasource-to-create-relationships, "automatically create relationships between objects based on a loaded datasource"). Core area (connections). +- `stackiq:land-version-carry-connections`, "Carry an application's connections over automatically when a new version replaces the old one." Rated no. Feature request from the VNG user research: https://www.softwarecatalogus.nl/gebruikersonderzoek%202021. Core area (landscape). + +## What stackiq has today + +- A connection links applications, not versions: `connection.moduleA` and `moduleB` are `$ref module` (`lib/Settings/softwarecatalogus_register.json:3689`, `:3705`). Registering a new version never drops a catalogue connection. +- What an organisation runs is a usage: `usage.module`, `usage.moduleVersion` and `usage.koppelingen`, "the connections used within this usage" (usage schema, `register.json:2656`). `usage.plannedReplacement` names a successor application. +- A replacing usage starts with an empty `koppelingen`. Nothing copies connections from the usage it replaces, and nothing fills `koppelingen` from the connections the application already has. +- Suppliers can already offer usages and connections that a municipality accepts or denies (`lib/Service/AanbodService.php:289` acceptAanbod, `:438` denyAanbod; `appinfo/routes.php:203-204`). That flow handles one offered object; it does not derive anything. + +## What this change builds + +1. A suggestion service that, for one usage, lists the connections of its application whose other end is an application the same organisation uses, and that are not yet in the usage. +2. A carry-over suggestion: when a usage of the same application at a newer version, or of the old usage's planned successor, is created for the organisation, stackiq offers the old usage's connections. For a successor, it offers draft connections from the successor to the same other ends. +3. A Suggested connections panel on the usage page (from `landscape-usage-registration`) with Accept and Dismiss per suggestion and Accept all. +4. Dismissed suggestions stay dismissed for that usage. + +## Out of scope + +- Discovering connections from network traffic, logs or a service desk. The matrix category says stackiq is not a discovery agent. +- Suggestions pulled from outside systems through integriq (`sharing-itsm-exchange` covers the service desk exchange). +- The offer workflow for suppliers (`AanbodService`), which stays as it is. + +## Risks + +- A popular application has many connections. Suggestions only list connections whose other end the organisation actually uses, which keeps the list short. +- A draft connection for a successor may be wrong. It starts with status `in development` and names its origin, so the user reviews it. diff --git a/openspec/changes/connections-derived-dependencies/specs/derived-connections/spec.md b/openspec/changes/connections-derived-dependencies/specs/derived-connections/spec.md new file mode 100644 index 000000000..9f98b4bf4 --- /dev/null +++ b/openspec/changes/connections-derived-dependencies/specs/derived-connections/spec.md @@ -0,0 +1,61 @@ +# derived-connections specification + +**Status**: proposed +**Scope**: stackiq +**OpenSpec changes**: +- connections-derived-dependencies + +## Purpose + +An organisation's usages get their connections filled in from what the catalogue already knows, instead of by hand. Matrix rows `stackiq:conn-auto-populate-dependencies` and `stackiq:land-version-carry-connections`. + +## ADDED Requirements + +### Requirement: REQ-DCN-001 Stackiq suggests the known connections of an application inside the organisation's landscape + +For a usage, stackiq SHALL suggest every readable connection of the usage's application whose other end is an application the same organisation uses, or a national provision, and that is not yet in the usage's connections and was not dismissed for this usage. + +#### Scenario: A municipality gets its connections suggested +@e2e tests/e2e/workflows/connection-suggestions.spec.ts + +- **GIVEN** a published connection between application X and application Y, and a municipality that has a usage of Y +- **WHEN** the municipality's information manager records a usage of X and opens it +- **THEN** the Suggested connections panel lists the connection between X and Y +- **AND** after Accept the usage's connections include it + +#### Scenario: A connection outside the landscape is not suggested +@e2e exclude Rule-level case; tests/Unit/Service/ConnectionSuggestionServiceTest.php covers an other end the organisation does not use. + +- **GIVEN** a connection between X and Z, and the municipality does not use Z +- **WHEN** the information manager opens the usage of X +- **THEN** that connection is not suggested + +### Requirement: REQ-DCN-002 A replacing usage is offered the connections of the usage it replaces + +When an organisation has a usage of the same application at an older version, or a usage whose planned replacement is the new usage's application, stackiq SHALL suggest the older usage's connections for the new usage. For a successor application it SHALL suggest draft connections from the successor to the same other ends, with status `in development` and a description naming the original connection. + +#### Scenario: A new version keeps its connections +@e2e tests/e2e/workflows/connection-suggestions.spec.ts + +- **GIVEN** a usage of X at version 1 with connections to Y and to a national provision +- **WHEN** the information manager records a usage of X at version 2 +- **THEN** both connections are suggested with the reason that they carry over from version 1 + +#### Scenario: A successor gets draft connections +@e2e exclude Rule-level case; tests/Unit/Service/ConnectionSuggestionServiceTest.php covers the successor path. + +- **GIVEN** a usage of X whose planned replacement is Z, with a connection from X to Y +- **WHEN** the information manager records a usage of Z and accepts the suggestion +- **THEN** a connection from Z to Y exists with status `in development` + +### Requirement: REQ-DCN-003 Only the organisation that owns the usage accepts or dismisses its suggestions + +`POST /api/usages/{uuid}/connection-suggestions/accept` and `/dismiss` SHALL refuse with 403 a caller whose active organisation is neither the usage's consumer nor one of its participants, and SHALL change nothing then. + +#### Scenario: Another organisation cannot accept +@e2e exclude API guard; tests/Unit/Controller/ConnectionSuggestionControllerTest.php asserts the 403 and that no object was written. + +- **GIVEN** a usage of municipality A +- **WHEN** a user whose active organisation is municipality B posts accept for it +- **THEN** the answer is 403 +- **AND** the usage's connections are unchanged diff --git a/openspec/changes/connections-derived-dependencies/tasks.md b/openspec/changes/connections-derived-dependencies/tasks.md new file mode 100644 index 000000000..430cfefd0 --- /dev/null +++ b/openspec/changes/connections-derived-dependencies/tasks.md @@ -0,0 +1,53 @@ +# Tasks: connections-derived-dependencies + +## Implementation tasks + +### Task 1: Suggestion service +- **spec_ref**: openspec/changes/connections-derived-dependencies/specs/derived-connections/spec.md#requirement-req-dcn-001-stackiq-suggests-the-known-connections-of-an-application-inside-the-organisations-landscape +- **files**: `lib/Service/ConnectionSuggestionService.php`, `lib/Settings/register.d/derived-connections.json` +- **acceptance_criteria**: + - GIVEN applications X and Y share a connection and the organisation uses both WHEN suggestions are asked for its usage of X THEN the connection is suggested with reason shared-landscape + - GIVEN the organisation does not use Y WHEN suggestions are asked THEN that connection is not suggested +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Service/ConnectionSuggestionServiceTest.php` with an ObjectService double built on the real interface) + +### Task 2: Carry-over suggestions +- **spec_ref**: openspec/changes/connections-derived-dependencies/specs/derived-connections/spec.md#requirement-req-dcn-002-a-replacing-usage-is-offered-the-connections-of-the-usage-it-replaces +- **files**: `lib/Service/ConnectionSuggestionService.php` +- **acceptance_criteria**: + - GIVEN an old usage of X at version 1 with two connections WHEN a usage of X at version 2 is created THEN both connections are suggested with reason carry-over + - GIVEN an old usage whose planned replacement is Z WHEN a usage of Z is created THEN draft connections from Z to the same ends are suggested +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Service/ConnectionSuggestionServiceTest.php`, carry-over cases) + +### Task 3: Endpoints with a per-object guard +- **spec_ref**: openspec/changes/connections-derived-dependencies/specs/derived-connections/spec.md#requirement-req-dcn-003-only-the-organisation-that-owns-the-usage-accepts-or-dismisses-its-suggestions +- **files**: `lib/Controller/ConnectionSuggestionController.php`, `appinfo/routes.php`, `lib/AppInfo/Application.php` +- **acceptance_criteria**: + - GIVEN a user of another organisation WHEN they post accept for this usage THEN the answer is 403 and nothing changes + - GIVEN the owning organisation WHEN it accepts a suggestion THEN the connection is in usage.koppelingen +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Controller/ConnectionSuggestionControllerTest.php`; Newman collection `postman/` request for the three routes) + +### Task 4: Suggested connections panel +- **spec_ref**: openspec/changes/connections-derived-dependencies/specs/derived-connections/spec.md#requirement-req-dcn-001-stackiq-suggests-the-known-connections-of-an-application-inside-the-organisations-landscape +- **files**: `src/components/connections/ConnectionSuggestionsPanel.vue`, `src/customComponents.js`, the usage detail page in `src/manifest.d/`, `l10n/en.json`, `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN one suggestion WHEN the user clicks Accept THEN it leaves the panel and appears in the usage's connections + - GIVEN one suggestion WHEN the user clicks Dismiss and reloads THEN it stays gone +- [ ] Implement +- [ ] Test (Playwright `tests/e2e/workflows/connection-suggestions.spec.ts`) + +### Task 5: Documentation +- **spec_ref**: openspec/changes/connections-derived-dependencies/specs/derived-connections/spec.md#requirement-req-dcn-002-a-replacing-usage-is-offered-the-connections-of-the-usage-it-replaces +- **files**: `docs/features/connection-suggestions.md`, `docs/images/connection-suggestions.png` +- **acceptance_criteria**: + - GIVEN the docs site WHEN a reader opens Connection suggestions THEN both sources of suggestions are explained with a screenshot +- [ ] Implement +- [ ] Test (docs build, screenshot with Playwright) + +## Verification + +- `openspec validate connections-derived-dependencies --type change --strict` passes. +- `composer check:strict` and `npm run lint` pass; PHPUnit, Newman and Playwright cases above pass. +- English and Dutch strings for every new label (ADR-005); docs with a screenshot (ADR-010). From 7850227e22a5e7979aa877bb24c3d8e32df2a3c4 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:40:22 +0200 Subject: [PATCH 05/15] docs(openspec): landscape-application-page, correct keys, usages, contracts and opening from the list --- .../landscape-application-page/.openspec.yaml | 2 + .../landscape-application-page/design.md | 54 ++++++++++++++++ .../landscape-application-page/proposal.md | 48 ++++++++++++++ .../specs/application-page/spec.md | 64 +++++++++++++++++++ .../landscape-application-page/tasks.md | 44 +++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 openspec/changes/landscape-application-page/.openspec.yaml create mode 100644 openspec/changes/landscape-application-page/design.md create mode 100644 openspec/changes/landscape-application-page/proposal.md create mode 100644 openspec/changes/landscape-application-page/specs/application-page/spec.md create mode 100644 openspec/changes/landscape-application-page/tasks.md diff --git a/openspec/changes/landscape-application-page/.openspec.yaml b/openspec/changes/landscape-application-page/.openspec.yaml new file mode 100644 index 000000000..7f2ad572a --- /dev/null +++ b/openspec/changes/landscape-application-page/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-27 diff --git a/openspec/changes/landscape-application-page/design.md b/openspec/changes/landscape-application-page/design.md new file mode 100644 index 000000000..84b8e5377 --- /dev/null +++ b/openspec/changes/landscape-application-page/design.md @@ -0,0 +1,54 @@ +# Design: landscape-application-page + +Read at development `49e65cb4`, `@conduction/nextcloud-vue` 2.57.1. + +## Context + +`ModuleDetail` is a manifest detail page (`src/manifest.json:491`) with a grid of widgets (:500-511) and body widgets (:514). The Applications list is a custom page, `FacetedCatalogIndexView` (`src/manifest.json` page `Modules`, component at `src/views/FacetedCatalogIndexView.vue`), which wraps a standalone `CnIndexPage` beside a GEMMA facet sidebar. + +## D1. Field keys + +In `src/manifest.json`: + +- `md-data.content.include` (:500): `beschrijvingKort` becomes `shortDescription`, `beschrijvingLang` becomes `longDescription`, `contactpersoon` becomes `contactPerson`. +- `suite-data.content.include` (:683): the same three renames. +- `md-compliance.content.columns` (:503): `bioMaatregel` becomes `bioMeasure`. + +`tests/vitest/manifestFilterEnumParity.spec.js` already checks enum filters against the schema; a new `tests/vitest/manifestIncludeKeys.spec.js` asserts every `include` key and every `object-list` column key of a detail page exists on its schema, so a rename cannot leave a page blank again. + +## D2. Usages list + +A new `object-list` widget `md-usages`: register `@resolve:voorzieningen_register`, schema `usage`, filter `{ "module": "@objectId" }`, columns consumer, moduleVersion, status. The `usage` read rule (`register.json:2656` schema) already scopes rows: a municipality sees its own usages, a supplier sees usages of its products. No row route until `landscape-usage-registration` adds the usage detail page; that change sets `rowRoute`. + +## D3. Contracts list + +`catalogContract` reaches an application in two ways: through `usage` (a usage of the application) or through `service` (a service whose `modules` include the application, `catalogService.modules`). A single `object-list` filter cannot follow a hop (`CnObjectListWidget.vue:503`, one filter object). + +A small custom widget `ApplicationContractsWidget` (`src/components/contracts/ApplicationContractsWidget.vue`, registered in `src/customComponents.js`, placed as a grid widget) does it with the object store: + +1. Fetch the application's usages (`usage`, `module` equals the id) and the services that offer it (`catalogService`, `modules` contains the id). +2. Fetch `catalogContract` with `usage` in the usage ids, and with `service` in the service ids, and merge by id. +3. Render the rows with the library's `CnObjectRow`, columns contract number, type, end date and status, each opening `ContractDetail`. + +Rejected: a denormalised `module` field on `catalogContract`. It would go stale when a usage or service changes its application, and needs a save hook to fill it. + +## D4. Open from the Applications list + +`FacetedCatalogIndexView.vue` gains a `detailRoute` prop and binds `@row-click` on its `CnIndexPage` (:108) to `$router.push({ name: detailRoute, params: { id } })`. The `Modules` page config passes `detailRoute: "ModuleDetail"`; the `Diensten` page passes none, and its rows stay as they are. `CnIndexPage` emits `row-click` for register and schema pages (`CnIndexPage.vue:5500` onwards); only a named source routes by itself. + +## D5. Layout + +The grid becomes: data 8 wide with files and related at the right, then versions and usages side by side, then contracts and compliance side by side. Body widgets (reviews) stay at the end. The layout follows ADR-062 (detail page grid discipline). + +## Declarative versus imperative + +D1, D2 and D5 are manifest edits. D3 is one custom widget because the relation is two hops; it reads through the object store and adds no endpoint (ADR-022). + +## Seed data + +No schema change. + +## Risks + +- `connections-catalogue-pages` and `connections-api-catalogue` add widgets to the same page; they append below this layout. +- The contract widget fires three queries per page view; each is scoped by ids and paged. diff --git a/openspec/changes/landscape-application-page/proposal.md b/openspec/changes/landscape-application-page/proposal.md new file mode 100644 index 000000000..aced1c8da --- /dev/null +++ b/openspec/changes/landscape-application-page/proposal.md @@ -0,0 +1,48 @@ +--- +kind: code +depends_on: [] +--- + +# One application page with versions, usages, contracts and compliance + +## Summary + +The application page becomes the one place to read an application: its data with the right field names, the supplier's contact person, its versions, the organisations' usages, the contracts behind it and its compliance claims. It also opens from the Applications list, which it does not today. + +## Why + +Rows from the stackiq matrix: + +- `stackiq:land-detail-page`, "Open one application and see its versions, usages, contracts and compliance on one page." Rated partial, built. Two competitors rate yes: SAP LeanIX (https://help.sap.com/docs/leanix/ea/application-modeling-guidelines, the application fact sheet with relations and cost on one page) and BlueDolphin (https://help.bluedolphin.io/en/articles/11967521-object-viewer, "shows all the object properties ... relationships, history"). The missing half: contracts on the page, opening it from the Applications list, and the stale field keys. Core area (landscape). +- `stackiq:ctr-per-application`, "See the contracts behind an application from that application's page." Rated no. Two competitors rate yes: SAP LeanIX (https://help.sap.com/docs/leanix/ea/contract-extension-to-meta-model, "Attached to Contract - Application Many-to-Many") and GLPI (source read at 11.0.9, `src/Appliance.php:99` adds the Contracts tab listing every contract of the application). +- `stackiq:mkt-contacts-per-product`, "Name different contact persons per product a supplier offers." Rated partial, built, no competitor yes and no demand. It rides with `land-detail-page`: its missing half is the stale `contactpersoon` key on the application page, which this change corrects. + +No tender, feature request or roadmap row names these rows. + +## What stackiq has today + +- `ModuleDetail` (`src/manifest.json:491`) has a data widget, files, a generic related panel, compliance claims and versions (:500-504), and a reviews panel. +- The data widget includes `beschrijvingKort`, `beschrijvingLang` and `contactpersoon` (:500). The module schema renamed them to `shortDescription`, `longDescription` and `contactPerson` (`lib/Settings/softwarecatalogus_register.json:6779` schema), so the descriptions and the contact person do not render. SuiteDetail carries the same stale keys (:683). +- The compliance list's column `bioMaatregel` (:503) names a key the `compliancy` schema calls `bioMeasure`, so that column stays empty. +- Usages show only as untyped entries in the related panel (:502). +- No contract list: `catalogContract` points at a `service` and a `usage` (`register.json:3252` schema), not at the application, so a one-hop list cannot reach it. +- The Applications list (`src/views/FacetedCatalogIndexView.vue:108`) renders `CnIndexPage` with no `@row-click` handler, so clicking a row or View does nothing. The page opens only from an organisation's list (`src/manifest.json:417`, `rowRoute: ModuleDetail`). + +## What this change builds + +1. Correct field keys on ModuleDetail and SuiteDetail, and the compliance column key. +2. A Usages list on the application page, filtered on `usage.module`. +3. A Contracts list on the application page: contracts whose usage is a usage of this application, or whose service offers this application. +4. Opening the application page from the Applications list, by row click and by the View action. + +## Out of scope + +- Registering a usage and its status on a usage page: `landscape-usage-registration`. +- Business and technical owners: `landscape-usage-registration`. +- Connections and APIs on the page: `connections-catalogue-pages`, `connections-api-catalogue`. +- A detail page for services (the Services list has none today); no matrix row asks for it in this pass. + +## Risks + +- Three changes in this pass add widgets to `ModuleDetail`. This one reorders the grid first; the others stack below. +- A contract visible through a service may belong to another organisation. The list shows only contracts the user may read under the `catalogContract` read rule. diff --git a/openspec/changes/landscape-application-page/specs/application-page/spec.md b/openspec/changes/landscape-application-page/specs/application-page/spec.md new file mode 100644 index 000000000..a965256aa --- /dev/null +++ b/openspec/changes/landscape-application-page/specs/application-page/spec.md @@ -0,0 +1,64 @@ +# application-page specification + +**Status**: proposed +**Scope**: stackiq +**OpenSpec changes**: +- landscape-application-page + +## Purpose + +One page per application shows its data, contact person, versions, usages, contracts and compliance, and opens from the Applications list. Matrix rows `stackiq:land-detail-page`, `stackiq:ctr-per-application` and `stackiq:mkt-contacts-per-product`. + +## ADDED Requirements + +### Requirement: REQ-APG-001 The application page shows every field it lists under the schema's current keys + +The application page `ModuleDetail` and the suite page `SuiteDetail` SHALL list only keys that exist on their schema, so the short description, long description and the supplier's contact person of a product render. The compliance list SHALL show the BIO measure of each claim. + +#### Scenario: A buyer reads a product's contact person +@e2e tests/e2e/workflows/application-page.spec.ts + +- **GIVEN** a supplier registered product X with a short description and contact person Anna +- **WHEN** a municipal buyer opens the page of product X +- **THEN** the data widget shows the short description and contact person Anna + +#### Scenario: A stale key cannot ship again +@e2e exclude Build-time guard; tests/vitest/manifestIncludeKeys.spec.js fails when a detail page lists a key its schema lacks. + +- **GIVEN** a detail page whose data widget lists a key the schema does not have +- **WHEN** the vitest suite runs +- **THEN** the test fails and names the page and the key + +### Requirement: REQ-APG-002 The application page lists the usages of the application + +The application page SHALL list the usages of the application the user may read, with the using organisation, the version and the status. + +#### Scenario: A supplier sees which organisations use its product +@e2e tests/e2e/workflows/application-page.spec.ts + +- **GIVEN** two municipalities have a usage of product X +- **WHEN** the supplier of X opens its page +- **THEN** the Usages list shows both usages with version and status + +### Requirement: REQ-APG-003 The application page lists the contracts behind the application + +The application page SHALL list every readable contract whose usage is a usage of the application, or whose service offers the application, once each, with contract number, type, end date and status, and each row SHALL open the contract page. + +#### Scenario: An information manager finds the contract behind an application +@e2e tests/e2e/workflows/application-page.spec.ts + +- **GIVEN** the municipality has a usage of application X and a contract on that usage +- **WHEN** the information manager opens the page of X +- **THEN** the Contracts list shows that contract with its end date +- **AND** clicking it opens the contract page + +### Requirement: REQ-APG-004 The Applications list opens the application page + +Clicking a row, or its View action, on the Applications list SHALL open that application's page. + +#### Scenario: A user opens an application from the list +@e2e tests/e2e/workflows/application-page.spec.ts + +- **GIVEN** the Applications list at `/modules` +- **WHEN** the user clicks the row of application X +- **THEN** the page `/modules/` opens diff --git a/openspec/changes/landscape-application-page/tasks.md b/openspec/changes/landscape-application-page/tasks.md new file mode 100644 index 000000000..675aa4c05 --- /dev/null +++ b/openspec/changes/landscape-application-page/tasks.md @@ -0,0 +1,44 @@ +# Tasks: landscape-application-page + +## Implementation tasks + +### Task 1: Correct field keys and guard them +- **spec_ref**: openspec/changes/landscape-application-page/specs/application-page/spec.md#requirement-req-apg-001-the-application-page-shows-every-field-it-lists-under-the-schemas-current-keys +- **files**: `src/manifest.json` (ModuleDetail, SuiteDetail), `tests/vitest/manifestIncludeKeys.spec.js` +- **acceptance_criteria**: + - GIVEN an application with a short description and a contact person WHEN its page opens THEN both show in the data widget + - GIVEN a detail page include key that is not on its schema WHEN vitest runs THEN the test fails +- [ ] Implement +- [ ] Test (vitest `tests/vitest/manifestIncludeKeys.spec.js`) + +### Task 2: Usages and contracts on the application page +- **spec_ref**: openspec/changes/landscape-application-page/specs/application-page/spec.md#requirement-req-apg-003-the-application-page-lists-the-contracts-behind-the-application +- **files**: `src/manifest.json` (ModuleDetail widgets and layout), `src/components/contracts/ApplicationContractsWidget.vue`, `src/customComponents.js`, `l10n/en.json`, `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN a contract on a usage of application X WHEN the page of X opens THEN the Contracts list shows it + - GIVEN a contract on a service that offers X WHEN the page of X opens THEN the Contracts list shows it once +- [ ] Implement +- [ ] Test (vitest `tests/vitest/applicationContracts.spec.js` for the merge; Playwright `tests/e2e/workflows/application-page.spec.ts`) + +### Task 3: Open the page from the Applications list +- **spec_ref**: openspec/changes/landscape-application-page/specs/application-page/spec.md#requirement-req-apg-004-the-applications-list-opens-the-application-page +- **files**: `src/views/FacetedCatalogIndexView.vue`, `src/manifest.json` (Modules page config) +- **acceptance_criteria**: + - GIVEN the Applications list WHEN the user clicks a row THEN ModuleDetail of that application opens + - GIVEN the Services list WHEN the user clicks a row THEN nothing navigates away +- [ ] Implement +- [ ] Test (Playwright `tests/e2e/workflows/application-page.spec.ts`) + +### Task 4: Documentation +- **spec_ref**: openspec/changes/landscape-application-page/specs/application-page/spec.md#requirement-req-apg-002-the-application-page-lists-the-usages-of-the-application +- **files**: `docs/features/application-page.md`, `docs/images/application-page.png` +- **acceptance_criteria**: + - GIVEN the docs site WHEN a reader opens the application page article THEN every section of the page is explained with a screenshot +- [ ] Implement +- [ ] Test (docs build, screenshot with Playwright) + +## Verification + +- `openspec validate landscape-application-page --type change --strict` passes. +- `npm run lint` passes; the vitest and Playwright cases above pass. +- English and Dutch strings for every new label (ADR-005); docs with a screenshot (ADR-010). From 2e19d751f00d5296ed8258cb909b69c5d1e24bc8 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:41:58 +0200 Subject: [PATCH 06/15] docs(openspec): landscape-usage-registration, usage pages, owners and a working usage lifecycle --- .../connections-catalogue-pages/design.md | 2 +- .../connections-catalogue-pages/proposal.md | 2 +- .../.openspec.yaml | 2 + .../landscape-usage-registration/design.md | 57 +++++++++++++++++ .../landscape-usage-registration/proposal.md | 45 +++++++++++++ .../specs/application-usage-pages/spec.md | 64 +++++++++++++++++++ .../landscape-usage-registration/tasks.md | 43 +++++++++++++ 7 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 openspec/changes/landscape-usage-registration/.openspec.yaml create mode 100644 openspec/changes/landscape-usage-registration/design.md create mode 100644 openspec/changes/landscape-usage-registration/proposal.md create mode 100644 openspec/changes/landscape-usage-registration/specs/application-usage-pages/spec.md create mode 100644 openspec/changes/landscape-usage-registration/tasks.md diff --git a/openspec/changes/connections-catalogue-pages/design.md b/openspec/changes/connections-catalogue-pages/design.md index 261ea9e59..749c118f5 100644 --- a/openspec/changes/connections-catalogue-pages/design.md +++ b/openspec/changes/connections-catalogue-pages/design.md @@ -32,7 +32,7 @@ Rejected: a custom widget that calls `GET /api/koppelingen-gebruik/{uuid}` (`Aan All in `lib/Settings/softwarecatalogus_register.json`, schema `connection`: -1. **Lifecycle states.** Replace the Dutch states in `x-openregister-lifecycle` (:3926) with the enum values: initial `in development`, final `withdrawn`, transitions release (`in development` to `in use`), sunset (`in use` to `end of support`), withdraw (`in use`, `end of support` to `withdrawn`). The rows already hold the English values (`lib/Repair/RenameDutchCatalogValues.php:87-88`). +1. **Lifecycle states.** Replace the Dutch states in `x-openregister-lifecycle` (:3926) with the enum values: initial `in development`, final `withdrawn`, transitions release (`in development` to `in use`), sunset (`in use` to `end of support`), withdraw (`in use`, `end of support` to `withdrawn`). The rows already hold the English values (`lib/Repair/RenameDutchCatalogValues.php:87-90`). 2. **Picker.** Change `objectConfiguration.queryParams` on `nonMunicipalProvision` (:3720) to `gemmaType=Buitengemeentelijke voorziening`, the spelling the GEMMA model uses. 3. **Name template.** `objectNameField` names `gegevensuitwisselingRichting` and `buitengemeentelijkVoorziening`, keys the schema renamed to `dataExchangeDirection` and `nonMunicipalProvision`, and maps `AnaarB`, `BnaarA`, `bi-directioneel` where the enum holds `AtoB`, `BtoA`, `bi-directional`. Rewrite it on the current keys and values, so a connection reads "Application A to Application B" in lists and pickers. 4. **Facets.** Set `facetable: true` on `type`, `status` and `dataExchangeDirection`, so the index page can count and filter them. diff --git a/openspec/changes/connections-catalogue-pages/proposal.md b/openspec/changes/connections-catalogue-pages/proposal.md index 11c63bb0b..248e72298 100644 --- a/openspec/changes/connections-catalogue-pages/proposal.md +++ b/openspec/changes/connections-catalogue-pages/proposal.md @@ -27,7 +27,7 @@ No tender, feature request or roadmap row names these rows. `conn-list-page` and - The application page `ModuleDetail` (`src/manifest.json:491`) shows connections only as untyped entries in the generic related panel `md-related` (:502). - `GET /api/koppelingen-gebruik/{uuid}` (`appinfo/routes.php:269`, `lib/Controller/AangebodenGebruikController.php:208`) returns an application's connections and usages. Nothing in `src/` calls it. - Two register defects would break the pages even once they exist: - - The connection lifecycle (`register.json:3926`) names `in ontwikkeling`, `in gebruik`, `einde ondersteuning` and `teruggetrokken`. The status enum and the migrated rows (`lib/Repair/RenameDutchCatalogValues.php:87-88`) hold `in development`, `in use`, `end of support` and `withdrawn`, so no transition matches any row. + - The connection lifecycle (`register.json:3926`) names `in ontwikkeling`, `in gebruik`, `einde ondersteuning` and `teruggetrokken`. The status enum and the migrated rows (`lib/Repair/RenameDutchCatalogValues.php:87-90`) hold `in development`, `in use`, `end of support` and `withdrawn`, so no transition matches any row. - The `nonMunicipalProvision` picker filters on `gemmaType=Buitengemeentenlijke voorziening` (`register.json:3720`). The GEMMA model spells it `Buitengemeentelijke voorziening` (59 times in `lib/Settings/GEMMA_release.xml`), so the picker finds nothing. ## What this change builds diff --git a/openspec/changes/landscape-usage-registration/.openspec.yaml b/openspec/changes/landscape-usage-registration/.openspec.yaml new file mode 100644 index 000000000..7f2ad572a --- /dev/null +++ b/openspec/changes/landscape-usage-registration/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-27 diff --git a/openspec/changes/landscape-usage-registration/design.md b/openspec/changes/landscape-usage-registration/design.md new file mode 100644 index 000000000..e3692eb9e --- /dev/null +++ b/openspec/changes/landscape-usage-registration/design.md @@ -0,0 +1,57 @@ +# Design: landscape-usage-registration + +Read at development `49e65cb4`. + +## Context + +A usage (`gebruik`) is an organisation's use of an application: `consumer` (the organisation), `module`, `moduleVersion`, `status`, phase dates, connections and replacement (`usage` schema, `lib/Settings/softwarecatalogus_register.json:2656`). Its read rule shows a usage to the using organisation (`consumer` or `_organisation` equals the active organisation) and to the supplier (`provider`). Everything the portfolio views compute (`src/views/LifecycleRoadmapView.vue:397`, `lib/Service/PortfolioReportService.php`) starts from usages, but no page creates them. + +## D1. Pages in a fragment + +`src/manifest.d/usages.json` (ADR-037): + +- `Gebruik`, route `/gebruik`, `type: index`, schema `usage`. Title "Applications in use". Columns: module, moduleVersion, status, businessOwner, technicalOwner, timeClassification. Quick filters on status. `filterMenu: true`. +- `GebruikDetail`, route `/gebruik/:id`, `type: detail`. Widgets: data (application, version, status, phase dates, owners, cloud model, annotation), files (the schema has `allowFiles` and tags DPIA, Contract, Verwerkingsovereenkomst), related, and a History tab. `lifecycleActions` on. +- Menu child "Applications in use" under the `Modules` entry. No new top-level entry (ADR-097). + +`landscape-application-page` added a usages list to `ModuleDetail` without a row route; this change sets its `rowRoute: GebruikDetail`. `OrganisatieDetail` (`src/manifest.json:403`) gets an `object-list` `org-usages` with filter `{ "consumer": "@objectId" }`, next to `org-modules` (:417), which lists what an organisation offers. + +## D2. "Add to our landscape" + +`ModuleDetail` gets a header action "Add to our landscape" that opens the library's create form for `usage` with `module` set to the page's object and `consumer` set to the active organisation. The action shows only when the user may create a usage. It mirrors the GEMMA Softwarecatalogus "+" behind a package (the row's evidence). + +Rejected: a wizard. The usage form has five fields a user must decide on; a dialog is enough, and `CnFormDialog` already renders the schema. + +## D3. Owners + +Two new properties on `usage`, added through `lib/Settings/register.d/usage-owners.json`: + +| property | type | notes | +|---|---|---| +| `businessOwner` | `$ref contactPerson` | `x-relation-filter: { "organization": "@object.consumer" }` | +| `technicalOwner` | `$ref contactPerson` | same filter | + +The existing hidden `contactPerson` stays as it is. The contact person read rule (`register.json:1788` schema) scopes a supplier to its own organisation's contact persons, so a supplier reading a usage of its product sees an owner reference it cannot open. + +Rejected: owner fields on `module`. A module is the supplier's product; the business owner is a person of the organisation that uses it, and two municipalities using one product have two owners. + +## D4. Register fixes + +In `lib/Settings/softwarecatalogus_register.json`, schema `usage`: + +1. `x-openregister-lifecycle` on the enum values: initial `Acquisition`, final `Phased out`, transitions plan (Acquisition to Planned), goLive (Planned to In production), phaseOut (In production to To be phased out), retire (To be phased out to Phased out). The rows already hold these (`lib/Repair/RenameDutchCatalogValues.php:80-84`). +2. `objectNameField` becomes `{{ module }} ({{ consumer }})`. +3. The schema version goes to 1.5.1 with a register changelog line, for the reason the 2.4.4 entry records (`register.json:7`). + +## Declarative versus imperative + +Declarative only: pages, relations, lifecycle and a header action that opens the library form (ADR-031). No PHP. + +## Seed data + +`lib/Settings/stackiq_mock_register.json`: two demo usages of demo applications by the demo municipality, one In production with a version and both owners, one Planned. + +## Risks + +- The default status stays In production, which the lifecycle treats as a valid state. A usage created as In production starts there; the lifecycle's initial state only applies when no status is sent. +- `ModuleDetail` gains a header action and a row route; `landscape-application-page` lands first. diff --git a/openspec/changes/landscape-usage-registration/proposal.md b/openspec/changes/landscape-usage-registration/proposal.md new file mode 100644 index 000000000..1d9d6e59e --- /dev/null +++ b/openspec/changes/landscape-usage-registration/proposal.md @@ -0,0 +1,45 @@ +--- +kind: code +depends_on: + - landscape-application-page +--- + +# Record the applications your organisation uses, with status, version and owners + +## Summary + +An information manager records that the organisation uses an application: which version it runs, where it stands in its lifecycle, and who owns it on the business side and the technical side. Today this record (a usage, `gebruik`) exists in the data but no stackiq page creates or edits it. This change adds the pages, fixes the usage lifecycle so its transitions work, and adds the two owner fields. + +## Why + +Rows from the stackiq matrix: + +- `stackiq:land-register-application`, "Register an application your organisation uses, with its supplier, description and status." Rated partial, built. Four competitors rate yes, among them GEMMA Softwarecatalogus (https://www.softwarecatalogus.nl/node/30355, "klik dan op de knop + achter de beschrijving van het pakket om het pakket toe te voegen aan je omgeving ... Vul onder Planning bij Status in gebruik in"), SAP LeanIX (https://help.sap.com/docs/leanix/ea/application-modeling-guidelines), BlueDolphin (https://help.bluedolphin.io/en/articles/11967529-welcome-to-the-objects) and GLPI (source read at 11.0.9, `src/Appliance.php:350` search option Status). The missing half: a lifecycle status for the application an organisation uses, set on a usage page. Core area (landscape). +- `stackiq:life-version-in-use`, "Record which version of an application your organisation currently runs." Rated partial, built. Two competitors rate yes: GEMMA Softwarecatalogus (https://www.softwarecatalogus.nl/node/30355, "Pakketversie - selecteer de versie die in gebruik is") and GLPI (source read at 11.0.9, `src/Item_SoftwareVersion.php:39`). The missing half: a usage page where the organisation sets the version. +- `stackiq:land-application-owner`, "Name the business owner and the technical owner responsible for an application." Rated partial, built. Two competitors rate yes: SAP LeanIX (https://help.sap.com/docs/leanix/ea/subscription-roles, "roles that map to your organization's positions, such as application owner") and GLPI (source read at 11.0.9, `glpi_appliances` holds `users_id` and `users_id_tech`, `src/Appliance.php:186` and `:240`). The missing half: a separate business owner and technical owner for the organisation that uses the application, on its usage. Core area. + +## What stackiq has today + +- The `usage` schema (`lib/Settings/softwarecatalogus_register.json:2656`, version 1.5.0) holds `consumer`, `module`, `moduleVersion` (filtered to the module's versions), `status` (Acquisition, Planned, In production, To be phased out, Phased out; default In production), five phase dates, `contactPerson` (hidden), `koppelingen`, `plannedReplacement` and the TIME classification. +- No manifest page uses the schema. `LifecycleRoadmapView.vue:397` and the portfolio report read usages, and the EOL badges depend on `usage.moduleVersion`, but nobody can set it in stackiq. +- The usage lifecycle names Verwerving, Gepland, In productie, Uit te faseren and Uitgefaseerd, while the enum and the migrated rows (`lib/Repair/RenameDutchCatalogValues.php:80-84`) hold the English values, so no transition matches a row. +- `usage.objectNameField` is `consumer`, so every usage of one organisation carries the same name in lists and pickers. + +## What this change builds + +1. A usage index page (`Gebruik`, `/gebruik`, "Applications in use") and a usage detail page, under Applications in the menu. +2. An "Add to our landscape" action on the application page that opens the usage form with the application and the active organisation filled in. +3. Business owner and technical owner fields on the usage, picked from the organisation's contact persons. +4. The usage lifecycle on the enum values, so Plan, Go live, Phase out and Retire work from the detail page. +5. A usage name built from the application and the organisation. +6. An "Applications in use" list on the organisation page. + +## Out of scope + +- Filling phase dates when the status changes (`stackiq:life-dates-follow-status`, deferred: feature request without a competitor yes). +- Suggested connections for a usage: `connections-derived-dependencies`. +- A status on the product itself: a product's own status is its versions' status (`moduleVersion.status`). + +## Risks + +- Suppliers can read usages of their products (`provider` read rule). The owner fields name people of the using organisation; the design keeps them readable only for that organisation. diff --git a/openspec/changes/landscape-usage-registration/specs/application-usage-pages/spec.md b/openspec/changes/landscape-usage-registration/specs/application-usage-pages/spec.md new file mode 100644 index 000000000..37b71c2e5 --- /dev/null +++ b/openspec/changes/landscape-usage-registration/specs/application-usage-pages/spec.md @@ -0,0 +1,64 @@ +# application-usage-pages specification + +**Status**: proposed +**Scope**: stackiq +**OpenSpec changes**: +- landscape-usage-registration + +## Purpose + +An organisation records the applications it uses, with the version it runs, its lifecycle status and its owners. Matrix rows `stackiq:land-register-application`, `stackiq:life-version-in-use` and `stackiq:land-application-owner`. + +## ADDED Requirements + +### Requirement: REQ-UAP-001 An organisation records and browses the applications it uses + +Stackiq SHALL offer a page "Applications in use" at `/gebruik` over the `usage` schema that lists the usages the user may read, with application, version, status and owners, and a detail page at `/gebruik/:id` where the user edits them. The organisation page SHALL list the organisation's usages. + +#### Scenario: An information manager lists the organisation's applications +@e2e tests/e2e/workflows/usages.spec.ts + +- **GIVEN** the municipality uses application X at version 2.1 in production and application Y as planned +- **WHEN** its information manager opens Applications, then Applications in use +- **THEN** the list shows X with version 2.1 and status In production, and Y with status Planned + +### Requirement: REQ-UAP-002 An organisation adds an application to its landscape from the application page + +The application page SHALL offer "Add to our landscape" to a user who may create a usage. It SHALL open the usage form with the application and the user's active organisation filled in, and the version picker SHALL offer only versions of that application. + +#### Scenario: Adding an application with its version +@e2e tests/e2e/workflows/usages.spec.ts + +- **GIVEN** application X has versions 2.0 and 2.1 +- **WHEN** the information manager opens the page of X, clicks Add to our landscape, picks version 2.1 and saves +- **THEN** a usage of X by their municipality with version 2.1 exists +- **AND** it shows on Applications in use + +### Requirement: REQ-UAP-003 A usage names a business owner and a technical owner + +A usage SHALL carry a business owner and a technical owner, each picked from the contact persons of the using organisation. + +#### Scenario: Setting both owners +@e2e tests/e2e/workflows/usages.spec.ts + +- **GIVEN** the municipality has contact persons Anna and Bram +- **WHEN** the information manager edits its usage of X and sets business owner Anna and technical owner Bram +- **THEN** the usage page shows Anna as business owner and Bram as technical owner + +#### Scenario: A supplier cannot open the owners +@e2e exclude Read rule of the contact person schema; tests/Unit/Settings/SchemaRbacTest.php asserts a supplier reads only its own organisation's contact persons. + +- **GIVEN** a usage of the supplier's product with both owners set +- **WHEN** the supplier opens that usage +- **THEN** the owner contact persons do not open for the supplier + +### Requirement: REQ-UAP-004 A usage moves through its lifecycle from its page + +The usage schema SHALL declare its lifecycle on the status values its rows hold, so the detail page offers Plan, Go live, Phase out and Retire from the matching status. + +#### Scenario: Going live +@e2e tests/e2e/workflows/usages.spec.ts + +- **GIVEN** a usage with status Planned +- **WHEN** the information manager opens it and clicks Go live +- **THEN** its status reads In production diff --git a/openspec/changes/landscape-usage-registration/tasks.md b/openspec/changes/landscape-usage-registration/tasks.md new file mode 100644 index 000000000..532ad2186 --- /dev/null +++ b/openspec/changes/landscape-usage-registration/tasks.md @@ -0,0 +1,43 @@ +# Tasks: landscape-usage-registration + +## Implementation tasks + +### Task 1: Usage schema fixes and owner fields +- **spec_ref**: openspec/changes/landscape-usage-registration/specs/application-usage-pages/spec.md#requirement-req-uap-003-a-usage-names-a-business-owner-and-a-technical-owner +- **files**: `lib/Settings/softwarecatalogus_register.json`, `lib/Settings/register.d/usage-owners.json`, `lib/Settings/stackiq_mock_register.json` +- **acceptance_criteria**: + - GIVEN the merged register WHEN a usage in Planned is opened THEN Go live is offered + - GIVEN a usage WHEN its business owner field opens THEN it lists contact persons of the consumer organisation only +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Settings/UsageSchemaTest.php`: lifecycle states are enum values, owner filters, name template keys exist) + +### Task 2: Usage index and detail pages +- **spec_ref**: openspec/changes/landscape-usage-registration/specs/application-usage-pages/spec.md#requirement-req-uap-001-an-organisation-records-and-browses-the-applications-it-uses +- **files**: `src/manifest.d/usages.json`, `src/manifest.json` (ModuleDetail row route, OrganisatieDetail list), `l10n/en.json`, `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN two usages of the organisation WHEN the user opens Applications in use THEN both show with version and status + - GIVEN a usage row WHEN the user opens it THEN the detail page shows version, status and owners +- [ ] Implement +- [ ] Test (Playwright `tests/e2e/workflows/usages.spec.ts`) + +### Task 3: Add to our landscape +- **spec_ref**: openspec/changes/landscape-usage-registration/specs/application-usage-pages/spec.md#requirement-req-uap-002-an-organisation-adds-an-application-to-its-landscape-from-the-application-page +- **files**: `src/manifest.json` (ModuleDetail header action), `src/customComponents.js` if the action needs a handler +- **acceptance_criteria**: + - GIVEN application X WHEN an information manager clicks Add to our landscape and saves version 2.1 THEN a usage of X by their organisation exists with version 2.1 +- [ ] Implement +- [ ] Test (Playwright `tests/e2e/workflows/usages.spec.ts`, add case) + +### Task 4: Documentation +- **spec_ref**: openspec/changes/landscape-usage-registration/specs/application-usage-pages/spec.md#requirement-req-uap-004-a-usage-moves-through-its-lifecycle-from-its-page +- **files**: `docs/features/applications-in-use.md`, `docs/images/applications-in-use.png` +- **acceptance_criteria**: + - GIVEN the docs site WHEN a reader opens Applications in use THEN adding, the lifecycle and the owners are explained with a screenshot +- [ ] Implement +- [ ] Test (docs build, screenshot with Playwright) + +## Verification + +- `openspec validate landscape-usage-registration --type change --strict` passes. +- `composer check:strict` and `npm run lint` pass; the PHPUnit and Playwright cases above pass. +- English and Dutch strings for every new label (ADR-005); docs with a screenshot (ADR-010). From 40505d7d2fc22bb0bfe185876027e83f9b7ca585 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:46:19 +0200 Subject: [PATCH 07/15] docs(openspec): landscape-application-components, partOf relation and a components list --- .../.openspec.yaml | 2 + .../design.md | 38 +++++++++++++++++ .../proposal.md | 40 ++++++++++++++++++ .../specs/application-components/spec.md | 42 +++++++++++++++++++ .../landscape-application-components/tasks.md | 35 ++++++++++++++++ 5 files changed, 157 insertions(+) create mode 100644 openspec/changes/landscape-application-components/.openspec.yaml create mode 100644 openspec/changes/landscape-application-components/design.md create mode 100644 openspec/changes/landscape-application-components/proposal.md create mode 100644 openspec/changes/landscape-application-components/specs/application-components/spec.md create mode 100644 openspec/changes/landscape-application-components/tasks.md diff --git a/openspec/changes/landscape-application-components/.openspec.yaml b/openspec/changes/landscape-application-components/.openspec.yaml new file mode 100644 index 000000000..7f2ad572a --- /dev/null +++ b/openspec/changes/landscape-application-components/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-27 diff --git a/openspec/changes/landscape-application-components/design.md b/openspec/changes/landscape-application-components/design.md new file mode 100644 index 000000000..fa0942a16 --- /dev/null +++ b/openspec/changes/landscape-application-components/design.md @@ -0,0 +1,38 @@ +# Design: landscape-application-components + +Read at development `49e65cb4`. + +## Context + +`module` (`lib/Settings/softwarecatalogus_register.json:6779` schema, version 0.3.3) is the application. It has versions (`moduleVersion.module`), usages (`usage.module`), connections (`connection.moduleA` and `moduleB`), standards and reference components. `suite` (`register.json:1137` schema) groups applications that are sold together. LeanIX models components as child applications, and GEMMA reference components apply to components as much as to whole applications, so a component stays a `module`. + +## D1. One self relation on module + +Through `lib/Settings/register.d/application-components.json`: + +| property | type | notes | +|---|---|---| +| `partOf` | `$ref module` | title "Part of", `x-relation-filter: { "provider": "@object.provider" }` so a component belongs to an application of the same supplier; `inversedBy: components` | + +and a computed inverse `components` (array of `$ref module`, `hideOnForm: true`, `x-relation-filter: { "partOf": "@objectId" }`), the same pattern `moduleVersion.usages` uses (`register.json:7651` schema). + +A module whose `partOf` points at itself, or at one of its own components, is refused: a `x-openregister-validation` rule if OpenRegister's dialect supports a not-self check, else a check in `ModuleRegistrationService` before save (the service that already hooks module saves, `lib/Service/ModuleRegistrationService.php`). + +Rejected: a separate `applicationComponent` schema. A component would then lose versions, usages, connections and compliance claims, which all point at `module`. + +## D2. Pages + +- `ModuleDetail` (`src/manifest.json:491`): an `object-list` widget `md-components`, filter `{ "partOf": "@objectId" }`, `rowRoute: ModuleDetail`, `allowCreate: true` with `partOf` and `provider` filled in. `partOf` joins the data widget's `include` list, so a component shows its parent. +- `Modules` page (`src/manifest.json`, component `FacetedCatalogIndexView`): a quick filter "Whole applications" (default) with `partOf` empty, and "All, including components". The page's quick filters already compose with the facet narrowing (its `_note`). + +## Declarative versus imperative + +A relation, a filter and page widgets (ADR-031). The only imperative part is the cycle check, and only if the dialect has no rule for it. + +## Seed data + +The demo register gains one demo application with two components. + +## Risks + +- `x-relation-filter` on `provider` means a component of another supplier's product cannot be recorded; LeanIX allows it. Suppliers register their own products, so this matches who may edit. diff --git a/openspec/changes/landscape-application-components/proposal.md b/openspec/changes/landscape-application-components/proposal.md new file mode 100644 index 000000000..8608f348f --- /dev/null +++ b/openspec/changes/landscape-application-components/proposal.md @@ -0,0 +1,40 @@ +--- +kind: config +depends_on: + - landscape-application-page +--- + +# Break an application into its components + +## Summary + +A supplier or an information manager records that an application consists of components, such as a case system with a separate portal and a document module. A component is an application in its own right, with its own versions, standards and connections, and it names the application it belongs to. The application page lists its components, and a component's page shows the application it is part of. + +## Why + +Row from the stackiq matrix: + +- `stackiq:land-application-modules`, "Break an application into modules and see which module belongs to which product." Rated partial, built. Two competitors rate yes: SAP LeanIX (https://help.sap.com/docs/leanix/ea/application-modeling-guidelines, "Applications often consist of multiple entities or modules within a common ecosystem or platform", modelled as a parent and child hierarchy) and BlueDolphin (https://help.bluedolphin.io/en/articles/11967518-grouping-and-child-objects-in-architecture-views, "breaking down a system into its components"). The missing half: breaking one application into its components. Core area (landscape). + +No tender, feature request or roadmap row names it. + +## What stackiq has today + +- Stackiq's `module` is the whole application (`lib/Settings/softwarecatalogus_register.json:6779` schema, title Application). Nothing breaks one application into parts. +- A suite (`register.json:1137` schema) lists applications that are sold together (`suite.applications`), with a wizard (`src/dialogs/SuiteWizardDialog.vue`) and a page (`SuiteDetail`, `src/manifest.json` around :676). That answers "which application belongs to which product", not what one application is made of. + +## What this change builds + +1. A `partOf` field on `module` pointing at the application it is a component of, limited to applications of the same supplier. +2. A Components list on the application page, with an Add component action, and the parent application in the component's data. +3. A column filter on the Applications list to hide components, so the list shows whole applications by default. + +## Out of scope + +- More than one level of nesting (a component of a component). The field allows it, but the pages show one level. +- Suites and the suite wizard, which stay as they are. +- Drawing the composition in an architecture view: `architecture-views-editor`. + +## Risks + +- A component that is also registered as a separate application in usages keeps its own usages; nothing moves usages to the parent. diff --git a/openspec/changes/landscape-application-components/specs/application-components/spec.md b/openspec/changes/landscape-application-components/specs/application-components/spec.md new file mode 100644 index 000000000..d6239b018 --- /dev/null +++ b/openspec/changes/landscape-application-components/specs/application-components/spec.md @@ -0,0 +1,42 @@ +# application-components specification + +**Status**: proposed +**Scope**: stackiq +**OpenSpec changes**: +- landscape-application-components + +## Purpose + +An application can be broken into components, each an application of its own that names the application it belongs to. Matrix row `stackiq:land-application-modules`. + +## ADDED Requirements + +### Requirement: REQ-ACM-001 An application can name the application it is a component of + +The `module` schema SHALL carry `partOf`, pointing at another application of the same supplier, and SHALL expose the inverse list of components. A module SHALL NOT be part of itself or of one of its own components. + +#### Scenario: A supplier records a component +@e2e tests/e2e/workflows/application-components.spec.ts + +- **GIVEN** a supplier's application "Zaaksysteem" +- **WHEN** the supplier opens its page, clicks Add component and saves "Zaaksysteem portaal" +- **THEN** "Zaaksysteem portaal" names "Zaaksysteem" as the application it is part of + +#### Scenario: A cycle is refused +@e2e exclude Save-time guard; tests/Unit/Settings/ApplicationComponentsFragmentTest.php or tests/Unit/Service/ModuleRegistrationServiceTest.php asserts the refusal. + +- **GIVEN** application A with component C +- **WHEN** a user sets A to be part of C +- **THEN** the save is refused and A keeps no parent + +### Requirement: REQ-ACM-002 The application page lists its components + +The application page SHALL list the application's components, each opening its own page, and a component's page SHALL show the application it is part of. The Applications list SHALL show whole applications by default and SHALL let the user include components. + +#### Scenario: A buyer reads what an application is made of +@e2e tests/e2e/workflows/application-components.spec.ts + +- **GIVEN** "Zaaksysteem" has components "Zaaksysteem portaal" and "Zaaksysteem documenten" +- **WHEN** a municipal buyer opens the page of "Zaaksysteem" +- **THEN** the Components list shows both components +- **AND** the Applications list shows "Zaaksysteem" but not its components until the buyer picks All, including components diff --git a/openspec/changes/landscape-application-components/tasks.md b/openspec/changes/landscape-application-components/tasks.md new file mode 100644 index 000000000..cedab0f75 --- /dev/null +++ b/openspec/changes/landscape-application-components/tasks.md @@ -0,0 +1,35 @@ +# Tasks: landscape-application-components + +## Implementation tasks + +### Task 1: The partOf relation and its guard +- **spec_ref**: openspec/changes/landscape-application-components/specs/application-components/spec.md#requirement-req-acm-001-an-application-can-name-the-application-it-is-a-component-of +- **files**: `lib/Settings/register.d/application-components.json`, `lib/Service/ModuleRegistrationService.php` (only if the dialect lacks a not-self rule), `lib/Settings/stackiq_mock_register.json` +- **acceptance_criteria**: + - GIVEN component C of application A WHEN A is read THEN its components list holds C + - GIVEN application A WHEN a user sets A part of A THEN the save is refused with a message +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Settings/ApplicationComponentsFragmentTest.php`; `tests/Unit/Service/ModuleRegistrationServiceTest.php` cycle case if the service check is used) + +### Task 2: Components on the application page and the list filter +- **spec_ref**: openspec/changes/landscape-application-components/specs/application-components/spec.md#requirement-req-acm-002-the-application-page-lists-its-components +- **files**: `src/manifest.json` (ModuleDetail widget, Modules quick filters), `l10n/en.json`, `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN application A with two components WHEN its page opens THEN the Components list shows both + - GIVEN the Applications list WHEN it opens THEN components are hidden until the user picks All, including components +- [ ] Implement +- [ ] Test (Playwright `tests/e2e/workflows/application-components.spec.ts`) + +### Task 3: Documentation +- **spec_ref**: openspec/changes/landscape-application-components/specs/application-components/spec.md#requirement-req-acm-002-the-application-page-lists-its-components +- **files**: `docs/features/application-components.md`, `docs/images/application-components.png` +- **acceptance_criteria**: + - GIVEN the docs site WHEN a reader opens Application components THEN recording a component and the list filter are explained with a screenshot +- [ ] Implement +- [ ] Test (docs build, screenshot with Playwright) + +## Verification + +- `openspec validate landscape-application-components --type change --strict` passes. +- `composer check:strict` and `npm run lint` pass; the PHPUnit and Playwright cases above pass. +- English and Dutch strings for every new label (ADR-005); docs with a screenshot (ADR-010). From 6ae331ff716547b031364041b1a1f2c46d829948 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:48:52 +0200 Subject: [PATCH 08/15] docs(openspec): landscape-change-entry-type, change type through OpenRegister's identity-keeping move --- .../.openspec.yaml | 2 + .../landscape-change-entry-type/design.md | 39 +++++++++++++++++ .../landscape-change-entry-type/proposal.md | 39 +++++++++++++++++ .../specs/entry-type-change/spec.md | 43 +++++++++++++++++++ .../landscape-change-entry-type/tasks.md | 35 +++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 openspec/changes/landscape-change-entry-type/.openspec.yaml create mode 100644 openspec/changes/landscape-change-entry-type/design.md create mode 100644 openspec/changes/landscape-change-entry-type/proposal.md create mode 100644 openspec/changes/landscape-change-entry-type/specs/entry-type-change/spec.md create mode 100644 openspec/changes/landscape-change-entry-type/tasks.md diff --git a/openspec/changes/landscape-change-entry-type/.openspec.yaml b/openspec/changes/landscape-change-entry-type/.openspec.yaml new file mode 100644 index 000000000..7f2ad572a --- /dev/null +++ b/openspec/changes/landscape-change-entry-type/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-27 diff --git a/openspec/changes/landscape-change-entry-type/design.md b/openspec/changes/landscape-change-entry-type/design.md new file mode 100644 index 000000000..0e8035e24 --- /dev/null +++ b/openspec/changes/landscape-change-entry-type/design.md @@ -0,0 +1,39 @@ +# Design: landscape-change-entry-type + +Read at development `49e65cb4`, OpenRegister development `4fee776`. + +## Context + +Stackiq keeps applications in `module` and services in `catalogService` (`lib/Settings/softwarecatalogus_register.json:6779` and `:1326` schemas). Both share `name`, `shortDescription`, `longDescription`, `website`, `contactPerson`, `provider`, `logo`, `koppelingen`, `publicationDate` and `depublicationDate`. A module also holds licence, hosting, reference components, standards and versions; a service holds `modules` and a service `type`. OpenRegister's `MoveObject` moves an object between schemas without a copy: the uuid, audit trail, versions, files, notes and watchers stay keyed on the same uuid (`openregister lib/Service/Object/MoveObject.php:3-24`), and the endpoint answers 422 when the object does not fit (`ObjectsController.php:5219-5223`). + +## D1. A type-change service + +New `lib/Service/EntryTypeService.php`: + +- `preview(string $uuid, string $targetType): array` returns `carried` (fields both schemas declare), `dropped` (fields only the source has, with their values), and `blockers`: incoming references the target cannot hold. For Application to Service the blockers are usages (`usage.module`), versions (`moduleVersion.module`) and connections (`connection.moduleA`, `moduleB`); for Service to Application, contracts (`catalogContract.service`). +- `change(string $uuid, string $targetType): array` refuses when `blockers` is not empty, clears the dropped fields with one update, then calls the move (`POST /api/objects/{register}/{schema}/{id}/move`, in process through OpenRegister's `MoveObject` service resolved from the container) and returns the outcome. +- Application to System software and back is not a move: it sets `module.type`. + +Routes in `appinfo/routes.php`: `GET /api/entries/{uuid}/type-change?target=` (preview) and `POST /api/entries/{uuid}/type-change` (change), both `#[NoAdminRequired]`. The service reads the object under the caller's own permissions, and OpenRegister's move authorises both sides again, so a caller who cannot edit the entry or create in the target gets a 403 or 422. + +Rejected: create in the target and delete the source. It mints a second uuid and orphans the audit trail, files and relations, which is the problem the row names. + +## D2. The action and the preview + +A dialog `ChangeEntryTypeDialog` in `src/dialogs/` (ADR-004: dialogs live in their own file), opened from a header action on `ModuleDetail` (`src/manifest.json:491`) and from a row action on the Services list. It shows the preview in three lists (carried, dropped, blockers), disables Confirm while there are blockers, and after the move opens the entry at its new page. + +## D3. module.type on the form + +`module.type` (`register.json` module schema) becomes `visible: true`, so the form and the data widget show Application or System software. No dialog is needed for that switch. + +## Declarative versus imperative + +The move itself is OpenRegister's. The preview and the guard are stackiq code because they depend on stackiq's schema pairs; there is no `x-openregister-*` construct for "which schemas may an object move between". + +## Seed data + +No schema added. `module.type` changes visibility only. + +## Risks + +- A future schema that references `module` would be missed by the blocker list. The list is built from the register's `$ref` properties at runtime, not hard-coded. diff --git a/openspec/changes/landscape-change-entry-type/proposal.md b/openspec/changes/landscape-change-entry-type/proposal.md new file mode 100644 index 000000000..574cdc0db --- /dev/null +++ b/openspec/changes/landscape-change-entry-type/proposal.md @@ -0,0 +1,39 @@ +--- +kind: code +depends_on: [] +--- + +# Change the type of an entry without recreating it + +## Summary + +A supplier or a functional administrator turns an application into a service, a service into an application, or an application into system software, without deleting the entry and typing it again. The entry keeps its identity: its history, files, relations and links stay attached, because OpenRegister moves the object instead of copying it. + +## Why + +Row from the stackiq matrix: + +- `stackiq:land-change-entry-type`, "Change the type of an existing entry without recreating it." Rated no, no competitor rates yes. Roadmap demand: https://tip.topdesk.com/c/89-changing-the-type-of-an-asset (TOPdesk). It sits in the product's core area (landscape), which is why it is built. + +## What stackiq has today + +- An entry lives in one schema: `module` (`lib/Settings/softwarecatalogus_register.json:6779` schema), `catalogService` (`:1326`) or `suite` (`:1137`). Nothing moves an object to another schema; the only route is delete and recreate, which loses its uuid and everything keyed on it. +- `module.type` (Application or System software) exists but is `visible: false` with default Application, so nobody can change it from a page. +- OpenRegister ships a move that keeps identity: `POST /api/objects/{register}/{schema}/{id}/move` (openregister `appinfo/routes.php:1245`, `lib/Controller/ObjectsController.php:5176`, `lib/Service/Object/MoveObject.php`). It authorises both sides and answers 422 when the object does not fit the target schema. + +## What this change builds + +1. A "Change type" action on the application page, and on the service rows of the Services list, offering: Application to Service, Service to Application, and Application to System software and back. +2. A preview of what carries over: the fields both schemas share, and the fields that would be dropped, before the user confirms. +3. A stackiq service that prepares the object for the target schema and calls OpenRegister's move, so the uuid, history, files and relations survive. +4. `module.type` shown and editable on the application form. + +## Out of scope + +- Suites: a suite groups applications and has no counterpart to turn into. +- Moving entries to another organisation: `landscape-move-between-organisations`. +- Bulk type changes. + +## Risks + +- Relations that point at the old schema by `$ref` (for example `usage.module`) keep the uuid but now point at an object in another schema. The preview lists incoming references that would no longer resolve, and the action refuses when the entry has usages or connections the target type cannot hold. diff --git a/openspec/changes/landscape-change-entry-type/specs/entry-type-change/spec.md b/openspec/changes/landscape-change-entry-type/specs/entry-type-change/spec.md new file mode 100644 index 000000000..9a2e13e0e --- /dev/null +++ b/openspec/changes/landscape-change-entry-type/specs/entry-type-change/spec.md @@ -0,0 +1,43 @@ +# entry-type-change specification + +**Status**: proposed +**Scope**: stackiq +**OpenSpec changes**: +- landscape-change-entry-type + +## Purpose + +An entry registered as the wrong type changes type in place and keeps its identity. Matrix row `stackiq:land-change-entry-type`. + +## ADDED Requirements + +### Requirement: REQ-ETC-001 An entry changes type and keeps its identity + +Stackiq SHALL change an application into a service, or a service into an application, by moving the object with OpenRegister's move, so the uuid, history, files and links stay. It SHALL refuse the change while the entry has incoming references the target type cannot hold, and SHALL switch an application between Application and System software by its type field. + +#### Scenario: A supplier turns an application into a service +@e2e tests/e2e/workflows/change-entry-type.spec.ts + +- **GIVEN** a supplier registered "Hosting en beheer" as an application, with no usages, versions or connections, and one file attached +- **WHEN** the supplier opens its page, picks Change type, Service, and confirms +- **THEN** "Hosting en beheer" opens as a service with the same identifier +- **AND** the attached file and the history are still there + +#### Scenario: A change that would break usages is refused +@e2e exclude Guard case; tests/Unit/Service/EntryTypeServiceTest.php asserts the blocker list and that no move is called. + +- **GIVEN** an application with one usage by a municipality +- **WHEN** a functional administrator asks to change it into a service +- **THEN** the dialog lists the usage as a blocker and Confirm stays disabled +- **AND** `POST /api/entries/{uuid}/type-change` answers 409 with the blocker + +### Requirement: REQ-ETC-002 The user sees what carries over before confirming + +Before a type change, stackiq SHALL show which fields carry over, which fields and values would be dropped, and which references block the change. + +#### Scenario: The preview names the dropped licence field +@e2e tests/e2e/workflows/change-entry-type.spec.ts + +- **GIVEN** an application with a licence type filled in +- **WHEN** the supplier picks Change type, Service +- **THEN** the dialog lists the licence type under fields that will be dropped, with its value diff --git a/openspec/changes/landscape-change-entry-type/tasks.md b/openspec/changes/landscape-change-entry-type/tasks.md new file mode 100644 index 000000000..16c673677 --- /dev/null +++ b/openspec/changes/landscape-change-entry-type/tasks.md @@ -0,0 +1,35 @@ +# Tasks: landscape-change-entry-type + +## Implementation tasks + +### Task 1: Entry type service with preview and guard +- **spec_ref**: openspec/changes/landscape-change-entry-type/specs/entry-type-change/spec.md#requirement-req-etc-001-an-entry-changes-type-and-keeps-its-identity +- **files**: `lib/Service/EntryTypeService.php`, `lib/Controller/EntryTypeController.php`, `appinfo/routes.php`, `lib/AppInfo/Application.php` +- **acceptance_criteria**: + - GIVEN an application without usages, versions or connections WHEN it is changed to a service THEN the same uuid answers as a catalogService with its history intact + - GIVEN an application with a usage WHEN a change to service is asked THEN the preview lists the usage as a blocker and the change is refused +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Service/EntryTypeServiceTest.php` with an ObjectService double on the real interface; `tests/Unit/Controller/EntryTypeControllerTest.php` for 403, 409 and 422) + +### Task 2: Change type dialog and module type on the form +- **spec_ref**: openspec/changes/landscape-change-entry-type/specs/entry-type-change/spec.md#requirement-req-etc-002-the-user-sees-what-carries-over-before-confirming +- **files**: `src/dialogs/ChangeEntryTypeDialog.vue`, `src/manifest.json` (ModuleDetail header action, Diensten row action), `lib/Settings/softwarecatalogus_register.json` (module.type visible), `l10n/en.json`, `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN the application page WHEN the user picks Change type, Service THEN the dialog lists carried and dropped fields + - GIVEN the application form WHEN it opens THEN the type field offers Application and System software +- [ ] Implement +- [ ] Test (Playwright `tests/e2e/workflows/change-entry-type.spec.ts`) + +### Task 3: Documentation +- **spec_ref**: openspec/changes/landscape-change-entry-type/specs/entry-type-change/spec.md#requirement-req-etc-001-an-entry-changes-type-and-keeps-its-identity +- **files**: `docs/features/change-entry-type.md`, `docs/images/change-entry-type.png` +- **acceptance_criteria**: + - GIVEN the docs site WHEN a reader opens Change entry type THEN the preview, the blockers and what survives are explained with a screenshot +- [ ] Implement +- [ ] Test (docs build, screenshot with Playwright) + +## Verification + +- `openspec validate landscape-change-entry-type --type change --strict` passes. +- `composer check:strict` and `npm run lint` pass; the PHPUnit and Playwright cases above pass. +- English and Dutch strings for every new label (ADR-005); docs with a screenshot (ADR-010). From 98e910c910a6005b776df70e27daff2185f2aef7 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:49:52 +0200 Subject: [PATCH 09/15] docs(openspec): landscape-move-between-organisations, transfer chosen entries with a dry run --- .../.openspec.yaml | 2 + .../design.md | 42 +++++++++++++++++ .../proposal.md | 41 ++++++++++++++++ .../specs/move-between-organisations/spec.md | 47 +++++++++++++++++++ .../tasks.md | 42 +++++++++++++++++ 5 files changed, 174 insertions(+) create mode 100644 openspec/changes/landscape-move-between-organisations/.openspec.yaml create mode 100644 openspec/changes/landscape-move-between-organisations/design.md create mode 100644 openspec/changes/landscape-move-between-organisations/proposal.md create mode 100644 openspec/changes/landscape-move-between-organisations/specs/move-between-organisations/spec.md create mode 100644 openspec/changes/landscape-move-between-organisations/tasks.md diff --git a/openspec/changes/landscape-move-between-organisations/.openspec.yaml b/openspec/changes/landscape-move-between-organisations/.openspec.yaml new file mode 100644 index 000000000..7f2ad572a --- /dev/null +++ b/openspec/changes/landscape-move-between-organisations/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-27 diff --git a/openspec/changes/landscape-move-between-organisations/design.md b/openspec/changes/landscape-move-between-organisations/design.md new file mode 100644 index 000000000..d979a813d --- /dev/null +++ b/openspec/changes/landscape-move-between-organisations/design.md @@ -0,0 +1,42 @@ +# Design: landscape-move-between-organisations + +Read at development `49e65cb4`. + +## Context + +Ownership in stackiq has two layers. OpenRegister multitenancy stamps `@self.organisation` on every object, and read rules match on it (`{"_organisation": "$organisation"}` throughout `lib/Settings/softwarecatalogus_register.json`). Domain fields name the organisation too: `module.provider`, `catalogService.provider`, `usage.consumer` and `participants`, `connection.provider`, `contactPerson.organization`. `MergeOrganisatieService` already knows both layers per type (`FIELD_RELATION_TYPES` :111, `SELF_ORGANISATION_RELATION_TYPES` :122) and saves the full object so unrelated fields survive (`repointBySelfOrganisation` :442, reading the owner through `readOwningOrganisation`). + +## D1. A transfer service on the merge primitives + +New `lib/Service/OwnershipTransferService.php`: + +- `plan(array $objectRefs, string $targetOrganisation, IUser $caller): array` returns, per object, `move` or `skip` with a reason (not owned by the source, caller cannot edit, target unknown), plus linked objects of the source organisation that stay behind (for a usage: its connections; for an application: its versions). +- `execute(...)` runs the plan's `move` items: sets `@self.organisation` to the target and the type's owning field (from the same map the merge uses), saving the full object. + +The per-type map and the owner reader move out of `MergeOrganisatieService` into a small shared class `lib/Service/Organisation/OwnershipMap.php`, used by both services, so the merge and the transfer cannot drift. + +Rejected: calling the merge with a filter. The merge tombstones the source organisation at the end; a transfer must never do that. + +## D2. Endpoints and authorisation + +`POST /api/ownership-transfers/plan` and `POST /api/ownership-transfers/execute` in `appinfo/routes.php`, body `{ objects: [{ schema, id }], targetOrganisation }`, controller `lib/Controller/OwnershipTransferController.php`, `#[NoAdminRequired]` with an explicit guard: the caller is a Nextcloud admin, or is in an organisation admin group (`SettingsService::getOrganizationAdminGroups()`, as `SettingsController::verifyOrgExportPermission()` uses at `lib/Controller/SettingsController.php:1737`) AND is a member of both organisations (the multi-org membership from the archived change `multi-org-membership`). Anything else is a 403 before any read. + +## D3. The action + +A dialog `MoveToOrganisationDialog` (`src/dialogs/`) with an organisation picker, the dry-run list and Confirm. It opens from: + +- a mass action on the list pages that support selection (Applications, Services, Applications in use, Connections, Contracts), through `CnIndexPage` mass actions (`CnMassActionBar` in the library); +- a header action on each of their detail pages. + +## Declarative versus imperative + +Imperative: a transfer rewrites ownership across types with a guard, which no `x-openregister-*` construct expresses. The per-type map is data in one class. + +## Seed data + +None. + +## Risks + +- Extracting the map touches the merge service; its existing tests (`tests/Unit/Service/MergeOrganisatieServiceTest.php` and the merge e2e) must stay green. +- `@self.organisation` is written through a full save. The archived merge change recorded why a partial write drops fields; the transfer uses the same full save. diff --git a/openspec/changes/landscape-move-between-organisations/proposal.md b/openspec/changes/landscape-move-between-organisations/proposal.md new file mode 100644 index 000000000..fa6dc87e3 --- /dev/null +++ b/openspec/changes/landscape-move-between-organisations/proposal.md @@ -0,0 +1,41 @@ +--- +kind: code +depends_on: + - landscape-usage-registration +--- + +# Move entries to another organisation without recreating them + +## Summary + +A functional administrator moves one entry, or a selection of entries, to another organisation: an application that belongs to a sister municipality, usages registered under the wrong organisation, or a contact person who changed employer. The entries keep their identity and relations; only who owns them changes. A dry run shows what will move before anything does. + +## Why + +Row from the stackiq matrix: + +- `stackiq:land-move-between-workspaces`, "Move one or many entries to another workspace or section without recreating them." Rated no. Two competitors rate yes: BlueDolphin (June 2026 update, https://bluedolphin.io/blog/june-2026-bluedolphin-updates/, "You can move one or multiple objects to another workspace directly from the Repository without leaving your current view", also the changelog demand row) and GLPI (source read at 11.0.9, `src/Transfer.php:50` moves selected records to another entity with their links, queued through `src/MassiveAction.php:598`). Core area (landscape). + +In stackiq the workspace is the organisation: every record belongs to one through OpenRegister multitenancy (`@self.organisation`), and the domain fields `module.provider`, `usage.consumer`, `connection.provider` and `contactPerson.organization` say the same thing in the data. + +## What stackiq has today + +- No page or action moves an entry to another organisation. +- The organisation merge moves everything of one organisation into another (`lib/Service/MergeOrganisatieService.php`): it re-points domain fields per type (`FIELD_RELATION_TYPES`, :111) and `@self.organisation` for contracts and compliance claims (`repointBySelfOrganisation`, :442), with a dry run and an execute (`lib/Controller/MergeController.php:82`, `:106`), for a Nextcloud admin only (:142). It moves all of an organisation, never a chosen set. + +## What this change builds + +1. A transfer service that moves a chosen set of entries (applications, services, usages, connections, contracts, compliance claims, contact persons) from one organisation to another, re-pointing `@self.organisation` and the type's owning field, reusing the merge service's per-type map. +2. A dry run that lists what will move and what will not (entries of another organisation, entries the caller may not edit). +3. A "Move to organisation" action on the list pages for the selected rows, and on each detail page. +4. Authorisation: a Nextcloud admin, or a user who is organisation admin in both the source and the target organisation. + +## Out of scope + +- Moving between registers or schemas: `landscape-change-entry-type`. +- Merging whole organisations, which stays in the merge panel. +- Moving files between Nextcloud folders; files stay attached to the entry's uuid. + +## Risks + +- A usage moved to another organisation keeps its connections, which may belong to the old organisation. The dry run lists such links so the administrator moves them together. diff --git a/openspec/changes/landscape-move-between-organisations/specs/move-between-organisations/spec.md b/openspec/changes/landscape-move-between-organisations/specs/move-between-organisations/spec.md new file mode 100644 index 000000000..7d69b3bf4 --- /dev/null +++ b/openspec/changes/landscape-move-between-organisations/specs/move-between-organisations/spec.md @@ -0,0 +1,47 @@ +# move-between-organisations specification + +**Status**: proposed +**Scope**: stackiq +**OpenSpec changes**: +- landscape-move-between-organisations + +## Purpose + +Entries registered under the wrong organisation move to the right one, one or many at a time, keeping their identity. Matrix row `stackiq:land-move-between-workspaces`. + +## ADDED Requirements + +### Requirement: REQ-MBO-001 An administrator moves chosen entries to another organisation and they keep their identity + +Stackiq SHALL move a chosen set of applications, services, usages, connections, contracts, compliance claims and contact persons from one organisation to another by re-pointing `@self.organisation` and the type's owning field, keeping each entry's uuid, history, files and relations. + +#### Scenario: A functional administrator moves two applications +@e2e tests/e2e/workflows/move-to-organisation.spec.ts + +- **GIVEN** a functional administrator who administers supplier A and supplier B, and two applications registered under A that belong to B +- **WHEN** they select both on the Applications list, pick Move to organisation, choose B and confirm +- **THEN** both applications show supplier B +- **AND** their pages open at the same addresses with their history + +### Requirement: REQ-MBO-002 Only an administrator of both organisations moves entries + +`POST /api/ownership-transfers/plan` and `/api/ownership-transfers/execute` SHALL answer 403 unless the caller is a Nextcloud admin, or an organisation admin who is a member of both the source and the target organisation. A refused call SHALL change nothing. + +#### Scenario: An administrator of one side is refused +@e2e exclude API guard; tests/Unit/Controller/OwnershipTransferControllerTest.php asserts the 403 and that no object was saved. + +- **GIVEN** a user who administers organisation A only +- **WHEN** they post a transfer of an A entry to organisation B +- **THEN** the answer is 403 + +### Requirement: REQ-MBO-003 The dry run shows what moves and what stays behind + +Before a transfer, stackiq SHALL list each chosen entry as moving or skipped with the reason, and SHALL list linked entries of the source organisation that stay behind. + +#### Scenario: Connections that stay behind are named +@e2e tests/e2e/workflows/move-to-organisation.spec.ts + +- **GIVEN** a usage of organisation A with one connection owned by A +- **WHEN** the administrator opens Move to organisation for that usage and picks organisation B +- **THEN** the dialog lists the usage under moving +- **AND** lists the connection under staying with organisation A diff --git a/openspec/changes/landscape-move-between-organisations/tasks.md b/openspec/changes/landscape-move-between-organisations/tasks.md new file mode 100644 index 000000000..c75f41626 --- /dev/null +++ b/openspec/changes/landscape-move-between-organisations/tasks.md @@ -0,0 +1,42 @@ +# Tasks: landscape-move-between-organisations + +## Implementation tasks + +### Task 1: Shared ownership map +- **spec_ref**: openspec/changes/landscape-move-between-organisations/specs/move-between-organisations/spec.md#requirement-req-mbo-001-an-administrator-moves-chosen-entries-to-another-organisation-and-they-keep-their-identity +- **files**: `lib/Service/Organisation/OwnershipMap.php`, `lib/Service/MergeOrganisatieService.php` +- **acceptance_criteria**: + - GIVEN the merge service WHEN it runs after the extraction THEN its dry run and execute give the same counts as before +- [ ] Implement +- [ ] Test (PHPUnit merge tests unchanged and green; `tests/Unit/Service/Organisation/OwnershipMapTest.php`) + +### Task 2: Transfer service and endpoints +- **spec_ref**: openspec/changes/landscape-move-between-organisations/specs/move-between-organisations/spec.md#requirement-req-mbo-002-only-an-administrator-of-both-organisations-moves-entries +- **files**: `lib/Service/OwnershipTransferService.php`, `lib/Controller/OwnershipTransferController.php`, `appinfo/routes.php`, `lib/AppInfo/Application.php` +- **acceptance_criteria**: + - GIVEN two applications of organisation A WHEN an admin of A and B executes a transfer to B THEN both carry organisation B and provider B with the same uuids + - GIVEN a user who administers only A WHEN they post a transfer to B THEN the answer is 403 and nothing changes +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Service/OwnershipTransferServiceTest.php`, `tests/Unit/Controller/OwnershipTransferControllerTest.php`; Newman request in `postman/stackiq-tests.json`) + +### Task 3: Move to organisation dialog +- **spec_ref**: openspec/changes/landscape-move-between-organisations/specs/move-between-organisations/spec.md#requirement-req-mbo-003-the-dry-run-shows-what-moves-and-what-stays-behind +- **files**: `src/dialogs/MoveToOrganisationDialog.vue`, `src/manifest.json` and `src/manifest.d/*.json` (mass and header actions), `l10n/en.json`, `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN three selected usages WHEN the admin picks Move to organisation THEN the dialog lists the three and their connections that stay behind +- [ ] Implement +- [ ] Test (Playwright `tests/e2e/workflows/move-to-organisation.spec.ts`) + +### Task 4: Documentation +- **spec_ref**: openspec/changes/landscape-move-between-organisations/specs/move-between-organisations/spec.md#requirement-req-mbo-003-the-dry-run-shows-what-moves-and-what-stays-behind +- **files**: `docs/features/move-to-organisation.md`, `docs/images/move-to-organisation.png` +- **acceptance_criteria**: + - GIVEN the docs site WHEN a reader opens Move to organisation THEN who may move, the dry run and what stays are explained with a screenshot +- [ ] Implement +- [ ] Test (docs build, screenshot with Playwright) + +## Verification + +- `openspec validate landscape-move-between-organisations --type change --strict` passes. +- `composer check:strict` and `npm run lint` pass; the PHPUnit, Newman and Playwright cases above pass. +- English and Dutch strings for every new label (ADR-005); docs with a screenshot (ADR-010). From 281174a9f750a8571f5cc5a558735f229b73980e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:51:50 +0200 Subject: [PATCH 10/15] docs(openspec): landscape-dependent-field-options, dependent option tables enforced by OpenRegister --- .../.openspec.yaml | 2 + .../design.md | 42 +++++++++++++++++ .../proposal.md | 39 ++++++++++++++++ .../specs/dependent-field-options/spec.md | 45 +++++++++++++++++++ .../tasks.md | 34 ++++++++++++++ 5 files changed, 162 insertions(+) create mode 100644 openspec/changes/landscape-dependent-field-options/.openspec.yaml create mode 100644 openspec/changes/landscape-dependent-field-options/design.md create mode 100644 openspec/changes/landscape-dependent-field-options/proposal.md create mode 100644 openspec/changes/landscape-dependent-field-options/specs/dependent-field-options/spec.md create mode 100644 openspec/changes/landscape-dependent-field-options/tasks.md diff --git a/openspec/changes/landscape-dependent-field-options/.openspec.yaml b/openspec/changes/landscape-dependent-field-options/.openspec.yaml new file mode 100644 index 000000000..7f2ad572a --- /dev/null +++ b/openspec/changes/landscape-dependent-field-options/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-27 diff --git a/openspec/changes/landscape-dependent-field-options/design.md b/openspec/changes/landscape-dependent-field-options/design.md new file mode 100644 index 000000000..8951fab03 --- /dev/null +++ b/openspec/changes/landscape-dependent-field-options/design.md @@ -0,0 +1,42 @@ +# Design: landscape-dependent-field-options + +Read at development `49e65cb4`, OpenRegister development `4fee776`, `@conduction/nextcloud-vue` 2.57.1. + +## Context + +OpenRegister reads `x-openregister-dependent-values` from a property: `{ "controlledBy": "", "allowed": { "": ["", ...] } }` (`openregister lib/Service/Rules/DependentValueTable.php:55`, the shape at :106-128). A controlling value the table does not list leaves the property unconstrained (:30-40). `DependentValueListener` refuses an object save with code `dependent-value-not-allowed` when a value is outside its row. The library form narrows relation pickers by `x-relation-filter` (`CnFormDialog.vue:1183`), but nothing in `@conduction/nextcloud-vue` 2.57.1 reads the dependent-values annotation (a search of `src/` finds none). + +## D1. Two tables in a register fragment + +`lib/Settings/register.d/dependent-field-options.json`: + +- `components.schemas.module.properties.licence["x-openregister-dependent-values"]`: `controlledBy: licentietype`, `allowed: { "Open source": [the five licences of the enum], "Closed source": [] }`. +- `components.schemas.organization.properties.samenwerkingtype["x-openregister-dependent-values"]`: `controlledBy: type`, `allowed: { "Collaboration": [the collaboration types], "Municipality": [], "Supplier": [], "Community": [] }`. + +The fragment adds a key to existing property objects; the deep merge unions object keys (`lib/Service/SettingsService.php:7338`), so nothing else in those properties changes. + +## D2. The stray enum value + +`organization.samenwerkingtype.enum` in `lib/Settings/softwarecatalogus_register.json` loses `samenwerkingtype`. That edit goes in the monolith, because a fragment can only append to a list (lists merge by `array_merge`, `SettingsService.php:7352`). The organization schema version is bumped with it. + +## D3. Existing rows + +A repair step `lib/Repair/ClearDisallowedDependentValues.php`, registered in `appinfo/info.xml` before the register import, clears `licence` on closed-source modules and `samenwerkingtype` on organisations that are not a collaboration, and logs how many it cleared. Without it the first edit of such a row after the import would be refused for a field the user did not touch. + +## D4. The form half lives in the library + +`CnFormDialog` gains the same treatment for `x-openregister-dependent-values` that `relationFilterDecls` gives `x-relation-filter`: when the controlling field changes, the dependent field's options become the table row, and a value outside it is cleared. That is a change in ConductionNL/nextcloud-vue; this change bumps `package.json` to the release that ships it, and until then the save-time refusal from OpenRegister is shown as the form error. + +Rejected: a stackiq-only form wrapper. ADR-012 keeps form behaviour in the shared library, and every app with an enum pair gains from it. + +## Declarative versus imperative + +Declarative: two annotations OpenRegister already enforces (ADR-031). The repair step is the one imperative piece, a one-off data fix. + +## Seed data + +The demo register's modules and organisations already satisfy both tables; the repair step's test uses its own fixtures. + +## Risks + +- A future licence value added to the enum must also be added to the table, or it is refused for open-source modules. The unit test compares the enum with the table's Open source row. diff --git a/openspec/changes/landscape-dependent-field-options/proposal.md b/openspec/changes/landscape-dependent-field-options/proposal.md new file mode 100644 index 000000000..cebd0ec48 --- /dev/null +++ b/openspec/changes/landscape-dependent-field-options/proposal.md @@ -0,0 +1,39 @@ +--- +kind: config +depends_on: [] +--- + +# Let the options of one field depend on another + +## Summary + +The catalogue's forms already narrow a picker by another field: the version picker follows the chosen application and the contact person picker follows the chosen supplier. Plain option lists do not: a closed-source application can still pick an open-source licence, and a municipality can pick a collaboration type. This change declares which values of one list are allowed for each value of another, so OpenRegister refuses a wrong pair on save and the form offers only the allowed options. + +## Why + +Row from the stackiq matrix: + +- `stackiq:land-dependent-fields`, "Make the options of one field depend on another, such as model depending on brand." Rated no, no competitor rates yes. Roadmap demand: https://tip.topdesk.com/c/87-field-dependencies-brand-type-model- (TOPdesk). Core area (landscape), which is why it is built. + +Re-reading the code for this change showed the rating is too low, so the matrix is corrected to partial in the same pull request: relation pickers already depend on another field (see below). What this change builds is the missing half, dependent option lists. + +## What stackiq has today + +- Relation pickers that follow another field: `usage.moduleVersion` with `x-relation-filter: { module: @object.module }`, `module.contactPerson` and `catalogService.contactPerson` and `catalogService.modules` on `@object.provider` (`lib/Settings/softwarecatalogus_register.json`, usage, module and catalogService schemas). The library form honours `@object.` filters (`@conduction/nextcloud-vue` 2.57.1, `src/components/CnFormDialog/CnFormDialog.vue:1183`). +- Plain option lists that do not: `module.licence` (five open-source licences) is offered whatever `module.licentietype` says, and `organization.samenwerkingtype` is offered whatever `organization.type` says. The `samenwerkingtype` enum also holds the stray value `samenwerkingtype`, its own name. +- OpenRegister declares dependent option lists as a table, `x-openregister-dependent-values` with `controlledBy` and `allowed`, and refuses a pair outside the table on save (`openregister lib/Service/Rules/DependentValueTable.php:55`, `lib/Listener/DependentValueListener.php`). No stackiq property uses it, and the library form does not read it. + +## What this change builds + +1. `x-openregister-dependent-values` on `module.licence` (controlled by `licentietype`: open-source licences only for Open source) and on `organization.samenwerkingtype` (controlled by `type`: collaboration types only for Collaboration). +2. The stray `samenwerkingtype` value removed from its own enum. +3. A form that offers only the allowed options, by asking `@conduction/nextcloud-vue` to read the annotation in `CnFormDialog`, the way it reads `x-relation-filter`. + +## Out of scope + +- The library change itself lives in ConductionNL/nextcloud-vue; this change names it and pins the release that carries it. Until then, OpenRegister's save-time refusal is the guard and the form shows every option. +- An admin screen to edit the tables. The tables live in the register fragment, reviewed like code. + +## Risks + +- Existing rows may hold a pair the table now refuses (a closed-source application with a licence set). The design adds a repair step that clears such values before the table is imported. diff --git a/openspec/changes/landscape-dependent-field-options/specs/dependent-field-options/spec.md b/openspec/changes/landscape-dependent-field-options/specs/dependent-field-options/spec.md new file mode 100644 index 000000000..f73c4ecb7 --- /dev/null +++ b/openspec/changes/landscape-dependent-field-options/specs/dependent-field-options/spec.md @@ -0,0 +1,45 @@ +# dependent-field-options specification + +**Status**: proposed +**Scope**: stackiq +**OpenSpec changes**: +- landscape-dependent-field-options + +## Purpose + +The allowed values of one option list follow the value of another, both on save and in the form. Matrix row `stackiq:land-dependent-fields`. + +## ADDED Requirements + +### Requirement: REQ-DFO-001 A value outside its dependent list is refused on save + +The `module` schema SHALL allow a `licence` only when `licentietype` is Open source, and the `organization` schema SHALL allow a `samenwerkingtype` only when `type` is Collaboration, declared with `x-openregister-dependent-values` so OpenRegister refuses any other pair. + +#### Scenario: A supplier cannot give a closed-source application an open-source licence +@e2e exclude Enforced by OpenRegister's save listener; tests/Unit/Settings/DependentFieldOptionsTest.php asserts both tables and the enum match. + +- **GIVEN** an application with licence type Closed source +- **WHEN** the supplier saves it with licence EUPL 1.2 +- **THEN** the save is refused with a message that the licence is not allowed for Closed source + +### Requirement: REQ-DFO-002 Existing rows that break a table are cleaned before it applies + +Before the tables are imported, stackiq SHALL clear `licence` on closed-source applications and `samenwerkingtype` on organisations that are not a collaboration, and SHALL log how many values it cleared. + +#### Scenario: An old row does not block the next edit +@e2e exclude Repair step; tests/Unit/Repair/ClearDisallowedDependentValuesTest.php covers it. + +- **GIVEN** a municipality whose collaboration type was filled in by an old import +- **WHEN** the repair step runs and an administrator then edits the municipality's name +- **THEN** the save succeeds and the collaboration type is empty + +### Requirement: REQ-DFO-003 The form offers only the allowed options + +The create and edit forms SHALL offer, for a dependent field, only the values its table allows for the current value of the controlling field, and SHALL clear a value that becomes disallowed when the controlling field changes. + +#### Scenario: Switching an application to open source opens the licence list +@e2e tests/e2e/workflows/dependent-field-options.spec.ts + +- **GIVEN** the edit form of an application with licence type Closed source and an empty licence list +- **WHEN** the supplier sets licence type to Open source +- **THEN** the licence field offers the five open-source licences diff --git a/openspec/changes/landscape-dependent-field-options/tasks.md b/openspec/changes/landscape-dependent-field-options/tasks.md new file mode 100644 index 000000000..2085ad456 --- /dev/null +++ b/openspec/changes/landscape-dependent-field-options/tasks.md @@ -0,0 +1,34 @@ +# Tasks: landscape-dependent-field-options + +## Implementation tasks + +### Task 1: Dependent value tables and the enum fix +- **spec_ref**: openspec/changes/landscape-dependent-field-options/specs/dependent-field-options/spec.md#requirement-req-dfo-001-a-value-outside-its-dependent-list-is-refused-on-save +- **files**: `lib/Settings/register.d/dependent-field-options.json`, `lib/Settings/softwarecatalogus_register.json` (samenwerkingtype enum, organization version) +- **acceptance_criteria**: + - GIVEN a closed-source module WHEN a licence is saved on it THEN OpenRegister refuses with dependent-value-not-allowed + - GIVEN the merged register WHEN the licence enum and the Open source row are compared THEN they hold the same values +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Settings/DependentFieldOptionsTest.php`) + +### Task 2: Clear disallowed values in existing rows +- **spec_ref**: openspec/changes/landscape-dependent-field-options/specs/dependent-field-options/spec.md#requirement-req-dfo-002-existing-rows-that-break-a-table-are-cleaned-before-it-applies +- **files**: `lib/Repair/ClearDisallowedDependentValues.php`, `appinfo/info.xml` +- **acceptance_criteria**: + - GIVEN a closed-source module with a licence WHEN the repair step runs THEN its licence is empty and the count is logged +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Repair/ClearDisallowedDependentValuesTest.php`) + +### Task 3: Adopt the library form support +- **spec_ref**: openspec/changes/landscape-dependent-field-options/specs/dependent-field-options/spec.md#requirement-req-dfo-003-the-form-offers-only-the-allowed-options +- **files**: `package.json`, `package-lock.json` +- **acceptance_criteria**: + - GIVEN the application form WHEN the licence type is Closed source THEN the licence field offers no options +- [ ] Implement (after ConductionNL/nextcloud-vue releases CnFormDialog support for x-openregister-dependent-values) +- [ ] Test (Playwright `tests/e2e/workflows/dependent-field-options.spec.ts`) + +## Verification + +- `openspec validate landscape-dependent-field-options --type change --strict` passes. +- `composer check:strict` and `npm run lint` pass; the PHPUnit and Playwright cases above pass. +- No new user-facing strings beyond the error text OpenRegister already translates. From 207678f3879158a27c9efae52c0ecf92f1c0f9df Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:54:02 +0200 Subject: [PATCH 11/15] docs(openspec): landscape-completeness-score, OpenRegister quality rules, confirm action and report --- .../.openspec.yaml | 2 + .../landscape-completeness-score/design.md | 64 +++++++++++++++++++ .../landscape-completeness-score/proposal.md | 44 +++++++++++++ .../specs/catalogue-data-quality/spec.md | 54 ++++++++++++++++ .../landscape-completeness-score/tasks.md | 51 +++++++++++++++ 5 files changed, 215 insertions(+) create mode 100644 openspec/changes/landscape-completeness-score/.openspec.yaml create mode 100644 openspec/changes/landscape-completeness-score/design.md create mode 100644 openspec/changes/landscape-completeness-score/proposal.md create mode 100644 openspec/changes/landscape-completeness-score/specs/catalogue-data-quality/spec.md create mode 100644 openspec/changes/landscape-completeness-score/tasks.md diff --git a/openspec/changes/landscape-completeness-score/.openspec.yaml b/openspec/changes/landscape-completeness-score/.openspec.yaml new file mode 100644 index 000000000..7f2ad572a --- /dev/null +++ b/openspec/changes/landscape-completeness-score/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-27 diff --git a/openspec/changes/landscape-completeness-score/design.md b/openspec/changes/landscape-completeness-score/design.md new file mode 100644 index 000000000..37f9ded03 --- /dev/null +++ b/openspec/changes/landscape-completeness-score/design.md @@ -0,0 +1,64 @@ +# Design: landscape-completeness-score + +Read at development `49e65cb4`, OpenRegister development `4fee776`. + +## Context + +OpenRegister computes a weighted data quality score on every save when a schema's `configuration` carries `x-openregister-quality` (`openregister lib/Listener/QualityScoreOnSaveListener.php:223-233` reads it). Rule types are `required`, `format` (named `email`, `url`, `date`, or a `pattern`) and `freshness` (exponential decay on a date field with `halfLifeDays`, default 180) (`lib/Service/Quality/QualityScorer.php`). `good` and `fair` thresholds default to 0.8 and 0.5 (`QualityScorer.php:134-147`). The score always lands in `@self.quality` and also in body fields the schema declares (`QualityScoreOnSaveListener.php:153-191`). The statistics endpoint reads the body field (`QualityStatisticsService.php:154`, default `qualityScore`). + +## D1. The rule set, in a fragment + +`lib/Settings/register.d/data-quality.json`: + +`module.configuration["x-openregister-quality"]`: + +| type | field | weight | +|---|---|---| +| required | name | 1 | +| required | shortDescription | 2 | +| required | longDescription | 1 | +| required | provider | 2 | +| required | contactPerson | 2 | +| required | referenceComponents | 3 | +| required | licentietype | 1 | +| required | cloudDienstverleningsmodel | 1 | +| required | hostingLocation | 1 | +| required | bbnLevel | 1 | +| format (url) | website | 1 | +| freshness (halfLifeDays 365) | lastConfirmedAt | 3 | + +`usage.configuration["x-openregister-quality"]`: required `module`, `moduleVersion`, `status`, `businessOwner`, `technicalOwner` (from `landscape-usage-registration`), `usedForReferenceComponents`, and freshness on `lastConfirmedAt`. Thresholds good 0.8, fair 0.5 on both. + +Plus, on both schemas: `lastConfirmedAt` (date-time, visible, editable only through the action in D3), `qualityScore` (number) and `qualityStatus` (string, enum good, fair, poor), both `hideOnForm: true`, so the form never shows a number the platform overwrites. + +Rejected: a stackiq scoring service. OpenRegister already scores on save and serves the statistics; a second scorer would drift from it (ADR-022, ADR-031). + +## D2. Where the score shows + +- Applications list (`FacetedCatalogIndexView`, `Modules` page columns) and Applications in use (`src/manifest.d/usages.json` from `landscape-usage-registration`): a `qualityStatus` column rendered as a status badge. +- `ModuleDetail` (`src/manifest.json:491`): a `stat` widget showing `qualityScore` as a percentage with its status. + +## D3. Confirm this entry is current + +A header action on `ModuleDetail` and on the usage detail page that saves `lastConfirmedAt = now` and nothing else. The save triggers the rescore, so the freshness part goes back to full. The action shows for users who may update the entry. + +## D4. The report + +`Reports` (`src/manifest.json:1021`) gets a second card, "Data quality", routing to a new page `DataQualityReport` (`/data-quality`), a custom view `src/views/DataQualityReportView.vue` that calls the two OpenRegister endpoints per schema (`module`, `usage`): the score spread (good, fair, poor counts, from `stats`) and the twenty lowest entries (from the listing), each row opening its page. + +## D5. Existing rows + +A repair step `lib/Repair/RescoreDataQuality.php`, run after the register import, saves every module and usage once through the object service so each gets a score. It logs the count and is idempotent. + +## Declarative versus imperative + +The rules and the scoring are declarative (ADR-031); the report is a read view over OpenRegister's endpoints; the rescore is a one-off repair. + +## Seed data + +`lastConfirmedAt` is set on the demo modules and usages so the demo shows good, fair and poor entries. + +## Risks + +- The rescore saves every row once; on a catalogue of 6,000 modules it runs as a background job rather than inside the repair step if it exceeds the step's time budget. +- Weights encode a judgement. They are in the fragment where a pull request can change them. diff --git a/openspec/changes/landscape-completeness-score/proposal.md b/openspec/changes/landscape-completeness-score/proposal.md new file mode 100644 index 000000000..4bf27d6a0 --- /dev/null +++ b/openspec/changes/landscape-completeness-score/proposal.md @@ -0,0 +1,44 @@ +--- +kind: config +depends_on: + - landscape-usage-registration +--- + +# Score how complete and current each entry is + +## Summary + +Every application, and every organisation's usage of one, gets a data quality score from a declared rule set: which fields must be filled, which must have the right format, and how recently the entry was confirmed. The score shows on the Applications list and the application page, and a Data quality report lists the weakest entries per type, so an information manager knows what to fix first. + +## Why + +Rows from the stackiq matrix: + +- `stackiq:land-completeness-score`, "See how complete and up to date each application's entry is, as a score." Rated no. Two competitors rate yes: SAP LeanIX (https://help.sap.com/docs/leanix/ea/fact-sheet-completeness, "The fact sheet completion score measures how much of the required data has been filled out ... in the fact sheet's header") and BlueDolphin (https://help.bluedolphin.io/en/articles/11967713-governance-insights, "Object completeness: Lists all objects and the completeness score of each object"). Core area (landscape). +- `stackiq:comp-health-scoring`, "Score the correctness and completeness of the register against a rule set." Rated no. Two competitors rate yes: SAP LeanIX (https://help.sap.com/docs/leanix/ea/application-portfolio-management-dashboard, "Data Quality KPI ... Overall Completion of Applications", weights set by admins) and BlueDolphin (the same governance insights page). + +No tender, feature request or roadmap row names these rows. + +## What stackiq has today + +- No completeness, quality or freshness score in `lib/` or `src/`. +- OpenRegister scores data quality from a declared annotation: `configuration.x-openregister-quality` with `rules` of type `required`, `format` and `freshness`, weights and `good` and `fair` thresholds (`openregister lib/Service/Quality/QualityScorer.php`, `QualityAnnotationValidator.php:42`). A save listener writes the score to `@self.quality` and to the body fields `qualityScore` and `qualityStatus` where the schema declares them (`lib/Listener/QualityScoreOnSaveListener.php:153-191`). Read-only statistics and a lowest-first listing sit at `GET /api/objects/quality/{register}/{schema}/stats` and `GET /api/objects/quality/{register}/{schema}` (openregister `appinfo/routes.php:628-629`), and they read the body field (`QualityStatisticsService.php:154`). +- No stackiq schema declares the annotation. + +## What this change builds + +1. A declared rule set on `module` and on `usage`: required fields with weights, a URL format check on websites, and a freshness rule on a new `lastConfirmedAt` date. +2. Hidden `qualityScore` and `qualityStatus` fields on both schemas, so OpenRegister's statistics and listing work. +3. A Data quality column on the Applications list and on Applications in use, and a score tile on the application page. +4. A "Confirm this entry is current" action on the application and usage pages that sets `lastConfirmedAt`. +5. A Data quality report, a card on the Reports page, with the score spread per type and the twenty weakest entries. + +## Out of scope + +- Asking owners to confirm their entries by survey: `landscape-owner-attestation`, which sets the same `lastConfirmedAt`. +- Scores for contracts, connections and compliance claims; the same annotation can be added per schema later. +- An admin screen for the rule set. The rules live in the register fragment and are edited like code, or in OpenRegister's schema editor. + +## Risks + +- Existing rows have no score until they are saved. The design adds a one-off rescore after the import. diff --git a/openspec/changes/landscape-completeness-score/specs/catalogue-data-quality/spec.md b/openspec/changes/landscape-completeness-score/specs/catalogue-data-quality/spec.md new file mode 100644 index 000000000..66c5beca2 --- /dev/null +++ b/openspec/changes/landscape-completeness-score/specs/catalogue-data-quality/spec.md @@ -0,0 +1,54 @@ +# catalogue-data-quality specification + +**Status**: proposed +**Scope**: stackiq +**OpenSpec changes**: +- landscape-completeness-score + +## Purpose + +Applications and usages carry a data quality score from a declared rule set, so users see what to fix and whether entries are current. Matrix rows `stackiq:land-completeness-score` and `stackiq:comp-health-scoring`. + +## ADDED Requirements + +### Requirement: REQ-CDQ-001 Every application and usage carries a data quality score from a declared rule set + +The `module` and `usage` schemas SHALL declare `x-openregister-quality` with weighted `required`, `format` and `freshness` rules and `good` and `fair` thresholds, so OpenRegister scores each entry on save. Stackiq SHALL show the resulting status (good, fair or poor) on the Applications list, on Applications in use and on the application page. + +#### Scenario: An incomplete application scores poor +@e2e tests/e2e/workflows/data-quality.spec.ts + +- **GIVEN** a supplier saves an application with only a name and a supplier +- **WHEN** a municipal information manager opens the Applications list +- **THEN** that application shows data quality poor + +#### Scenario: The rule set is valid for OpenRegister +@e2e exclude Config check; tests/Unit/Settings/DataQualityFragmentTest.php asserts the annotation shape and that every rule names an existing field. + +- **GIVEN** the merged register +- **WHEN** OpenRegister validates the `x-openregister-quality` annotation of `module` and `usage` +- **THEN** it reports no errors + +### Requirement: REQ-CDQ-002 A user confirms an entry is current and its freshness resets + +The application page and the usage page SHALL offer "Confirm this entry is current" to a user who may edit the entry. Confirming SHALL set `lastConfirmedAt` to now and nothing else, and the score SHALL be recomputed. + +#### Scenario: An application owner confirms a stale entry +@e2e tests/e2e/workflows/data-quality.spec.ts + +- **GIVEN** an application last confirmed 14 months ago with status fair +- **WHEN** its owner opens the page and clicks Confirm this entry is current +- **THEN** the page shows today as last confirmed +- **AND** the data quality score is higher than before + +### Requirement: REQ-CDQ-003 A data quality report shows the spread and the weakest entries + +The Reports page SHALL offer a Data quality report that shows, for applications and for usages, how many entries are good, fair and poor, and lists the twenty weakest entries first, each opening its page. + +#### Scenario: An information manager finds what to fix first +@e2e tests/e2e/workflows/data-quality.spec.ts + +- **GIVEN** the catalogue has good, fair and poor applications +- **WHEN** the information manager opens Reports, then Data quality +- **THEN** the page shows the three counts for applications +- **AND** the first rows of the weakest list are poor entries diff --git a/openspec/changes/landscape-completeness-score/tasks.md b/openspec/changes/landscape-completeness-score/tasks.md new file mode 100644 index 000000000..6aa5cc815 --- /dev/null +++ b/openspec/changes/landscape-completeness-score/tasks.md @@ -0,0 +1,51 @@ +# Tasks: landscape-completeness-score + +## Implementation tasks + +### Task 1: Rule set and fields +- **spec_ref**: openspec/changes/landscape-completeness-score/specs/catalogue-data-quality/spec.md#requirement-req-cdq-001-every-application-and-usage-carries-a-data-quality-score-from-a-declared-rule-set +- **files**: `lib/Settings/register.d/data-quality.json`, `lib/Settings/stackiq_mock_register.json` +- **acceptance_criteria**: + - GIVEN an application with every weighted field filled and confirmed today WHEN it is saved THEN its status is good + - GIVEN an application without reference components and never confirmed WHEN it is saved THEN its status is poor or fair +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Settings/DataQualityFragmentTest.php`: annotation passes OpenRegister's shape rules, every rule field exists on its schema) + +### Task 2: Score on lists and the application page, and the confirm action +- **spec_ref**: openspec/changes/landscape-completeness-score/specs/catalogue-data-quality/spec.md#requirement-req-cdq-002-a-user-confirms-an-entry-is-current-and-its-freshness-resets +- **files**: `src/manifest.json` (Modules columns, ModuleDetail stat widget and action), `src/manifest.d/usages.json`, `src/views/FacetedCatalogIndexView.vue` if the column needs a renderer, `l10n/en.json`, `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN the Applications list WHEN it opens THEN each row shows good, fair or poor + - GIVEN an application confirmed a year ago WHEN its owner clicks Confirm this entry is current THEN lastConfirmedAt is today and the score rises +- [ ] Implement +- [ ] Test (Playwright `tests/e2e/workflows/data-quality.spec.ts`) + +### Task 3: Data quality report +- **spec_ref**: openspec/changes/landscape-completeness-score/specs/catalogue-data-quality/spec.md#requirement-req-cdq-003-a-data-quality-report-shows-the-spread-and-the-weakest-entries +- **files**: `src/views/DataQualityReportView.vue`, `src/customComponents.js`, `src/manifest.json` (Reports card, DataQualityReport page) +- **acceptance_criteria**: + - GIVEN applications with mixed scores WHEN the information manager opens Reports, Data quality THEN the page shows the good, fair and poor counts and the weakest entries first +- [ ] Implement +- [ ] Test (vitest `tests/vitest/dataQualityReport.spec.js` for the view model; Playwright case in `tests/e2e/workflows/data-quality.spec.ts`) + +### Task 4: Rescore existing rows +- **spec_ref**: openspec/changes/landscape-completeness-score/specs/catalogue-data-quality/spec.md#requirement-req-cdq-001-every-application-and-usage-carries-a-data-quality-score-from-a-declared-rule-set +- **files**: `lib/Repair/RescoreDataQuality.php`, `appinfo/info.xml` +- **acceptance_criteria**: + - GIVEN modules without a score WHEN the repair step runs THEN each has a score and the count is logged +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Repair/RescoreDataQualityTest.php`) + +### Task 5: Documentation +- **spec_ref**: openspec/changes/landscape-completeness-score/specs/catalogue-data-quality/spec.md#requirement-req-cdq-003-a-data-quality-report-shows-the-spread-and-the-weakest-entries +- **files**: `docs/features/data-quality.md`, `docs/images/data-quality-report.png` +- **acceptance_criteria**: + - GIVEN the docs site WHEN a reader opens Data quality THEN the rules, the score, the confirm action and the report are explained with a screenshot +- [ ] Implement +- [ ] Test (docs build, screenshot with Playwright) + +## Verification + +- `openspec validate landscape-completeness-score --type change --strict` passes. +- `composer check:strict` and `npm run lint` pass; the PHPUnit, vitest and Playwright cases above pass. +- English and Dutch strings for every new label (ADR-005); docs with a screenshot (ADR-010). From 1a4de2fc48e4dc62385fd1aa22b1c8df168902ad Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:55:27 +0200 Subject: [PATCH 12/15] docs(openspec): landscape-owner-attestation, confirmation rounds for owners --- .../.openspec.yaml | 2 + .../landscape-owner-attestation/design.md | 61 +++++++++++++++++ .../landscape-owner-attestation/proposal.md | 45 +++++++++++++ .../specs/owner-attestation/spec.md | 65 +++++++++++++++++++ .../landscape-owner-attestation/tasks.md | 53 +++++++++++++++ 5 files changed, 226 insertions(+) create mode 100644 openspec/changes/landscape-owner-attestation/.openspec.yaml create mode 100644 openspec/changes/landscape-owner-attestation/design.md create mode 100644 openspec/changes/landscape-owner-attestation/proposal.md create mode 100644 openspec/changes/landscape-owner-attestation/specs/owner-attestation/spec.md create mode 100644 openspec/changes/landscape-owner-attestation/tasks.md diff --git a/openspec/changes/landscape-owner-attestation/.openspec.yaml b/openspec/changes/landscape-owner-attestation/.openspec.yaml new file mode 100644 index 000000000..7f2ad572a --- /dev/null +++ b/openspec/changes/landscape-owner-attestation/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-27 diff --git a/openspec/changes/landscape-owner-attestation/design.md b/openspec/changes/landscape-owner-attestation/design.md new file mode 100644 index 000000000..565a602c1 --- /dev/null +++ b/openspec/changes/landscape-owner-attestation/design.md @@ -0,0 +1,61 @@ +# Design: landscape-owner-attestation + +Read at development `49e65cb4`, OpenRegister development `4fee776`. + +## Context + +The entries to confirm are usages (an organisation's applications in use, owners from `landscape-usage-registration`) and modules (a supplier's products, contact from `module.contactPerson`). `landscape-completeness-score` gives both `lastConfirmedAt` and a freshness rule. Owners act as Nextcloud users; contact persons get accounts through `ContactPersonHandler::createUserAccount()` (called from `lib/Controller/ContactpersonenController.php:393` onwards). + +## D1. Two schemas in a fragment + +`lib/Settings/register.d/owner-attestation.json`, both added to the `stackiq` register list: + +`attestationRound`: `name`, `deadline` (date), `scopeSchema` (enum `usage`, `module`), `scopeOrganisation` (`$ref organization`), `startedBy` (string, uid), `status` (enum `open`, `closed`) with an `x-openregister-lifecycle` (open to closed), `requestCount`, `answeredCount` (numbers written by the service). + +`attestationRequest`: `round` (`$ref attestationRound`), `entrySchema`, `entryId`, `entryName` (string, for lists), `assigneeUserId` (string, Nextcloud uid), `assigneeContact` (`$ref contactPerson`), `status` (enum `pending`, `confirmed`, `corrected`, `overdue`), `respondedAt`. + +Authorization: an organisation reads its own rounds and requests (`_organisation` match); a request is also readable and updatable by its assignee (`{"match": {"assigneeUserId": "$userId"}}` in the read and update rules). + +## D2. Creating a round + +`lib/Service/AttestationService.php`, `start(string $scopeSchema, string $organisationId, string $deadline, IUser $caller)`: + +1. Load the entries: usages whose `consumer` is the organisation, or modules whose `provider` is the organisation. +2. For each entry pick owners: `businessOwner` and `technicalOwner` for a usage, `contactPerson` for a module. Resolve each contact person to a Nextcloud uid through `ContactPersonHandler`; entries whose owner has no account are returned as `unassigned`. +3. Create the round and one request per entry and owner. + +Route `POST /api/attestation-rounds` (`#[NoAdminRequired]`), guarded: the caller is an organisation admin of the scope organisation (`SettingsService::getOrganizationAdminGroups()`). The response lists the unassigned entries. + +## D3. Answering + +- Confirm: `POST /api/attestation-requests/{id}/confirm` sets the entry's `lastConfirmedAt` to now (a normal update under the caller's rights, so OpenRegister rescores it) and the request's status to `confirmed`. +- Correct: the owner edits the entry through its normal page; a listener on `ObjectUpdatedEvent` for `usage` and `module` marks an open pending request for that entry and that user `corrected` and sets `lastConfirmedAt`. +- Overdue: a daily `TimedJob` (`lib/BackgroundJob/AttestationOverdueJob.php`, registered in `appinfo/info.xml`) sets pending requests past the round's deadline to `overdue` and updates the round's counts. + +## D4. Notifications, declared + +`attestationRequest.configuration["x-openregister-notifications"]`: + +- `request-created`: trigger `created`, channels `nc-notification` and `email`, recipient `{ "kind": "field", "field": "assigneeUserId" }`, subject "Please confirm your entry {{entryName}}". +- `deadline-near`: trigger `scheduled` daily with filter on the round deadline within three days and status `pending`, same recipient. + +## D5. Pages + +`src/manifest.d/owner-attestation.json`: + +- `MyAttestations` (`/my-confirmations`), an index over `attestationRequest` filtered on `assigneeUserId` of the current user and status `pending`, with row actions Confirm and Open entry. +- `AttestationRounds` (`/confirmation-rounds`) and `AttestationRoundDetail`, with a Start round action (dialog `src/dialogs/StartAttestationRoundDialog.vue`), the counts as stat widgets and the requests as an object list grouped by status. +- Menu: both as children of an existing entry (Organisations), not new top-level entries (ADR-097). + +## Declarative versus imperative + +Schemas, lifecycle, notifications and pages are declarative (ADR-031). Creating requests from a scope, resolving owners to users and the overdue sweep are imperative: they join several schemas and Nextcloud users. + +## Seed data + +None beyond the schemas; the demo shows a round only after an administrator starts one. + +## Risks + +- The update listener must only mark requests of the user who saved; a colleague's edit must not answer someone else's request. +- The overdue job must stay idempotent; it only moves `pending` to `overdue`. diff --git a/openspec/changes/landscape-owner-attestation/proposal.md b/openspec/changes/landscape-owner-attestation/proposal.md new file mode 100644 index 000000000..82b5ce5ea --- /dev/null +++ b/openspec/changes/landscape-owner-attestation/proposal.md @@ -0,0 +1,45 @@ +--- +kind: code +depends_on: + - landscape-usage-registration + - landscape-completeness-score +--- + +# Ask owners to confirm or correct their entries + +## Summary + +An information manager starts a confirmation round: every owner of an application in use, or every supplier contact of a product, gets a request to confirm the entry is current or to correct it, by a deadline. Owners answer from a notification, see exactly which entries are theirs, and confirm or edit each. The round shows who answered, who corrected and who is overdue, and a confirmed entry's data quality score goes back to fresh. + +## Why + +Row from the stackiq matrix: + +- `stackiq:land-data-quality-survey`, "Ask application owners through a survey to confirm or correct their entries." Rated no. Two competitors rate yes: SAP LeanIX (https://help.sap.com/docs/leanix/ea/application-modernization-collect-data, "Create a new survey to get key information from application or business owners", and https://help.sap.com/docs/leanix/ea/reviewing-responses, "Review and approve survey responses before they are saved") and BlueDolphin (https://help.bluedolphin.io/en/articles/11967524-create-a-survey, surveys "allowing external stakeholders to contribute directly to the Enterprise Architecture repository"). Core area (landscape). + +No tender, feature request or roadmap row names it. + +## What stackiq has today + +- Nothing asks owners to confirm or correct entries: no survey, attestation or confirmation code in `lib/` or `src/`. +- `landscape-usage-registration` adds `businessOwner` and `technicalOwner` to a usage; `module.contactPerson` names the supplier's contact per product. Contact persons become Nextcloud users through `ContactpersonenController::convertToUser()` (`lib/Controller/ContactpersonenController.php:393`). +- `landscape-completeness-score` adds `lastConfirmedAt` and a freshness rule, and a manual Confirm action. +- OpenRegister notifications resolve a recipient from an object field (`kind: field`, `openregister lib/Service/Notification/NotificationRecipientResolver.php:187`), and stackiq already declares notification rules in its register (for example the usage phase-out rule, `lib/Settings/softwarecatalogus_register.json:2662`). + +## What this change builds + +1. Two schemas: a confirmation round (name, deadline, which entries, who started it) and a confirmation request per entry and owner (status pending, confirmed, corrected or overdue). +2. A service that creates a round's requests from a scope (the organisation's usages, or a supplier's products), resolving each owner to a Nextcloud user. +3. A notification to each owner when a request is created and a reminder three days before the deadline, declared with `x-openregister-notifications`. +4. A "My confirmation requests" page for the owner with Confirm and Edit per entry; confirming sets the entry's `lastConfirmedAt`, editing and saving marks the request corrected. +5. A round page for the information manager with the answer counts and the overdue owners. + +## Out of scope + +- Free-form survey questions. A request asks one thing: is this entry right. Custom questions per object type belong to a later change. +- An approval step before an owner's correction is saved. Owners already have edit rights on their entries; the round records that they changed it. +- Owners without a Nextcloud account: portaliq's contribution contract covers outside parties (open change `portal-contribution`). + +## Risks + +- An entry without an owner cannot be asked. The round lists those entries separately so the information manager assigns owners first. diff --git a/openspec/changes/landscape-owner-attestation/specs/owner-attestation/spec.md b/openspec/changes/landscape-owner-attestation/specs/owner-attestation/spec.md new file mode 100644 index 000000000..0d47ca6bd --- /dev/null +++ b/openspec/changes/landscape-owner-attestation/specs/owner-attestation/spec.md @@ -0,0 +1,65 @@ +# owner-attestation specification + +**Status**: proposed +**Scope**: stackiq +**OpenSpec changes**: +- landscape-owner-attestation + +## Purpose + +Owners confirm or correct their catalogue entries in rounds an information manager starts and follows. Matrix row `stackiq:land-data-quality-survey`. + +## ADDED Requirements + +### Requirement: REQ-OAT-001 An information manager starts a confirmation round for a scope + +An organisation admin SHALL start a round for the organisation's usages or for its products, with a deadline. Stackiq SHALL create one request per entry and owner, resolved to a Nextcloud user, and SHALL report the entries that have no owner with an account. + +#### Scenario: A round over the applications in use +@e2e tests/e2e/workflows/owner-attestation.spec.ts + +- **GIVEN** a municipality with four usages that have owners and one usage without +- **WHEN** its information manager starts a confirmation round for applications in use with a deadline in two weeks +- **THEN** the round shows four pending requests +- **AND** it lists the one usage without an owner under unassigned + +### Requirement: REQ-OAT-002 Each owner is notified and reminded + +Stackiq SHALL notify an owner in Nextcloud and by email when a request for them is created, and SHALL remind them three days before the deadline while the request is pending. + +#### Scenario: An application owner gets the request +@e2e exclude Delivered by OpenRegister's notification engine; tests/Unit/Settings/OwnerAttestationFragmentTest.php asserts the rule, its trigger and its field recipient. + +- **GIVEN** a round with a request for application owner Anna +- **WHEN** the request is created +- **THEN** Anna receives a Nextcloud notification naming the entry + +### Requirement: REQ-OAT-003 An owner confirms or corrects each entry + +An owner SHALL confirm an entry from My confirmation requests, which sets the entry's `lastConfirmedAt` and marks the request confirmed. When the owner edits and saves the entry instead, the request SHALL be marked corrected. An edit by someone else SHALL NOT answer the owner's request. A request still pending after the deadline SHALL become overdue. + +#### Scenario: Confirming from the list +@e2e tests/e2e/workflows/owner-attestation.spec.ts + +- **GIVEN** Anna has a pending request for the usage of application X +- **WHEN** she opens My confirmation requests and clicks Confirm +- **THEN** the request reads confirmed +- **AND** the usage shows today as last confirmed + +#### Scenario: Correcting by editing +@e2e exclude Listener behaviour; tests/Unit/Listener/AttestationCorrectionListenerTest.php covers the owner's edit and a colleague's edit with the real event class. + +- **GIVEN** Anna has a pending request for the usage of application X +- **WHEN** she changes its version and saves +- **THEN** the request reads corrected + +### Requirement: REQ-OAT-004 The round shows who answered and who is overdue + +The round page SHALL show how many requests are pending, confirmed, corrected and overdue, and list the requests grouped by status with their owners. + +#### Scenario: Following up overdue owners +@e2e tests/e2e/workflows/owner-attestation.spec.ts + +- **GIVEN** a round past its deadline with one request still pending +- **WHEN** the overdue job has run and the information manager opens the round +- **THEN** the round shows one overdue request with its owner diff --git a/openspec/changes/landscape-owner-attestation/tasks.md b/openspec/changes/landscape-owner-attestation/tasks.md new file mode 100644 index 000000000..3b1fb8c06 --- /dev/null +++ b/openspec/changes/landscape-owner-attestation/tasks.md @@ -0,0 +1,53 @@ +# Tasks: landscape-owner-attestation + +## Implementation tasks + +### Task 1: Round and request schemas with notifications +- **spec_ref**: openspec/changes/landscape-owner-attestation/specs/owner-attestation/spec.md#requirement-req-oat-002-each-owner-is-notified-and-reminded +- **files**: `lib/Settings/register.d/owner-attestation.json` +- **acceptance_criteria**: + - GIVEN the merged register WHEN it is imported THEN attestationRound and attestationRequest exist with their lifecycle and notification rules +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Settings/OwnerAttestationFragmentTest.php`) + +### Task 2: Start a round +- **spec_ref**: openspec/changes/landscape-owner-attestation/specs/owner-attestation/spec.md#requirement-req-oat-001-an-information-manager-starts-a-confirmation-round-for-a-scope +- **files**: `lib/Service/AttestationService.php`, `lib/Controller/AttestationController.php`, `appinfo/routes.php`, `lib/AppInfo/Application.php` +- **acceptance_criteria**: + - GIVEN four usages with owners and one without WHEN an organisation admin starts a round THEN four requests exist and one entry is reported unassigned + - GIVEN a user who is not an organisation admin WHEN they post a round THEN the answer is 403 +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Service/AttestationServiceTest.php`, `tests/Unit/Controller/AttestationControllerTest.php`) + +### Task 3: Confirm, correct and overdue +- **spec_ref**: openspec/changes/landscape-owner-attestation/specs/owner-attestation/spec.md#requirement-req-oat-003-an-owner-confirms-or-corrects-each-entry +- **files**: `lib/Controller/AttestationController.php`, `lib/Listener/AttestationCorrectionListener.php`, `lib/BackgroundJob/AttestationOverdueJob.php`, `appinfo/info.xml` +- **acceptance_criteria**: + - GIVEN a pending request WHEN its owner confirms THEN the entry's lastConfirmedAt is today and the request is confirmed + - GIVEN a pending request WHEN another user edits the entry THEN the request stays pending + - GIVEN a pending request past its deadline WHEN the job runs THEN it is overdue +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Listener/AttestationCorrectionListenerTest.php` with the real ObjectUpdatedEvent class, `tests/Unit/BackgroundJob/AttestationOverdueJobTest.php`) + +### Task 4: Owner and round pages +- **spec_ref**: openspec/changes/landscape-owner-attestation/specs/owner-attestation/spec.md#requirement-req-oat-004-the-round-shows-who-answered-and-who-is-overdue +- **files**: `src/manifest.d/owner-attestation.json`, `src/dialogs/StartAttestationRoundDialog.vue`, `l10n/en.json`, `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN an owner with two pending requests WHEN they open My confirmation requests THEN both show with Confirm + - GIVEN a round with answers WHEN the information manager opens it THEN it shows confirmed, corrected, pending and overdue counts +- [ ] Implement +- [ ] Test (Playwright `tests/e2e/workflows/owner-attestation.spec.ts`) + +### Task 5: Documentation +- **spec_ref**: openspec/changes/landscape-owner-attestation/specs/owner-attestation/spec.md#requirement-req-oat-001-an-information-manager-starts-a-confirmation-round-for-a-scope +- **files**: `docs/features/owner-attestation.md`, `docs/images/confirmation-round.png` +- **acceptance_criteria**: + - GIVEN the docs site WHEN a reader opens Confirmation rounds THEN starting a round, answering and following it are explained with a screenshot +- [ ] Implement +- [ ] Test (docs build, screenshot with Playwright) + +## Verification + +- `openspec validate landscape-owner-attestation --type change --strict` passes. +- `composer check:strict` and `npm run lint` pass; the PHPUnit and Playwright cases above pass. +- English and Dutch strings for every new label and notification (ADR-005); docs with a screenshot (ADR-010). From 8ff4a82d537a380fe0b3997e490b07d30a4054ca Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:56:26 +0200 Subject: [PATCH 13/15] docs(openspec): landscape-ai-system-inventory, AI systems with AI Act classification and evidence --- .../.openspec.yaml | 2 + .../landscape-ai-system-inventory/design.md | 49 +++++++++++++++++++ .../landscape-ai-system-inventory/proposal.md | 42 ++++++++++++++++ .../specs/ai-system-inventory/spec.md | 46 +++++++++++++++++ .../landscape-ai-system-inventory/tasks.md | 43 ++++++++++++++++ 5 files changed, 182 insertions(+) create mode 100644 openspec/changes/landscape-ai-system-inventory/.openspec.yaml create mode 100644 openspec/changes/landscape-ai-system-inventory/design.md create mode 100644 openspec/changes/landscape-ai-system-inventory/proposal.md create mode 100644 openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md create mode 100644 openspec/changes/landscape-ai-system-inventory/tasks.md diff --git a/openspec/changes/landscape-ai-system-inventory/.openspec.yaml b/openspec/changes/landscape-ai-system-inventory/.openspec.yaml new file mode 100644 index 000000000..7f2ad572a --- /dev/null +++ b/openspec/changes/landscape-ai-system-inventory/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-27 diff --git a/openspec/changes/landscape-ai-system-inventory/design.md b/openspec/changes/landscape-ai-system-inventory/design.md new file mode 100644 index 000000000..46e548dc1 --- /dev/null +++ b/openspec/changes/landscape-ai-system-inventory/design.md @@ -0,0 +1,49 @@ +# Design: landscape-ai-system-inventory + +Read at development `49e65cb4`. + +## Context + +The catalogue holds applications (`module`, `lib/Settings/softwarecatalogus_register.json:6779` schema) and organisations' usages of them (`usage`, `:2656`). An AI system either is a product of its own or runs inside an application; in both cases the organisation needs to see it next to the application and classify it. New schemas go in a fragment (ADR-037) that appends them to the `stackiq` register (`SettingsService::loadSettings()`, `lib/Service/SettingsService.php:1653-1680`). + +## D1. The aiSystem schema + +`lib/Settings/register.d/ai-system-inventory.json`, schema.org type `SoftwareApplication` with `applicationCategory` AI: + +| property | type | notes | +|---|---|---| +| `name` | string, required | | +| `description` | string, markdown | | +| `kind` | enum `AI agent`, `AI model`, `AI feature` | facetable | +| `module` | `$ref module` | the application it runs in or supports, `inversedBy: aiSystems` | +| `provider` | `$ref organization` | the supplier | +| `purpose` | string | what it decides or produces | +| `aiActRiskCategory` | enum `prohibited`, `high risk`, `limited risk`, `minimal risk`, `not yet assessed`, default `not yet assessed` | facetable | +| `aiActRole` | enum `provider`, `deployer` | | +| `algorithmRegisterUrl` | string, format uri | the entry at algoritmes.overheid.nl | +| `assessedOn` | date | | +| `status` | enum `in development`, `in use`, `withdrawn`, with an `x-openregister-lifecycle` on those exact values | | + +Configuration: `allowFiles: true`, `allowedTags`: `FRIA`, `Technical documentation`, `Human oversight`, `Logging`. Authorization copied from `usage`: the organisation reads and edits its own AI systems (`_organisation` match); suppliers read those whose `provider` is their organisation. + +Rejected: a new value `AI system` in `module.type`. The act's fields (category, role, assessment) do not belong on every application, and one application can carry several AI features. + +## D2. Pages + +`src/manifest.d/ai-systems.json`: `AiSystems` (`/ai-systems`, index, columns name, kind, module, aiActRiskCategory, status, `filterMenu: true`, quick filters per risk category) and `AiSystemDetail` (`/ai-systems/:id`: data, files with the four tags, related, history). A menu child "AI systems" under Applications (ADR-097). On `ModuleDetail` (`src/manifest.json:491`) an `object-list` `md-ai-systems` with filter `{ "module": "@objectId" }`. + +## D3. The missing assessment badge + +A computed flag through OpenRegister's calculation dialect if it can test for an attached file with a tag; otherwise the detail page shows a `CnStatusBadge` from a small check in a custom body widget `AiActChecklist` (`src/components/ai/AiActChecklist.vue`) that lists the four tags and marks which have a file. The index quick filter "High risk without FRIA" uses the same rule through the list's filter on a boolean `hasFria` that the widget cannot set; so the design writes `hasFria` from a listener on file add and remove (`lib/Listener/AiSystemFileListener.php`) only if the calculation dialect cannot express it. + +## Declarative versus imperative + +The schema, lifecycle, tags and pages are declarative (ADR-031). The file-presence flag is the one piece that may need a listener, decided at build time by what OpenRegister's calculation dialect supports. + +## Seed data + +Two demo AI systems: a chat assistant (AI agent, limited risk) inside a demo application, and a scoring model (AI model, high risk) without a FRIA, so the badge shows. + +## Risks + +- A supplier may register the same AI feature for its product that a municipality registered for its usage. The detail page shows provider and organisation; `operations-record-reconciliation` merges duplicates. diff --git a/openspec/changes/landscape-ai-system-inventory/proposal.md b/openspec/changes/landscape-ai-system-inventory/proposal.md new file mode 100644 index 000000000..7f9132723 --- /dev/null +++ b/openspec/changes/landscape-ai-system-inventory/proposal.md @@ -0,0 +1,42 @@ +--- +kind: config +depends_on: + - landscape-usage-registration +--- + +# Register the AI systems you use and classify them under the AI Act + +## Summary + +An information manager registers the AI agents, AI models and AI features the organisation uses, links each to the application it runs in, and records its EU AI Act risk category, the organisation's role under the act and the documents the act asks for. The application page shows its AI systems, and an AI systems list filters on risk category, so a privacy officer sees which high-risk systems still lack their assessment. + +## Why + +Rows from the stackiq matrix: + +- `stackiq:land-ai-agent-inventory`, "Register the AI agents and AI models the organisation uses and link them to the applications and processes they support." Rated no. SAP LeanIX rates yes: https://help.sap.com/docs/leanix/ea/application-modeling-guidelines (AI agent is an application subtype, AI model an IT component subtype), with the changelog row https://updates.leanix.net/announcements/discover-verify-and-govern-ai-assets-with-sap-ai-agent-hub. Core area (landscape). +- `stackiq:comp-ai-act-classification`, "Classify the AI systems in the landscape by EU AI Act risk category and keep the evidence the act requires." Rated no, no competitor yes. Roadmap demand: https://roadmap.leanix.net/c/812-meta-model-eu-ai-act-extension. It rides with `land-ai-agent-inventory`: its whole capability is a risk category and evidence on the AI system record that change adds. + +## What stackiq has today + +- No schema for AI agents, models or systems; the register's catalogue schemas are listed at `lib/Settings/softwarecatalogus_register.json:817` onwards. +- `module.type` distinguishes Application from System software only. +- Evidence documents already hang on records through Nextcloud files (`allowFiles`, for example `usage` with tags DPIA, Contract, Verwerkingsovereenkomst), and `module.dpiaDocumentRef` links a DPIA. + +## What this change builds + +1. A schema `aiSystem`: name, description, kind (AI agent, AI model, AI feature), the application it runs in or supports, the supplier, the purpose, the EU AI Act risk category (prohibited, high risk, limited risk, minimal risk, not yet assessed), the organisation's role (provider or deployer), a link to the entry in the Dutch algorithm register, the date of the last assessment, and a status. +2. File tags on `aiSystem` for the documents the act asks of a deployer of a high-risk system: fundamental rights impact assessment, technical documentation from the provider, human oversight procedure, logging arrangement. +3. An AI systems list with filters on kind and risk category, and an AI systems section on the application page. +4. A warning badge on a high-risk AI system that has no fundamental rights impact assessment file. + +## Out of scope + +- Discovering AI systems automatically. The matrix category says stackiq is not a discovery agent. +- Publishing to the Dutch algorithm register (algoritmes.overheid.nl). Stackiq stores the link; exchange with that register is integriq's. +- Linking AI systems to business processes: `architecture-process-mapping` adds processes; a relation from `aiSystem` follows once that schema exists. +- Legal advice on the category. The field records the organisation's own classification. + +## Risks + +- The AI Act's categories and deployer duties may be refined by guidance. The enum and the file tags live in the fragment and change with a pull request. diff --git a/openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md b/openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md new file mode 100644 index 000000000..a4d758997 --- /dev/null +++ b/openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md @@ -0,0 +1,46 @@ +# ai-system-inventory specification + +**Status**: proposed +**Scope**: stackiq +**OpenSpec changes**: +- landscape-ai-system-inventory + +## Purpose + +The organisation keeps its AI agents, models and features next to the applications they run in, with their EU AI Act classification and evidence. Matrix rows `stackiq:land-ai-agent-inventory` and `stackiq:comp-ai-act-classification`. + +## ADDED Requirements + +### Requirement: REQ-AIS-001 An organisation registers the AI systems it uses next to their applications + +Stackiq SHALL store an AI system with its name, kind (AI agent, AI model or AI feature), the application it runs in or supports, the supplier, its purpose and a status, and the application page SHALL list the AI systems linked to it. + +#### Scenario: An information manager registers a chat assistant +@e2e tests/e2e/workflows/ai-systems.spec.ts + +- **GIVEN** the municipality uses application X, which has a built-in chat assistant +- **WHEN** the information manager opens AI systems, clicks Add and saves "Chat assistant" of kind AI feature linked to X +- **THEN** the page of X lists "Chat assistant" in its AI systems section + +### Requirement: REQ-AIS-002 An AI system carries its AI Act classification and evidence + +An AI system SHALL record its EU AI Act risk category (prohibited, high risk, limited risk, minimal risk or not yet assessed), the organisation's role under the act, the date of the last assessment and a link to its algorithm register entry, and SHALL hold evidence files tagged FRIA, Technical documentation, Human oversight and Logging. The AI systems list SHALL filter on risk category. + +#### Scenario: A privacy officer lists the high-risk systems +@e2e tests/e2e/workflows/ai-systems.spec.ts + +- **GIVEN** two AI systems, one high risk and one minimal risk +- **WHEN** the privacy officer filters the AI systems list on high risk +- **THEN** only the high-risk system remains + +### Requirement: REQ-AIS-003 A high-risk AI system without a fundamental rights impact assessment is flagged + +The detail page of an AI system SHALL show which of the four evidence tags have a file, and a high-risk AI system without a FRIA file SHALL show a warning on its page and in the list. + +#### Scenario: The missing assessment shows +@e2e tests/e2e/workflows/ai-systems.spec.ts + +- **GIVEN** a high-risk AI system with technical documentation but no FRIA file +- **WHEN** the privacy officer opens its page +- **THEN** the evidence checklist marks FRIA as missing +- **AND** the AI systems list shows a warning on that row diff --git a/openspec/changes/landscape-ai-system-inventory/tasks.md b/openspec/changes/landscape-ai-system-inventory/tasks.md new file mode 100644 index 000000000..83795d786 --- /dev/null +++ b/openspec/changes/landscape-ai-system-inventory/tasks.md @@ -0,0 +1,43 @@ +# Tasks: landscape-ai-system-inventory + +## Implementation tasks + +### Task 1: The aiSystem schema +- **spec_ref**: openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md#requirement-req-ais-001-an-organisation-registers-the-ai-systems-it-uses-next-to-their-applications +- **files**: `lib/Settings/register.d/ai-system-inventory.json`, `lib/Settings/stackiq_mock_register.json` +- **acceptance_criteria**: + - GIVEN the merged register WHEN it is imported THEN the stackiq register lists aiSystem with its lifecycle and file tags +- [ ] Implement +- [ ] Test (PHPUnit `tests/Unit/Settings/AiSystemFragmentTest.php`) + +### Task 2: Pages and the application page section +- **spec_ref**: openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md#requirement-req-ais-002-an-ai-system-carries-its-ai-act-classification-and-evidence +- **files**: `src/manifest.d/ai-systems.json`, `src/manifest.json` (ModuleDetail list), `l10n/en.json`, `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN an application with one AI feature WHEN its page opens THEN the AI systems section lists it with its risk category + - GIVEN the AI systems list WHEN the user filters on high risk THEN only high-risk systems remain +- [ ] Implement +- [ ] Test (Playwright `tests/e2e/workflows/ai-systems.spec.ts`) + +### Task 3: Evidence checklist and missing FRIA flag +- **spec_ref**: openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md#requirement-req-ais-003-a-high-risk-ai-system-without-a-fundamental-rights-impact-assessment-is-flagged +- **files**: `src/components/ai/AiActChecklist.vue`, `src/customComponents.js`, `lib/Listener/AiSystemFileListener.php` (only if the calculation dialect cannot express it) +- **acceptance_criteria**: + - GIVEN a high-risk AI system without a FRIA file WHEN its page opens THEN the checklist marks FRIA missing and the list shows the warning + - GIVEN a FRIA file is attached WHEN the page reloads THEN the warning is gone +- [ ] Implement +- [ ] Test (vitest `tests/vitest/aiActChecklist.spec.js`; PHPUnit for the listener if built) + +### Task 4: Documentation +- **spec_ref**: openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md#requirement-req-ais-001-an-organisation-registers-the-ai-systems-it-uses-next-to-their-applications +- **files**: `docs/features/ai-systems.md`, `docs/images/ai-systems.png` +- **acceptance_criteria**: + - GIVEN the docs site WHEN a reader opens AI systems THEN registering, classifying and the evidence checklist are explained with a screenshot +- [ ] Implement +- [ ] Test (docs build, screenshot with Playwright) + +## Verification + +- `openspec validate landscape-ai-system-inventory --type change --strict` passes. +- `composer check:strict` and `npm run lint` pass; the PHPUnit, vitest and Playwright cases above pass. +- English and Dutch strings for every new label (ADR-005); docs with a screenshot (ADR-010). From d6e1f2657eb13831effb2897e5306866c2af906a Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 19:57:20 +0200 Subject: [PATCH 14/15] chore(parity): OpenSpec-pass decisions and matrix states for the first 13 changes --- openspec/parity/capabilities.json | 190 ++++---- openspec/parity/gap-decisions.json | 674 +++++++++++++++++++++++++++++ 2 files changed, 769 insertions(+), 95 deletions(-) create mode 100644 openspec/parity/gap-decisions.json diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index fc289e17a..5c51959fa 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -366,7 +366,7 @@ "topdesk": "partial", "stackiq": "partial", "built": { - "state": "built", + "state": "specified", "evidence": "src/manifest.json:592 Modules page (FacetedCatalogIndexView, schema module) with the library CnIndexPage create form at src/views/FacetedCatalogIndexView.vue:108; lib/Settings/softwarecatalogus_register.json:6777 module schema has name, shortDescription/longDescription and provider (Supplier) but NO status property; status lives on usage (register.json:2654, enum Acquisition..In production) which has no page", "owner": "ConductionNL/stackiq" }, @@ -375,7 +375,7 @@ "providerHow": "read-from-code", "feature": "software-landscape-register", "featureConfidence": "high", - "note": "An application with supplier and description can be registered on the Modules page, but the module schema has no status field, and the per-organisation usage that carries a status has no page to create it on. The Modules list also cannot open ModuleDetail: its standalone CnIndexPage (FacetedCatalogIndexView.vue:108-117) binds no @view/@row-click, so the View action is inert.", + "note": "An application with supplier and description can be registered on the Modules page, but the module schema has no status field, and the per-organisation usage that carries a status has no page to create it on. The Modules list also cannot open ModuleDetail: its standalone CnIndexPage (FacetedCatalogIndexView.vue:108-117) binds no @view/@row-click, so the View action is inert. Specified in openspec/changes/landscape-usage-registration (OpenSpec pass 2026-09-27).", "evidence": { "vng-softwarecatalogus": "https://www.softwarecatalogus.nl/node/30355: \"klik dan op de knop + achter de beschrijving van het pakket om het pakket toe te voegen aan je omgeving ... Pakketversie ... Referentiecomponenten ... Vul onder Planning bij Status in gebruik in\" (read 2026-09-26); https://www.softwarecatalogus.nl/hoe-werkt-de-catalogus: \"Wanneer Gemeenten en samenwerkingen hun applicatielandschap hebben ingevoerd, wordt deze automatisch geplot op de GEMMA referentiecomponentenkaart\" (read 2026-09-26). Reached on: Mijn softwarecatalogus > Pakketten > Voeg pakket toe.", "sap-leanix": "https://help.sap.com/docs/leanix/ea/application-modeling-guidelines: 'Applications are software systems or programs that process or analyze business data'; application fact sheet with description and lifecycle, supplier via 'provider -> IT component -> application relation' (https://help.sap.com/docs/leanix/ea/provider-modeling-guidelines) (read 2026-09-26). Reached on: Inventory > Application fact sheet.", @@ -397,7 +397,7 @@ "topdesk": "unknown", "stackiq": "partial", "built": { - "state": "built", + "state": "specified", "evidence": "register.json:1135 suite schema with applications[] (register.json:1231); src/manifest.json:674 SuiteDetail with suite-related panel; no schema breaks one application into sub-modules (module IS the application, register.json:6777 title 'Application')", "owner": "ConductionNL/stackiq" }, @@ -406,7 +406,7 @@ "providerHow": "read-from-code", "feature": "software-landscape-register", "featureConfidence": "low", - "note": "Stackiq's 'module' is the whole application, so there is no breakdown of one application into modules. The nearest thing is a suite (product) listing its applications, which answers 'which module belongs to which product' but not the decomposition.", + "note": "Stackiq's 'module' is the whole application, so there is no breakdown of one application into modules. The nearest thing is a suite (product) listing its applications, which answers 'which module belongs to which product' but not the decomposition. Specified in openspec/changes/landscape-application-components (OpenSpec pass 2026-09-27).", "evidence": { "sap-leanix": "https://help.sap.com/docs/leanix/ea/application-modeling-guidelines: 'Applications often consist of multiple entities or modules within a common ecosystem or platform', modeled as parent/child hierarchy, e.g. Adobe Photoshop as child of Adobe Creative Cloud (read 2026-09-26). Reached on: Application fact sheet > parent/child relations.", "stackiq": "register.json:1135 suite schema with applications[] (register.json:1231); src/manifest.json:674 SuiteDetail with suite-related panel; no schema breaks one application into sub-modules (module IS the application, register.json:6777 title 'Application')", @@ -521,14 +521,14 @@ "topdesk": "unknown", "stackiq": "no", "built": { - "state": "specified", + "state": "decided-no", "evidence": "register.json:1034 sector schema (name, description only); no schema property references #/components/schemas/sector (grep found none), no manifest page for sector; only the admin schema mapping in src/views/settings/sections/OpenRegisterIntegration.vue:395", "owner": "ConductionNL/stackiq" }, "reachedOn": "nothing reaches it", "provider": "stackiq", "providerHow": "read-from-code", - "note": "A sector schema exists, but no application, service or organisation can be tagged with a sector and no page lists sectors.", + "note": "A sector schema exists, but no application, service or organisation can be tagged with a sector and no page lists sectors. Decided no (OpenSpec pass 2026-09-27): Recorded non-goal: aanvullende-informatie.md:511 lists VNG issue #4 (classify packages on the reference architectures of the relevant sectors, so buyers from several sectors find them) with the analysis \"Classificeren op meerdere sectorale referentiearchitecturen, buiten scope\", and issues/4.md carries the label \"Buiten scope oplevering\" and is closed. Tagging applications with sectors is the entry to that out-of-scope classification. The unused sector schema (lib/Settings/softwarecatalogus_register.json:1036) is what the specified state pointed at; no change directory existed.", "evidence": { "stackiq": "register.json:1034 sector schema (name, description only); no schema property references #/components/schemas/sector (grep found none), no manifest page for sector; only the admin schema mapping in src/views/settings/sections/OpenRegisterIntegration.vue:395", "topdesk": "unknown: the TOPdesk documentation is about service management and does not cover this; searched the full-text search index of docs.topdesk.com (https://docs.topdesk.com/en/js/fuzzydata.js, 987 pages) (read 2026-09-26)", @@ -550,7 +550,7 @@ "topdesk": "partial", "stackiq": "partial", "built": { - "state": "built", + "state": "specified", "evidence": "register.json:6856 module.contactPerson is a single related contactPerson; register.json:1786 contactPerson has free-text role (job title) and a roles enum of catalogue roles (Aanbod-beheerder, Gebruik-beheerder, ...), no business/technical owner distinction; shown on ModuleDetail md-data (src/manifest.json:500 lists the stale key 'contactpersoon', not 'contactPerson')", "owner": "ConductionNL/stackiq" }, @@ -559,7 +559,7 @@ "providerHow": "read-from-code", "feature": "software-landscape-register", "featureConfidence": "low", - "note": "One contact person per application can be set, but there is no separate business owner and technical owner. ModuleDetail's data widget includes 'contactpersoon', a key the schema no longer has, so the contact may not show there.", + "note": "One contact person per application can be set, but there is no separate business owner and technical owner. ModuleDetail's data widget includes 'contactpersoon', a key the schema no longer has, so the contact may not show there. Specified in openspec/changes/landscape-usage-registration (OpenSpec pass 2026-09-27).", "evidence": { "sap-leanix": "https://help.sap.com/docs/leanix/ea/subscription-roles: 'Define roles that map to your organization's positions, such as application owner', with subscription types 'Responsible, Accountable, Observer' per fact sheet (read 2026-09-26). Reached on: Fact sheet > Subscriptions; Administration > Subscription Roles.", "stackiq": "register.json:6856 module.contactPerson is a single related contactPerson; register.json:1786 contactPerson has free-text role (job title) and a roles enum of catalogue roles (Aanbod-beheerder, Gebruik-beheerder, ...), no business/technical owner distinction; shown on ModuleDetail md-data (src/manifest.json:500 lists the stale key 'contactpersoon', not 'contactPerson')", @@ -640,7 +640,7 @@ "topdesk": "partial", "stackiq": "partial", "built": { - "state": "built", + "state": "specified", "evidence": "src/manifest.json:491 ModuleDetail: md-versions (:504), md-compliance (:503), md-related generic Related panel (:502); no contract widget (catalogContract links to service/usage, register.json:3250, not to module); md-data include lists stale keys beschrijvingKort/beschrijvingLang/contactpersoon (:500)", "owner": "ConductionNL/stackiq" }, @@ -649,7 +649,7 @@ "providerHow": "read-from-code", "feature": "software-landscape-register", "featureConfidence": "medium", - "note": "The application page shows versions and compliance claims, and usages only as untyped entries in the generic Related panel. Contracts are not shown. The page cannot be opened from the Applications list itself, and its data widget asks for three field names the schema no longer has, so the descriptions do not render.", + "note": "The application page shows versions and compliance claims, and usages only as untyped entries in the generic Related panel. Contracts are not shown. The page cannot be opened from the Applications list itself, and its data widget asks for three field names the schema no longer has, so the descriptions do not render. Specified in openspec/changes/landscape-application-page (OpenSpec pass 2026-09-27).", "evidence": { "vng-softwarecatalogus": "https://www.softwarecatalogus.nl/pakket/archi: package page shows versions with status and start dates, \"Pakket geschikt voor (GEMMA 2) Ingevuld door (28)\", and per version the mandatory and recommended standards with support, compliancy and testrapport (read 2026-09-26). No contracts on the page. Reached on: Alle pakketten > package name.", "sap-leanix": "https://help.sap.com/docs/leanix/ea/application-modeling-guidelines and https://help.sap.com/docs/leanix/ea/adding-and-editing-data-in-fact-sheets: the application fact sheet holds lifecycle, relations to IT components, organizations, interfaces, cost on relations, and a Relations Explorer on one fact sheet (read 2026-09-26). Reached on: Inventory > Application fact sheet.", @@ -731,14 +731,14 @@ "topdesk": "unknown", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "No survey, attestation or owner-confirmation code in lib/ or src/ (searched survey/enquete/confirm entry)", "owner": "ConductionNL/stackiq" }, "reachedOn": "nothing reaches it", "provider": "stackiq", "providerHow": "read-from-code", - "note": "Nothing asks owners to confirm or correct their entries.", + "note": "Nothing asks owners to confirm or correct their entries. Specified in openspec/changes/landscape-owner-attestation (OpenSpec pass 2026-09-27).", "evidence": { "sap-leanix": "https://help.sap.com/docs/leanix/ea/reviewing-responses: 'Review and approve survey responses before they are saved to fact sheets'; https://help.sap.com/docs/leanix/ea/application-modernization-collect-data: 'Create a new survey to get key information from application or business owners' (read 2026-09-26). Reached on: Surveys.", "bluedolphin": "https://help.bluedolphin.io/en/articles/11967524-create-a-survey: surveys 'gather input from stakeholders outside your core BlueDolphin users ... allowing external stakeholders to contribute directly to the Enterprise Architecture repository' on an object's questionnaire (read 2026-09-26). Reached on: Object > Questionnaire tab > Create survey.", @@ -760,14 +760,14 @@ "topdesk": "unknown", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "No completeness or data-quality score in lib/ or src/ (searched completeness/volledigheid/score outside reviews)", "owner": "ConductionNL/stackiq" }, "reachedOn": "nothing reaches it", "provider": "stackiq", "providerHow": "read-from-code", - "note": "No page scores how complete or current an entry is.", + "note": "No page scores how complete or current an entry is. Specified in openspec/changes/landscape-completeness-score (OpenSpec pass 2026-09-27).", "evidence": { "stackiq": "No completeness or data-quality score in lib/ or src/ (searched completeness/volledigheid/score outside reviews)", "topdesk": "unknown: the only readiness score described is the AI readiness score for the knowledge base; searched the full-text search index of docs.topdesk.com (https://docs.topdesk.com/en/js/fuzzydata.js, 987 pages) (read 2026-09-26)", @@ -907,7 +907,7 @@ "topdesk": "unknown", "stackiq": "partial", "built": { - "state": "built", + "state": "specified", "evidence": "register.json:3720 connection.nonMunicipalProvision -> element filtered gemmaType 'Buitengemeentenlijke voorziening'; no page for connection", "owner": "ConductionNL/stackiq" }, @@ -916,7 +916,7 @@ "providerHow": "read-from-code", "feature": "software-landscape-register", "featureConfidence": "low", - "note": "The field for a national provision exists on the connection schema, but with no connection page nobody can fill it in stackiq.", + "note": "The field for a national provision exists on the connection schema, but with no connection page nobody can fill it in stackiq. Specified in openspec/changes/connections-catalogue-pages (OpenSpec pass 2026-09-27).", "evidence": { "vng-softwarecatalogus": "https://www.softwarecatalogus.nl/Opvoeren%20koppeling%20iJw%20en%20iWmo: \"De richting van het berichtenverkeer, de landelijke voorziening waarmee gekoppeld is/wordt, in dit geval GGK\" (read 2026-09-26); https://www.softwarecatalogus.nl/hoe-werkt-de-catalogus: \"kunnen koppelingen tussen applicaties onderling en met Landelijke Voorzieningen vastgelegd worden\" (read 2026-09-26). Reached on: Mijn softwarecatalogus > Koppelingen > koppeling toevoegen.", "stackiq": "register.json:3720 connection.nonMunicipalProvision -> element filtered gemmaType 'Buitengemeentenlijke voorziening'; no page for connection", @@ -947,7 +947,7 @@ "providerHow": "read-from-code", "feature": "software-landscape-register", "featureConfidence": "medium", - "note": "There is no list of connections in the catalogue. The Integrations page lists outside integrations, a different thing.", + "note": "There is no list of connections in the catalogue. The Integrations page lists outside integrations, a different thing. Specified in openspec/changes/connections-catalogue-pages (OpenSpec pass 2026-09-27).", "evidence": { "vng-softwarecatalogus": "https://www.softwarecatalogus.nl/node/13683: \"Alle koppelingen ... staan de koppelingen van alle gemeenten en samenwerkingsverbanden ... Door te klikken op het icoontje rechts van een koppeling, krijg je nog enige detail informatie\" (read 2026-09-26). Reached on: Inlogmenu > Alle koppelingen (logged-in municipal users).", "stackiq": "No manifest page with schema connection (src/manifest.json and src/manifest.d/*.json); the Integrations page (src/manifest.d/connection-registry.json:23) lists integriq app_connection, explicitly not stackiq's connection schema (its _note)", @@ -969,7 +969,7 @@ "topdesk": "partial", "stackiq": "partial", "built": { - "state": "built", + "state": "specified", "evidence": "src/manifest.json:502 ModuleDetail md-related generic Related panel (OpenRegister /uses + /used merged into an Objects tab); register.json:7064 module.koppelingen is hideOnForm and not in md-data include (:500); lib/Controller/AangebodenGebruikController.php:208 GET /api/koppelingen-gebruik/{uuid} (public) has no caller in src/", "owner": "ConductionNL/stackiq" }, @@ -978,7 +978,7 @@ "providerHow": "read-from-code", "feature": "software-landscape-register", "featureConfidence": "low", - "note": "Connections that reference an application should appear among the untyped related objects on its page, but there is no connections section and nothing to open. The dedicated per-application endpoint is API only.", + "note": "Connections that reference an application should appear among the untyped related objects on its page, but there is no connections section and nothing to open. The dedicated per-application endpoint is API only. Specified in openspec/changes/connections-catalogue-pages (OpenSpec pass 2026-09-27).", "evidence": { "vng-softwarecatalogus": "https://www.softwarecatalogus.nl/node/13683: \"Mijn pakketoverzicht ... Onder het eerste tabblad zitten de pakketten en onder het tweede tabblad de koppelingen\" (read 2026-09-26); https://www.softwarecatalogus.nl/node/30890: \"Door op een applicatienaam in het Models venster te klikken, zie je in de Visualiser alle koppelingen tussen die applicatie met andere applicaties\" (in Archi after export) (read 2026-09-26). No per-application connection view inside the catalogue is described. Reached on: Mijn softwarecatalogus > Koppelingen.", "stackiq": "src/manifest.json:502 ModuleDetail md-related generic Related panel (OpenRegister /uses + /used merged into an Objects tab); register.json:7064 module.koppelingen is hideOnForm and not in md-data include (:500); lib/Controller/AangebodenGebruikController.php:208 GET /api/koppelingen-gebruik/{uuid} (public) has no caller in src/", @@ -1000,7 +1000,7 @@ "topdesk": "yes", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "No diagram rendering in src/ (no graph library; src/store/modules/view.js GET /api/views has no importer outside itself); lib/Service/ArchiMateExportService.php exports GEMMA views as ArchiMate XML without koppeling objects (no 'koppeling' in lib/Service/ArchiMate*)", "owner": "ConductionNL/stackiq" }, @@ -1009,7 +1009,7 @@ "providerHow": "read-from-code", "feature": "archimate-import-and-export", "featureConfidence": "low", - "note": "No page draws connections. The ArchiMate export can be opened in Archi, but it carries GEMMA views and usages, not the catalogue's connections.", + "note": "No page draws connections. The ArchiMate export can be opened in Archi, but it carries GEMMA views and usages, not the catalogue's connections. Specified in openspec/changes/connections-diagram-and-graph-export (OpenSpec pass 2026-09-27).", "evidence": { "sap-leanix": "https://help.sap.com/docs/leanix/ea/data-flow: data flow diagrams help 'understand how applications are connected, identify dependencies, and trace data movement between systems' (read 2026-09-26). Reached on: Diagrams > Data Flow Diagram.", "bluedolphin": "https://help.bluedolphin.io/en/articles/11967472-welcome-to-bluedolphin: 'insight into connections between business processes, applications, and their underlying infrastructure. This way, you can visualize chains and information flows'; https://help.bluedolphin.io/en/articles/11967545-spider-tool adds related objects with 'all existing relationships' to a view (read 2026-09-26). Reached on: Views > architecture view.", @@ -1067,7 +1067,7 @@ "reachedOn": "nothing reaches it", "provider": "stackiq", "providerHow": "read-from-code", - "note": "The type field exists but there is no connection list to filter.", + "note": "The type field exists but there is no connection list to filter. Specified in openspec/changes/connections-catalogue-pages (OpenSpec pass 2026-09-27).", "evidence": { "stackiq": "register.json:3563 connection.type enum (file transfer, digikoppeling, message que, webservices, api, ...) exists, but no connection list page to filter", "topdesk": "unknown: custom link types exist, but filtering relations by type is not described; https://tip.topdesk.com/c/90-graphical-overview-improvements is still under consideration; searched the full-text search index of docs.topdesk.com (https://docs.topdesk.com/en/js/fuzzydata.js, 987 pages) (read 2026-09-26)", @@ -1089,14 +1089,14 @@ "topdesk": "unknown", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "No API/interface schema in register.json (schemas listed at register.json:1034-7920); connection.type 'api' is only a transport label", "owner": "ConductionNL/stackiq" }, "reachedOn": "nothing reaches it", "provider": "stackiq", "providerHow": "read-from-code", - "note": "Stackiq has no record for an API an application exposes.", + "note": "Stackiq has no record for an API an application exposes. Specified in openspec/changes/connections-api-catalogue (OpenSpec pass 2026-09-27).", "evidence": { "sap-leanix": "https://help.sap.com/docs/leanix/ea/interface-modeling-guidelines: interface subtype 'API ... APIs provide functionalities accessible to external applications ... Examples: Metrics API, Import API', related to the providing application (read 2026-09-26). Reached on: Inventory > Interface fact sheet, subtype API.", "stackiq": "No API/interface schema in register.json (schemas listed at register.json:1034-7920); connection.type 'api' is only a transport label", @@ -1118,7 +1118,7 @@ "topdesk": "unknown", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "src/views/settings/sections/ArchiMateImportExport.vue:571 org export options are Modules, Deelnames, Gebruik only; lib/Controller/SettingsController.php:1685 exportOrgArchiMate; no koppeling handling in lib/Service/ArchiMate*", "owner": "ConductionNL/stackiq" }, @@ -1127,7 +1127,7 @@ "providerHow": "read-from-code", "feature": "archimate-import-and-export", "featureConfidence": "low", - "note": "The organisation ArchiMate export carries modules and usages but not connections, so an application's link graph cannot be exported.", + "note": "The organisation ArchiMate export carries modules and usages but not connections, so an application's link graph cannot be exported. Specified in openspec/changes/connections-diagram-and-graph-export (OpenSpec pass 2026-09-27).", "evidence": { "glpi": "source read at 11.0.9: front/impactcsv.php streams Glpi\\Csv\\ImpactCsvExport for an item, linked from the impact list view at src/Impact.php:393; the graph Download button src/Impact.php:1165 calls js/impact.js:2528 download, which writes PNG (js/impact.js:2539) or JPEG (js/impact.js:2546). Reached on: Appliance > Impact analysis tab, Download and CSV export.", "stackiq": "src/views/settings/sections/ArchiMateImportExport.vue:571 org export options are Modules, Deelnames, Gebruik only; lib/Controller/SettingsController.php:1685 exportOrgArchiMate; no koppeling handling in lib/Service/ArchiMate*", @@ -1817,14 +1817,14 @@ "topdesk": "unknown", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "grep for completeness/health score/quality score in lib and src: no hits; lib/Command/ReferencesAuditCommand.php:34 (occ stackiq:references:audit) only audits cross-app uuid references, it is not a scored rule set", "owner": "ConductionNL/stackiq" }, "reachedOn": "nothing reaches it", "provider": "stackiq", "providerHow": "read-from-code", - "note": "No rule-based score of register correctness or completeness exists on any page.", + "note": "No rule-based score of register correctness or completeness exists on any page. Specified in openspec/changes/landscape-completeness-score (OpenSpec pass 2026-09-27).", "evidence": { "stackiq": "grep for completeness/health score/quality score in lib and src: no hits; lib/Command/ReferencesAuditCommand.php:34 (occ stackiq:references:audit) only audits cross-app uuid references, it is not a scored rule set", "topdesk": "unknown: standards compliance of applications, BIO and DPIA are not covered; searched the full-text search index of docs.topdesk.com (https://docs.topdesk.com/en/js/fuzzydata.js, 987 pages) (read 2026-09-26)", @@ -2088,14 +2088,14 @@ "topdesk": "unknown", "stackiq": "partial", "built": { - "state": "built", + "state": "specified", "evidence": "register :6856 module.contactPerson -> contactPerson with x-relation-filter organization = @object.provider; catalogService and suite also carry contactPerson; src/manifest.json:500 ModuleDetail md-data include lists 'contactpersoon', 'beschrijvingKort', 'beschrijvingLang', which are not module properties (renamed to contactPerson/shortDescription/longDescription)", "owner": "ConductionNL/stackiq" }, "reachedOn": "Applications /modules create/edit form; not shown on ModuleDetail /modules/:id", "provider": "stackiq", "providerHow": "read-from-code", - "note": "Each product can point at its own contact person of the supplier through the form. The application page's data widget names the old Dutch keys, so the product's contact person (and its descriptions) do not show there; the same stale keys are on SuiteDetail (src/manifest.json:683).", + "note": "Each product can point at its own contact person of the supplier through the form. The application page's data widget names the old Dutch keys, so the product's contact person (and its descriptions) do not show there; the same stale keys are on SuiteDetail (src/manifest.json:683). Specified in openspec/changes/landscape-application-page (OpenSpec pass 2026-09-27).", "evidence": { "stackiq": "register :6856 module.contactPerson -> contactPerson with x-relation-filter organization = @object.provider; catalogService and suite also carry contactPerson; src/manifest.json:500 ModuleDetail md-data include lists 'contactpersoon', 'beschrijvingKort', 'beschrijvingLang', which are not module properties (renamed to contactPerson/shortDescription/longDescription)", "topdesk": "unknown: supplier contacts are registered per supplier (\"Registering a supplier contact\"); contacts per product are not described; searched the full-text search index of docs.topdesk.com (https://docs.topdesk.com/en/js/fuzzydata.js, 987 pages) (read 2026-09-26)", @@ -2266,7 +2266,7 @@ "topdesk": "unknown", "stackiq": "partial", "built": { - "state": "built", + "state": "specified", "evidence": "lib/Settings/softwarecatalogus_register.json usage.moduleVersion ($ref moduleVersion); read by src/views/LifecycleRoadmapView.vue:397 for EOL state; ModuleversieDetail mv-related shows related usages; no usage create/edit page in src/manifest.json", "owner": "ConductionNL/stackiq" }, @@ -2275,7 +2275,7 @@ "providerHow": "read-from-code", "feature": "lifecycle-and-end-of-support", "featureConfidence": "low", - "note": "The version an organisation runs is a field on its usage and drives the EOL badges, but no stackiq page lets the organisation set or change it.", + "note": "The version an organisation runs is a field on its usage and drives the EOL badges, but no stackiq page lets the organisation set or change it. Specified in openspec/changes/landscape-usage-registration (OpenSpec pass 2026-09-27).", "evidence": { "stackiq": "lib/Settings/softwarecatalogus_register.json usage.moduleVersion ($ref moduleVersion); read by src/views/LifecycleRoadmapView.vue:397 for EOL state; ModuleversieDetail mv-related shows related usages; no usage create/edit page in src/manifest.json", "topdesk": "unknown: versions in use are only possible as a self-defined field; searched the full-text search index of docs.topdesk.com (https://docs.topdesk.com/en/js/fuzzydata.js, 987 pages) (read 2026-09-26)", @@ -2480,7 +2480,7 @@ "topdesk": "partial", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "src/manifest.json:491 ModuleDetail widgets: md-data, md-files, md-related, md-compliance, md-versions, ReviewsPanel; no catalogContract list. catalogContract points at service and usage (register :3252), not at module, so the one-hop related panel cannot reach it", "owner": "ConductionNL/stackiq" }, @@ -2489,7 +2489,7 @@ "providerHow": "read-from-code", "feature": "contract-administration", "featureConfidence": "medium", - "note": "The application page has no contracts list, and a contract links to a usage and a service rather than the application, so there is no path from an application to its contracts in the UI.", + "note": "The application page has no contracts list, and a contract links to a usage and a service rather than the application, so there is no path from an application to its contracts in the UI. Specified in openspec/changes/landscape-application-page (OpenSpec pass 2026-09-27).", "evidence": { "glpi": "source read at 11.0.9: src/Appliance.php:99 and src/Software.php:131 add the Contract_Item tab (src/Contract_Item.php:43, install/mysql/glpi-empty.sql:1536 glpi_contracts_items) listing every contract of the application. Reached on: Management > Appliances > Contracts tab. Driven on the lab at 11.0.9 (2026-09-26): after linking the contract, the appliance Contracts tab showed \"Lab contract, 2025-01-01, 12 months -> 2025-12-31\".", "stackiq": "src/manifest.json:491 ModuleDetail widgets: md-data, md-files, md-related, md-compliance, md-versions, ReviewsPanel; no catalogContract list. catalogContract points at service and usage (register :3252), not at module, so the one-hop related panel cannot reach it", @@ -3249,14 +3249,14 @@ "topdesk": "yes", "stackiq": "no", "built": { - "state": "none", + "state": "decided-no", "evidence": "No OIDC/SAML code in lib/ or src/; stackiq creates local Nextcloud users with a password (lib/Service/Stackiq/ContactPersonHandler.php:292)", - "owner": "ConductionNL/stackiq" + "owner": "nextcloud/server" }, "reachedOn": "nothing reaches it in stackiq; Nextcloud's own user_oidc/user_saml apps would apply platform-wide", "provider": "nextcloud", "providerHow": "read-from-code", - "note": "Stackiq does nothing for single sign-on. A Nextcloud admin can add an identity provider app, but that is the platform, not a stackiq page.", + "note": "Stackiq does nothing for single sign-on. A Nextcloud admin can add an identity provider app, but that is the platform, not a stackiq page. Decided no (OpenSpec pass 2026-09-27): Platform capability: Nextcloud signs users in through its identity provider apps (user_oidc, user_saml) for every app, and stackiq users are Nextcloud users. built.owner corrected to nextcloud/server.", "evidence": { "glpi": "source read at 11.0.9: src/Auth.php:106 EXTERNAL (web server provided identity, for example a SAML or OIDC module in front of GLPI), src/Auth.php:107 CAS with phpCAS::client at src/Auth.php:557, and src/Auth.php:108 X509 certificates, next to LDAP at src/Auth.php:105. Reached on: Setup > Authentication > Other authentication methods.", "topdesk": "https://docs.topdesk.com/en/automatic-login-methods.html: \"Single Sign-on via SAML requirements TOPdesk uses OpenSAML 3 for authentication. You can connect all common IdP solutions which support SAML 2.0\" (read 2026-09-26). Reached on: Settings > Login Settings.", @@ -3278,14 +3278,14 @@ "topdesk": "yes", "stackiq": "no", "built": { - "state": "none", + "state": "decided-no", "evidence": "lib/Service/OrganizationSyncService.php and the 'Organization synchronization' admin section sync catalogue organisations to OpenRegister organisation entities, not users from a directory; no LDAP code in lib/", - "owner": "ConductionNL/stackiq" + "owner": "nextcloud/server" }, "reachedOn": "nothing reaches it in stackiq; Nextcloud's user_ldap would apply platform-wide", "provider": "nextcloud", "providerHow": "read-from-code", - "note": "Stackiq has no directory sync for users or groups. Nextcloud's LDAP app could do it platform-wide, outside stackiq.", + "note": "Stackiq has no directory sync for users or groups. Nextcloud's LDAP app could do it platform-wide, outside stackiq. Decided no (OpenSpec pass 2026-09-27): Platform capability: Nextcloud user_ldap keeps users and groups in step with a directory for every app, and stackiq users and groups are Nextcloud users and groups. built.owner corrected to nextcloud/server.", "evidence": { "glpi": "source read at 11.0.9: src/AuthLDAP.php:59 LDAP directories with user import and group import (src/AuthLDAP.php:2816 ldapImportGroup), and the CLI src/Glpi/Console/Ldap/SynchronizeUsersCommand.php:79 ldap:synchronize_users (alias ldap:sync at :80). Reached on: Administration > Users > LDAP directory link; Setup > Authentication > LDAP directories. Driven on the lab at 11.0.9 (2026-09-26): /front/ldap.php offers \"Bulk import users from a LDAP directory\" and \"Synchronizing already imported users\".", "stackiq": "lib/Service/OrganizationSyncService.php and the 'Organization synchronization' admin section sync catalogue organisations to OpenRegister organisation entities, not users from a directory; no LDAP code in lib/", @@ -3307,14 +3307,14 @@ "topdesk": "yes", "stackiq": "partial", "built": { - "state": "built", + "state": "decided-no", "evidence": "src/components/ContactpersonenList.vue:115 'Change Password' opens src/dialogs/ChangePasswordDialog.vue -> POST /api/contactpersonen/change-password -> lib/Controller/ContactpersonenController.php:718, self-reset allowed at :753; GET /api/me (:1578) used only by src/App.vue and OrganisationSwitcher", - "owner": "ConductionNL/stackiq" + "owner": "nextcloud/server" }, "reachedOn": "Organisations /organisaties card -> contact persons view -> Change Password on your own row", - "provider": "stackiq", + "provider": "nextcloud", "providerHow": "read-from-code", - "note": "A user can change their own password from their contact row in the organisation card, which is hard to find. There is no 'my account' page in stackiq; /api/me feeds only the organisation switcher. Nextcloud's personal settings do both natively.", + "note": "A user can change their own password from their contact row in the organisation card, which is hard to find. There is no 'my account' page in stackiq; /api/me feeds only the organisation switcher. Nextcloud's personal settings do both natively. Decided no (OpenSpec pass 2026-09-27): Platform capability: Nextcloud personal settings let every user change their password and see their account details. The stackiq half (change password from your own contact row) stays as built evidence. built.owner corrected to nextcloud/server, provider nextcloud.", "evidence": { "stackiq": "src/components/ContactpersonenList.vue:115 'Change Password' opens src/dialogs/ChangePasswordDialog.vue -> POST /api/contactpersonen/change-password -> lib/Controller/ContactpersonenController.php:718, self-reset allowed at :753; GET /api/me (:1578) used only by src/App.vue and OrganisationSwitcher", "topdesk": "https://docs.topdesk.com/en/editing-your-personal-profile.html: \"Click on Personal Profile . In the General and Private section, you can edit your personal information. In the Change password section, you can change your password\" (read 2026-09-26). Reached on: Profile picture > Personal Profile.", @@ -4024,14 +4024,14 @@ "topdesk": "partial", "stackiq": "no", "built": { - "state": "none", + "state": "decided-no", "evidence": "No discovery agent or agent-ingest endpoint in lib/ or appinfo/routes.php. The nearest capability is SBOM import per module version (lib/Controller/SbomController.php:129), which records components of a known release, not installed software.", "owner": "ConductionNL/stackiq" }, "reachedOn": "nothing reaches it", "provider": "stackiq", "providerHow": "read-from-code", - "note": "Stackiq is a catalogue, and nothing discovers installed software.", + "note": "Stackiq is a catalogue, and nothing discovers installed software. Decided no (OpenSpec pass 2026-09-27): Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" Agent-based discovery of installed software is a discovery agent.", "evidence": { "glpi": "source read at 11.0.9: native inventory receives glpi-agent submissions at src/Glpi/Controller/InventoryController.php:61 /Inventory (legacy :62 /front/inventory.php), processed by src/Glpi/Inventory/Inventory.php:106 with src/Glpi/Inventory/Asset/Software.php creating software and installations. The agent is the separate glpi-project/glpi-agent repository. Reached on: Administration > Inventory; Assets > Software.", "stackiq": "No discovery agent or agent-ingest endpoint in lib/ or appinfo/routes.php. The nearest capability is SBOM import per module version (lib/Controller/SbomController.php:129), which records components of a known release, not installed software.", @@ -4053,14 +4053,14 @@ "topdesk": "partial", "stackiq": "no", "built": { - "state": "none", + "state": "decided-no", "evidence": "No network scanning code in lib/ or routes (appinfo/routes.php).", "owner": "ConductionNL/stackiq" }, "reachedOn": "nothing reaches it", "provider": "stackiq", "providerHow": "read-from-code", - "note": "No device discovery.", + "note": "No device discovery. Decided no (OpenSpec pass 2026-09-27): Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" Network device discovery is a discovery agent.", "evidence": { "glpi": "source read at 11.0.9: src/Glpi/Inventory/Request.php:97 NETDISCOVERY_ACTION calls src/Glpi/Inventory/Request.php:237 networkDiscovery, importing devices found by the agent's network discovery; scheduling discovery tasks from the server goes through the HANDLE_NETDISCOVERY_TASK hook (src/Glpi/Inventory/Request.php:448), which the separate glpiinventory plugin implements. Reached on: Administration > Inventory; Assets > Network devices.", "stackiq": "No network scanning code in lib/ or routes (appinfo/routes.php).", @@ -4082,14 +4082,14 @@ "topdesk": "unknown", "stackiq": "no", "built": { - "state": "none", + "state": "decided-no", "evidence": "No SaaS or SSO-log discovery code in lib/; lib/Settings/connections.json has no such source.", "owner": "ConductionNL/stackiq" }, "reachedOn": "nothing reaches it", "provider": "stackiq", "providerHow": "read-from-code", - "note": "No discovery of unregistered SaaS use.", + "note": "No discovery of unregistered SaaS use. Decided no (OpenSpec pass 2026-09-27): Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" Discovering unregistered SaaS use is a discovery agent.", "evidence": { "sap-leanix": "https://help.sap.com/docs/leanix/ea/saas-discovery: 'SaaS discovery identifies your organization's SaaS applications through integrations with third-party systems like Single-Sign-on (SSO) ... Eliminate shadow IT and business-managed IT' (read 2026-09-26). Reached on: Discovery > SaaS discovery inbox.", "stackiq": "No SaaS or SSO-log discovery code in lib/; lib/Settings/connections.json has no such source.", @@ -4198,14 +4198,14 @@ "topdesk": "yes", "stackiq": "no", "built": { - "state": "none", + "state": "decided-no", "evidence": "No incident or request schema or endpoint in lib/Settings/softwarecatalogus_register.json or appinfo/routes.php.", "owner": "ConductionNL/stackiq" }, "reachedOn": "nothing reaches it", "provider": "stackiq", "providerHow": "read-from-code", - "note": "No ticketing.", + "note": "No ticketing. Decided no (OpenSpec pass 2026-09-27): Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" Logging incidents and requests is a service desk. Exchange with the organisation's service desk is specified under share-itsm-integration.", "evidence": { "glpi": "source read at 11.0.9: src/autoload/CFG_GLPI.php:301 ticket_types includes Appliance (line 305), so tickets link to an application through src/Item_Ticket.php:41, shown on the appliance Tickets tab (src/Appliance.php:105). Reached on: Assistance > Tickets; Appliance > Tickets tab. Driven on the lab at 11.0.9 (2026-09-26): the default Super-Admin profile lists Computer, Monitor, NetworkEquipment, Peripheral, Phone, Printer, Software, DCRoom, Rack, Enclosure and Database as associable to tickets, not Appliance, so an appliance shows no Tickets tab until an administrator adds it in the profile; rating kept.", "topdesk": "https://docs.topdesk.com/en/linking-assets-to-cards.html: \"On Call, Change (Activity) ... cards, you can link multiple assets\" (read 2026-09-26). Reached on: Call card > Links > Assets.", @@ -4227,14 +4227,14 @@ "topdesk": "yes", "stackiq": "no", "built": { - "state": "none", + "state": "decided-no", "evidence": "No change-request workflow for applications. The only approval flow is for contracts: src/components/contracts/ContractApprovalPanel.vue on ContractDetail via /api/contracts/{uuid}/approval (routes.php:35-37), delegated to decidiq.", "owner": "ConductionNL/stackiq" }, "reachedOn": "nothing reaches it", "provider": "stackiq", "providerHow": "read-from-code", - "note": "Contracts go through an approval, but changes to an application do not.", + "note": "Contracts go through an approval, but changes to an application do not. Decided no (OpenSpec pass 2026-09-27): Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" Change approval workflows are service desk change management.", "evidence": { "glpi": "source read at 11.0.9: changes link to the appliance (src/Appliance.php:107 Change_Item tab) and go through approvals, src/ChangeValidation.php:39 ChangeValidation extends CommonITILValidation. Reached on: Assistance > Changes; Appliance > Changes tab.", "topdesk": "https://docs.topdesk.com/en/requesting-a-change.html: \"A Preliminary Request for Change can only be dealt with as a Request for Change after it is authorized\" (read 2026-09-26). Reached on: Modules > Change Management.", @@ -4256,14 +4256,14 @@ "topdesk": "yes", "stackiq": "no", "built": { - "state": "none", + "state": "decided-no", "evidence": "catalogContract.contractType enum includes 'SLA' (lib/Settings/softwarecatalogus_register.json:3344) as a label only; no service-level target, measurement or breach fields in any schema.", "owner": "ConductionNL/stackiq" }, "reachedOn": "nothing reaches it", "provider": "stackiq", "providerHow": "read-from-code", - "note": "A contract can be typed as SLA, but no service level targets are recorded or tracked.", + "note": "A contract can be typed as SLA, but no service level targets are recorded or tracked. Decided no (OpenSpec pass 2026-09-27): Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" The competitor evidence tracks service desk response and resolution targets on calls and tickets.", "evidence": { "glpi": "source read at 11.0.9: src/SLM.php:42 service level management with src/SLA.php:44 SLA and OLA targets on tickets (install/mysql/glpi-empty.sql:7304 glpi_tickets.slas_id_ttr), assigned by business rules (src/RuleCommonITILObject.php:73) that can key on the linked appliance (src/RuleCommonITILObject.php:305 assign_appliance). Reached on: Setup > Service levels.", "topdesk": "https://docs.topdesk.com/en/track-when-you-respond-to-calls, response-times.html: \"you register and track how quickly your operators need to respond ... you need a Contract Management and SLM license\" (read 2026-09-26). Reached on: Contract Management and SLM.", @@ -4285,14 +4285,14 @@ "topdesk": "yes", "stackiq": "no", "built": { - "state": "none", + "state": "decided-no", "evidence": "No software-request flow. The only public intake is organisation self-registration (lib/Controller/IntakeController.php, POST /api/intake/register, routes.php:214), which no src/ page calls.", "owner": "ConductionNL/stackiq" }, "reachedOn": "nothing reaches it", "provider": "stackiq", "providerHow": "read-from-code", - "note": "End users cannot request software.", + "note": "End users cannot request software. Decided no (OpenSpec pass 2026-09-27): Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" A self-service portal where end users request software is a service desk.", "evidence": { "topdesk": "https://docs.topdesk.com/en/mobile-access-to-the-self-service-portal.html: \"The SSP layout is suited to be displayed in a mobile interface\" (read 2026-09-26); https://tip.topdesk.com/c/86-webshop-is-connected-with-asset-management: roadmap card in column \"Building\", \"End-user can order items which are in Asset Management\" (read 2026-09-26); pricing lists \"Self-Service Portal\" and \"Webshop\". Reached on: Self-Service Portal.", "stackiq": "No software-request flow. The only public intake is organisation self-registration (lib/Controller/IntakeController.php, POST /api/intake/register, routes.php:214), which no src/ page calls.", @@ -4523,7 +4523,7 @@ "originUrl": "https://www.tenderned.nl/aankondigingen/overzicht/418890", "stackiq": "partial", "built": { - "state": "built", + "state": "decided-no", "evidence": "lib/Settings/softwarecatalogus_register.json:7307 module authorization.read grants group public once publicationDate has passed (same rule as share-public-browse); no stackiq route or page serves it anonymously, the public surface is OpenRegister's objects API and the external VNG frontend", "owner": "ConductionNL/stackiq" }, @@ -4532,7 +4532,7 @@ "providerHow": "read-from-code", "feature": "offering-and-usage-listings", "featureConfidence": "medium", - "note": "Standard municipal tender text (Noordwijk 418890, Reimerswaal 417169, FUMO 415897, HLT Samen 383984): a supplier inside the GEMMA scope can make its product information transparent through the Softwarecatalogus. stackiq publishes the data; the page a buyer opens lives in the external frontend.", + "note": "Standard municipal tender text (Noordwijk 418890, Reimerswaal 417169, FUMO 415897, HLT Samen 383984): a supplier inside the GEMMA scope can make its product information transparent through the Softwarecatalogus. stackiq publishes the data; the page a buyer opens lives in the external frontend. Decided no (OpenSpec pass 2026-09-27): Recorded design: README.md:266 says stackiq runs \"with a separate React-based public frontend\", and README.md:273 names it (ConductionNL/tilburg-woo-ui) as the public search and detail pages. The missing half is the public page a buyer opens, which that frontend serves; stackiq publishes the data (module read rule for group public after publicationDate).", "vng-softwarecatalogus": "yes", "evidence": { "vng-softwarecatalogus": "https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier: \"De leveranciersinformatie in de Softwarecatalogus is openbaar\" (read 2026-09-26); https://www.softwarecatalogus.nl/inkoopondersteuning%20standaarden: \"De gegenereerde bestekstekst kunt u gebruiken in uw offerte-uitvraag ... Als informatiebron is de GEMMA softwarecatalogus gebruikt\" (read 2026-09-26). Reached on: Supplier login > Productportfolio; Inkoopondersteuning.", @@ -4765,12 +4765,12 @@ "originUrl": "https://github.com/VNG-Realisatie/Softwarecatalogus/issues/104", "stackiq": "no", "built": { - "state": "none", + "state": "decided-no", "evidence": "no impersonation in lib/ or src/ (grep impersonat); stackiq relies on Nextcloud users", - "owner": "ConductionNL/stackiq" + "owner": "nextcloud/server" }, "reachedOn": "nothing reaches it", - "provider": "stackiq", + "provider": "nextcloud", "providerHow": "read-from-code", "featureConfidence": "medium", "vng-softwarecatalogus": "unknown", @@ -4781,7 +4781,7 @@ "glpi": "source read at 11.0.9: src/Session.php:2054 startImpersonating and :2113 stopImpersonating, allowed by src/Session.php:1995 canImpersonate for users with fewer rights and the Impersonate right (src/User.php:6235); the button 'Impersonate' is on the user form (src/User.php:2974). CHANGELOG.md 11.0.0 adds the dedicated right. Reached on: Administration > Users > user form, Impersonate.", "topdesk": "unknown: acting as another user is not described; searched the full-text search index of docs.topdesk.com (https://docs.topdesk.com/en/js/fuzzydata.js, 987 pages) (read 2026-09-26)" }, - "note": "Mined from vng-softwarecatalogus (featureRequest) on 2026-09-26.", + "note": "Mined from vng-softwarecatalogus (featureRequest) on 2026-09-26. Decided no (OpenSpec pass 2026-09-27): Platform capability: the Nextcloud Impersonate app lets an administrator sign in as another account, and stackiq relies on Nextcloud users. built.owner corrected to nextcloud/server, provider nextcloud.", "sap-leanix": "unknown", "bluedolphin": "unknown", "glpi": "yes", @@ -4825,7 +4825,7 @@ "originUrl": "https://www.softwarecatalogus.nl/gebruikersonderzoek%202021", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "moduleVersion (lib/Settings/softwarecatalogus_register.json) is created on the Moduleversies index form; no copy of a version with its connections and no carry-over logic in lib/Service/ModuleVersionService.php", "owner": "ConductionNL/stackiq" }, @@ -4841,7 +4841,7 @@ "glpi": "source read at 11.0.9: installations can be moved to another version by the massive action src/Item_SoftwareVersion.php:179 move_version, but impact relations (install/mysql/glpi-empty.sql:1247 glpi_impactrelations) point at the item and are never copied to a replacing version or appliance; grep -n 'Impact' src/SoftwareVersion.php returns nothing.", "topdesk": "unknown: versions of applications are not modelled; searched the full-text search index of docs.topdesk.com (https://docs.topdesk.com/en/js/fuzzydata.js, 987 pages) (read 2026-09-26)" }, - "note": "Mined from vng-softwarecatalogus (featureRequest) on 2026-09-26.", + "note": "Mined from vng-softwarecatalogus (featureRequest) on 2026-09-26. Specified in openspec/changes/connections-derived-dependencies (OpenSpec pass 2026-09-27).", "sap-leanix": "partial", "bluedolphin": "unknown", "glpi": "no", @@ -4883,10 +4883,10 @@ "name": "Make the options of one field depend on another, such as model depending on brand.", "origin": "roadmap", "originUrl": "https://tip.topdesk.com/c/87-field-dependencies-brand-type-model-", - "stackiq": "no", + "stackiq": "partial", "built": { - "state": "none", - "evidence": "register properties are independent enums; no dependent option lists in lib/Settings/softwarecatalogus_register.json", + "state": "specified", + "evidence": "Relation pickers already follow another field on the form: module.contactPerson x-relation-filter {organization: @object.provider}, catalogService.modules and contactPerson on @object.provider, usage.moduleVersion on @object.module (lib/Settings/softwarecatalogus_register.json), honoured by the library form (@conduction/nextcloud-vue 2.57.1 src/components/CnFormDialog/CnFormDialog.vue:1183 relationFilterDecls). No enum property declares dependent values (x-openregister-dependent-values, openregister lib/Service/Rules/DependentValueTable.php), and the library form does not read that annotation.", "owner": "ConductionNL/stackiq" }, "reachedOn": "nothing reaches it", @@ -4901,7 +4901,7 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; questionnaire field options depending on another field are not documented (read 2026-09-26)", "glpi": "source read at 11.0.9: native forms show or hide questions on conditions (src/Glpi/Form/Condition/Engine.php:138, src/Glpi/Form/Condition/VisibilityStrategy.php:39), but options of one field do not filter by another on item forms; the open requests github.com/glpi-project/roadmap/discussions/414 (cascading filters) and /240 (custom field conditions) ask for it. Reached on: Administration > Forms, question conditions." }, - "note": "Mined from topdesk (roadmap) on 2026-09-26.", + "note": "Mined from topdesk (roadmap) on 2026-09-26. Corrected 2026-09-27 (OpenSpec pass): partial, not no. Relation pickers depend on another field (the version picker follows the application, the contact person follows the supplier); enum options do not. Missing half specified in landscape-dependent-field-options. Specified in openspec/changes/landscape-dependent-field-options (OpenSpec pass 2026-09-27).", "vng-softwarecatalogus": "unknown", "sap-leanix": "unknown", "bluedolphin": "unknown", @@ -4915,7 +4915,7 @@ "originUrl": "https://tip.topdesk.com/c/89-changing-the-type-of-an-asset", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "each entry lives in one schema (module, catalogService, suite) and no action moves an object to another schema", "owner": "ConductionNL/stackiq" }, @@ -4931,7 +4931,7 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; changing the type of a BPMN element is documented (https://help.bluedolphin.io/en/articles/11967561-add-an-object), changing the object definition of an existing repository object is not (read 2026-09-26)", "glpi": "source read at 11.0.9: the type of an appliance is an editable dropdown (install/mysql/glpi-empty.sql:8941 appliancetypes_id), changeable in place or by massive update (src/MassiveAction.php:666); changing the itemtype itself (for example a custom asset to another definition) is not possible, since each custom asset class is bound to one definition (src/Glpi/Asset/Asset.php:113), and the open request github.com/glpi-project/roadmap/discussions/232 asks for it. Reached on: Management > Appliances, Type field." }, - "note": "Mined from topdesk (roadmap) on 2026-09-26.", + "note": "Mined from topdesk (roadmap) on 2026-09-26. Specified in openspec/changes/landscape-change-entry-type (OpenSpec pass 2026-09-27).", "vng-softwarecatalogus": "unknown", "sap-leanix": "unknown", "bluedolphin": "unknown", @@ -4945,7 +4945,7 @@ "originUrl": "https://tip.topdesk.com/c/252-audit-logging-for-topdesk-mcp-server", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "stackiq has no AI or MCP integration of its own (see share-ai-assistant); the History tab (widget type audit, src/manifest.json:436) shows every change per object but does not single out actions an assistant took", "owner": "ConductionNL/stackiq" }, @@ -4961,7 +4961,7 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; the MCP server is read only (https://help.bluedolphin.io/en/articles/15602927-add-bluedolphin-mcp-server-to-an-ai-assistant) and AI credit usage is reported, but a log of AI actions on records is not documented (read 2026-09-26)", "glpi": "source read at 11.0.9: there is no AI assistant in core (grep -rliw 'llm\\|mcp' over src/ returns nothing); the history log (src/Log.php:48) records changes per user or API client, without any AI actor." }, - "note": "Mined from topdesk (roadmap) on 2026-09-26.", + "note": "Mined from topdesk (roadmap) on 2026-09-26. Specified in open change openspec/changes/mcp-full-action-surface (OpenSpec pass 2026-09-27).", "vng-softwarecatalogus": "unknown", "sap-leanix": "unknown", "bluedolphin": "unknown", @@ -4975,7 +4975,7 @@ "originUrl": "https://tip.topdesk.com/c/162-support-multi-factor-authentication-mfa-", "stackiq": "no", "built": { - "state": "none", + "state": "decided-no", "evidence": "no sign-in code in stackiq; stackiq creates local Nextcloud users (lib/Service/Stackiq/ContactPersonHandler.php:292) and second factors come from Nextcloud two-factor apps platform-wide, as with org-sso", "owner": "nextcloud/server" }, @@ -4991,7 +4991,7 @@ "sap-leanix": "unknown: docs searched at https://help.sap.com/docs/leanix/ea for multi factor and two factor, no hits; strong sign in is left to the identity provider through SSO (https://help.sap.com/docs/leanix/ea/managing-users) (read 2026-09-26)", "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/ for multi factor and two factor, no hits; strong sign in is left to the SSO identity provider (read 2026-09-26)" }, - "note": "Mined from topdesk (roadmap) on 2026-09-26. Also mined from glpi (changelog, https://github.com/glpi-project/glpi/releases/tag/11.0.0).", + "note": "Mined from topdesk (roadmap) on 2026-09-26. Also mined from glpi (changelog, https://github.com/glpi-project/glpi/releases/tag/11.0.0). Decided no (OpenSpec pass 2026-09-27): Owed to nextcloud/server: Nextcloud two-factor apps require a second factor at sign-in for every app, and stackiq users are Nextcloud users.", "glpi": "yes", "vng-softwarecatalogus": "unknown", "sap-leanix": "unknown", @@ -5005,7 +5005,7 @@ "originUrl": "https://tip.topdesk.com/c/163-enforce-strong-passwords", "stackiq": "no", "built": { - "state": "none", + "state": "decided-no", "evidence": "stackiq sets passwords through lib/Controller/ContactpersonenController.php:718 change-password and relies on Nextcloud for rules; a strength policy is the Nextcloud password_policy app, platform-wide", "owner": "nextcloud/server" }, @@ -5021,7 +5021,7 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; local passwords are managed in 'BlueDolphin's private Active Directory' (https://help.bluedolphin.io/en/articles/11967616-user-management) but no password policy setting is documented (read 2026-09-26)", "glpi": "source read at 11.0.9: Setup > General > Security enables a password policy (templates/pages/setup/general/security_setup.html.twig:46 use_password_security, :56 password_min_length, :63 password_need_number, :70 password_need_letter), enforced by src/User.php:7187 validatePassword with checks for length, digits, letters, capitals and symbols (src/User.php:7196 onwards), plus expiry settings (install/empty_data.php:380 password_expiration_delay). Reached on: Setup > General > Security." }, - "note": "Mined from topdesk (roadmap) on 2026-09-26.", + "note": "Mined from topdesk (roadmap) on 2026-09-26. Decided no (OpenSpec pass 2026-09-27): Owed to nextcloud/server: the Nextcloud password_policy app enforces password rules for every local account, including the ones stackiq creates.", "vng-softwarecatalogus": "unknown", "sap-leanix": "yes", "bluedolphin": "unknown", @@ -5035,7 +5035,7 @@ "originUrl": "https://tip.topdesk.com/c/241-ai-risk-prediction-", "stackiq": "no", "built": { - "state": "none", + "state": "decided-no", "evidence": "stackiq runs no changes and scores no risk (see ops-change)", "owner": "ConductionNL/stackiq" }, @@ -5051,7 +5051,7 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; change risk scoring is not documented (read 2026-09-26)", "glpi": "source read at 11.0.9: changes carry manual urgency, impact and priority (install/mysql/glpi-empty.sql:659 urgency, :660 impact, :661 priority) and a free text impact analysis (:663 impactcontent); no score is computed from past outcomes or dependencies, and grep -rli 'risk' over src/Change.php returns nothing." }, - "note": "Mined from topdesk (roadmap) on 2026-09-26.", + "note": "Mined from topdesk (roadmap) on 2026-09-26. Decided no (OpenSpec pass 2026-09-27): Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" A risk score for a planned change belongs to service desk change management, which stackiq does not run.", "vng-softwarecatalogus": "unknown", "sap-leanix": "unknown", "bluedolphin": "unknown", @@ -5065,7 +5065,7 @@ "originUrl": "https://github.com/glpi-project/roadmap/discussions/336", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "connections are registered one at a time or arrive through the ArchiMate import (lib/Service/ArchiMateImportService.php); nothing derives dependencies from known relations", "owner": "ConductionNL/stackiq" }, @@ -5081,7 +5081,7 @@ "bluedolphin": "https://help.bluedolphin.io/en/articles/11967645-use-datasource-to-create-relationships: 'how to automatically create relationships between objects based on a loaded datasource', for example application component to node (read 2026-09-26). Reached on: Admin > Sources > relationship creation.", "topdesk": "unknown: relations are created by hand in the Relationships widget; automatic filling is not described; searched the full-text search index of docs.topdesk.com (https://docs.topdesk.com/en/js/fuzzydata.js, 987 pages) (read 2026-09-26)" }, - "note": "Mined from glpi (featureRequest) on 2026-09-26.", + "note": "Mined from glpi (featureRequest) on 2026-09-26. Specified in openspec/changes/connections-derived-dependencies (OpenSpec pass 2026-09-27).", "vng-softwarecatalogus": "unknown", "sap-leanix": "yes", "bluedolphin": "yes", @@ -5275,7 +5275,7 @@ "originUrl": "https://updates.leanix.net/announcements/discover-verify-and-govern-ai-assets-with-sap-ai-agent-hub", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "no schema for AI agents or models in lib/Settings/softwarecatalogus_register.json (20 schemas, none for AI systems)", "owner": "ConductionNL/stackiq" }, @@ -5291,7 +5291,7 @@ "glpi": "source read at 11.0.9: no AI system itemtype (grep -rli 'artificial intelligence' over src/ locales/glpi.pot returns nothing); an admin can define an 'AI model' custom asset type (src/Html.php:1330 Setup > Asset definitions, src/Glpi/Asset/AssetDefinition.php) and link it to applications as an Appliance item (src/Appliance_Item.php:45) or impact relation (install/mysql/glpi-empty.sql:1247). There is no process model to link to. Reached on: Setup > Asset definitions, then Appliance > Items tab.", "topdesk": "unknown: AI agents and models as registered items are not described; searched the full-text search index of docs.topdesk.com (https://docs.topdesk.com/en/js/fuzzydata.js, 987 pages) (read 2026-09-26)" }, - "note": "Mined from sap-leanix (changelog) on 2026-09-26.", + "note": "Mined from sap-leanix (changelog) on 2026-09-26. Specified in openspec/changes/landscape-ai-system-inventory (OpenSpec pass 2026-09-27).", "vng-softwarecatalogus": "unknown", "bluedolphin": "unknown", "glpi": "partial", @@ -5335,7 +5335,7 @@ "originUrl": "https://updates.leanix.net/announcements/answer-your-questions-in-seconds-with-the-enterprise-architecture-assistant", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "no assistant in stackiq lib/ or src/ (see share-ai-assistant, where only OpenRegister's generic MCP endpoint exists outside the app)", "owner": "ConductionNL/stackiq" }, @@ -5351,7 +5351,7 @@ "glpi": "source read at 11.0.9: grep -rli 'openai\\|llm\\|artificial intelligence' over src/ returns nothing; search is criteria based only (src/Glpi/Search/Input/QueryBuilder.php:72).", "topdesk": "https://docs.topdesk.com/en/td-robin-for-operators.html: \"Chat: Ask TOPdesk Robin a question. TOPdesk Robin searches your organization's knowledge base for an answer\" (read 2026-09-26); https://docs.topdesk.com/en/ai-answer-assistant.html: \"The Knowledge Base is the only source for the AI\" (read 2026-09-26). Answers cite knowledge items, not asset or landscape entries. Reached on: Call card > TOPdesk Robin panel." }, - "note": "Mined from sap-leanix (changelog) on 2026-09-26. Also mined from bluedolphin (changelog, https://bluedolphin.io/product-news/).", + "note": "Mined from sap-leanix (changelog) on 2026-09-26. Also mined from bluedolphin (changelog, https://bluedolphin.io/product-news/). Specified in open change openspec/changes/stackiq-mcp-adoption (OpenSpec pass 2026-09-27).", "bluedolphin": "yes", "vng-softwarecatalogus": "unknown", "glpi": "no", @@ -5365,7 +5365,7 @@ "originUrl": "https://roadmap.leanix.net/c/812-meta-model-eu-ai-act-extension", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "no AI system or AI Act risk property on module or any other schema in lib/Settings/softwarecatalogus_register.json", "owner": "ConductionNL/stackiq" }, @@ -5381,7 +5381,7 @@ "glpi": "source read at 11.0.9: grep -rli 'ai act\\|artificial intelligence' over src/ locales/glpi.pot returns nothing; appliances have no risk category field (install/mysql/glpi-empty.sql:8935 glpi_appliances).", "topdesk": "unknown: the AI Act is mentioned only for TOPdesk's own AI features (\"post-market monitoring procedures ... in accordance with the AI Act\"), not for classifying the customer's AI systems; searched the full-text search index of docs.topdesk.com (https://docs.topdesk.com/en/js/fuzzydata.js, 987 pages) (read 2026-09-26)" }, - "note": "Mined from sap-leanix (roadmap) on 2026-09-26.", + "note": "Mined from sap-leanix (roadmap) on 2026-09-26. Specified in openspec/changes/landscape-ai-system-inventory (OpenSpec pass 2026-09-27).", "vng-softwarecatalogus": "unknown", "bluedolphin": "unknown", "glpi": "no", @@ -5453,11 +5453,11 @@ "name": "Send entries to connected outside systems only once they reach a chosen lifecycle state.", "origin": "changelog", "originUrl": "https://bluedolphin.io/blog/november-2025-product-updates-effortless-enterprise-architecture-management/", - "stackiq": "unknown", + "stackiq": "yes", "built": { - "state": "none", - "evidence": "stackiq has no sync code of its own; outgoing events go through OpenRegister flows authored on the Flows page (src/manifest.json:1057, see share-webhooks), and whether a flow can gate on a lifecycle status was not traced", - "owner": "ConductionNL/stackiq" + "state": "built", + "evidence": "src/manifest.json:1057 Flows page (entitySource flows, app stackiq) and FlowDetail canvas over OpenRegister's flow store; openregister lib/Service/Flow/Nodes/TriggerObjectNode.php:82 object.updated trigger per register and schema; lib/Service/Flow/Nodes/FilterNode.php:10 and :186 per-item condition (for example the status); integriq lib/Flow/SourceCallNode.php (development) calls the outside source.", + "owner": "ConductionNL/openregister" }, "reachedOn": "nothing reaches it", "provider": "openregister", @@ -5471,7 +5471,7 @@ "glpi": "source read at 11.0.9: webhooks carry search criteria filters (src/Webhook.php:64 implements FilterableInterface, src/Glpi/Search/FilterableTrait.php:45) and are only sent when the changed item matches them, src/Webhook.php:1228 itemMatchFilter; filtering on the Status field (src/Appliance.php:350) sends an appliance only once it reaches the chosen state. Reached on: Setup > Webhooks > Filter tab.", "topdesk": "https://docs.topdesk.com/en/creating-events.html: \"Edit card is triggered when a card is modified. Fill in the conditions ... Conditions check the current value of the card\" (read 2026-09-26); https://docs.topdesk.com/en/let-your-topdesk-talk-to-other-applications.html: \"Send a request from TOPdesk to another program\" (read 2026-09-26). The customer builds it as an automated action; no ready-made lifecycle sync. Reached on: Action Management > events and action sequences." }, - "note": "Mined from bluedolphin (changelog) on 2026-09-26.", + "note": "Mined from bluedolphin (changelog) on 2026-09-26. Matrix corrected (OpenSpec pass 2026-09-27): a flow on the Flows page gates on the lifecycle status before integriq sends the entry; rated yes.", "vng-softwarecatalogus": "unknown", "sap-leanix": "unknown", "glpi": "yes", @@ -5515,7 +5515,7 @@ "originUrl": "https://bluedolphin.io/blog/june-2026-bluedolphin-updates/", "stackiq": "no", "built": { - "state": "none", + "state": "specified", "evidence": "entries belong to an organisation through OpenRegister multitenancy (see org-data-segregation); no stackiq page or action moves an entry to another organisation or register", "owner": "ConductionNL/stackiq" }, @@ -5531,7 +5531,7 @@ "glpi": "source read at 11.0.9: selected records are moved to another entity with their links by src/Transfer.php:50 Transfer, queued through the massive action add_transfer_list (src/MassiveAction.php:598), for one or many entries at once. Reached on: any list, Actions > Add to transfer list; Administration > Entities > transfer.", "topdesk": "unknown: workspaces are not described; changing an asset's type is still an idea at https://tip.topdesk.com/c/89-changing-the-type-of-an-asset; searched the full-text search index of docs.topdesk.com (https://docs.topdesk.com/en/js/fuzzydata.js, 987 pages) (read 2026-09-26)" }, - "note": "Mined from bluedolphin (changelog) on 2026-09-26.", + "note": "Mined from bluedolphin (changelog) on 2026-09-26. Specified in openspec/changes/landscape-move-between-organisations (OpenSpec pass 2026-09-27).", "vng-softwarecatalogus": "unknown", "sap-leanix": "unknown", "glpi": "yes", diff --git a/openspec/parity/gap-decisions.json b/openspec/parity/gap-decisions.json new file mode 100644 index 000000000..e9e45b35d --- /dev/null +++ b/openspec/parity/gap-decisions.json @@ -0,0 +1,674 @@ +[ + { + "row": "arch-definitions", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (0 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "arch-export-amef", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (1 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "arch-export-org", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (1 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "arch-import-amef", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (1 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "comp-ai-act-classification", + "matrix": "stackiq", + "decision": "build", + "reason": "Below the bar on its own (0 competitor yes, no tender, feature request or roadmap demand), and it rides with land-ai-agent-inventory: its whole capability is an AI Act risk category on the AI system record that change adds.", + "change": "landscape-ai-system-inventory", + "decidedOn": "2026-09-27" + }, + { + "row": "comp-audit-questionnaire", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: single competitor, no demand, outside the core areas (compliance).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "comp-bio-assessment", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (0 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "comp-bio-measures", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (0 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "comp-bulk-sync-standards", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (0 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "comp-common-ground-fit", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: no competitor rates yes, featureRequest demand without a competitor yes, outside the core areas (compliance).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "comp-forum-standaardisatie", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: no competitor rates yes, no demand, outside the core areas (compliance).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "comp-health-scoring", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 2 competitors rate yes (SAP LeanIX, BlueDolphin).", + "change": "landscape-completeness-score", + "decidedOn": "2026-09-27" + }, + { + "row": "comp-processing-register-generate", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: no competitor rates yes, featureRequest demand without a competitor yes, outside the core areas (compliance).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "comp-retention-cleanup", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: single competitor, changelog only, which counts as the competitor it names, outside the core areas (compliance).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "comp-security-officer-signoff", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: no competitor rates yes, featureRequest demand without a competitor yes, outside the core areas (compliance).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "comp-verified-vs-claimed", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (1 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "conn-api-catalogue", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: core area (connections).", + "change": "connections-api-catalogue", + "decidedOn": "2026-09-27" + }, + { + "row": "conn-auto-populate-dependencies", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 2 competitors rate yes (SAP LeanIX, BlueDolphin); featureRequest demand; core area (connections).", + "change": "connections-derived-dependencies", + "decidedOn": "2026-09-27" + }, + { + "row": "conn-diagram", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 4 competitors rate yes (SAP LeanIX, BlueDolphin, GLPI, TOPdesk); core area (connections).", + "change": "connections-diagram-and-graph-export", + "decidedOn": "2026-09-27" + }, + { + "row": "conn-export-graph", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 2 competitors rate yes (GEMMA Softwarecatalogus, GLPI); core area (connections).", + "change": "connections-diagram-and-graph-export", + "decidedOn": "2026-09-27" + }, + { + "row": "conn-external-provision", + "matrix": "stackiq", + "decision": "build", + "reason": "Below the bar on its own (1 competitor yes, no tender, feature request or roadmap demand), and it rides with conn-list-page: its missing half is a page to fill the field, and the connection form on the new connections page shows it.", + "change": "connections-catalogue-pages", + "decidedOn": "2026-09-27" + }, + { + "row": "conn-list-page", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 2 competitors rate yes (GEMMA Softwarecatalogus, SAP LeanIX); core area (connections). Marked specified with no change directory; this change is the missing change.", + "change": "connections-catalogue-pages", + "decidedOn": "2026-09-27" + }, + { + "row": "conn-per-application", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 3 competitors rate yes (SAP LeanIX, BlueDolphin, GLPI); core area (connections). Partial and built: the change builds the missing half, a connections section on the application page that opens each connection.", + "change": "connections-catalogue-pages", + "decidedOn": "2026-09-27" + }, + { + "row": "conn-type-filter", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: core area (connections). Marked specified with no change directory; this change is the missing change.", + "change": "connections-catalogue-pages", + "decidedOn": "2026-09-27" + }, + { + "row": "ctr-budget", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: single competitor, no demand, outside the core areas (contracts).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ctr-collective-agreements", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: no competitor rates yes, featureRequest demand without a competitor yes, outside the core areas (contracts).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ctr-depreciation", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: single competitor, no demand, outside the core areas (contracts).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ctr-effective-licence-position", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: no competitor rates yes, no demand, outside the core areas (contracts).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ctr-linked-contracts", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: no competitor rates yes, featureRequest demand without a competitor yes, outside the core areas (contracts).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ctr-per-application", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 2 competitors rate yes (SAP LeanIX, GLPI).", + "change": "landscape-application-page", + "decidedOn": "2026-09-27" + }, + { + "row": "ctr-saas-spend", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (0 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ins-ai-action-audit", + "matrix": "stackiq", + "decision": "existing", + "reason": "Open change mcp-full-action-surface, spec mcp-tool-surface, requires every agent tool invocation on stackiq to land in Hermiq's audit trail, which lists what an assistant did, when and on which record.", + "change": "mcp-full-action-surface", + "decidedOn": "2026-09-27" + }, + { + "row": "ins-concept-orgs-widget", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: no competitor rates yes, no demand, outside the core areas (insight).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ins-cost-report", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (1 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ins-natural-language-query", + "matrix": "stackiq", + "decision": "existing", + "reason": "Open change stackiq-mcp-adoption declares read-only search and get MCP tools on nine catalogue schemas so Hermiq answers questions about the landscape from the entries it read; mcp-full-action-surface design section 6 grounds the chat scenario. The chat, the answer and its citations are Hermiq's (ADR-034).", + "change": "stackiq-mcp-adoption", + "decidedOn": "2026-09-27" + }, + { + "row": "ins-scheduled-report", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: single competitor, no demand, outside the core areas (insight).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ins-usage-analytics", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: single competitor, changelog only, which counts as the competitor it names, outside the core areas (insight).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "land-ai-agent-inventory", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: core area (landscape).", + "change": "landscape-ai-system-inventory", + "decidedOn": "2026-09-27" + }, + { + "row": "land-application-modules", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 2 competitors rate yes (SAP LeanIX, BlueDolphin); core area (landscape). Partial and built: the change builds the missing half, breaking one application into its components.", + "change": "landscape-application-components", + "decidedOn": "2026-09-27" + }, + { + "row": "land-application-owner", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 2 competitors rate yes (SAP LeanIX, GLPI); core area (landscape). Partial and built: the change builds the missing half, a separate business owner and technical owner for the organisation that uses the application, on its usage.", + "change": "landscape-usage-registration", + "decidedOn": "2026-09-27" + }, + { + "row": "land-bulk-edit", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (1 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "land-change-entry-type", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: roadmap demand; core area (landscape).", + "change": "landscape-change-entry-type", + "decidedOn": "2026-09-27" + }, + { + "row": "land-completeness-score", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 2 competitors rate yes (SAP LeanIX, BlueDolphin); core area (landscape).", + "change": "landscape-completeness-score", + "decidedOn": "2026-09-27" + }, + { + "row": "land-data-quality-survey", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 2 competitors rate yes (SAP LeanIX, BlueDolphin); core area (landscape).", + "change": "landscape-owner-attestation", + "decidedOn": "2026-09-27" + }, + { + "row": "land-dependent-fields", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: roadmap demand (https://tip.topdesk.com/c/87-field-dependencies-brand-type-model-); core area (landscape). Matrix corrected to partial while re-reading the code: relation pickers already follow another field (CnFormDialog relationFilterDecls, usage.moduleVersion on @object.module); the change builds the missing half, dependent enum options declared with OpenRegister x-openregister-dependent-values.", + "change": "landscape-dependent-field-options", + "decidedOn": "2026-09-27" + }, + { + "row": "land-detail-page", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 2 competitors rate yes (SAP LeanIX, BlueDolphin); core area (landscape). Partial and built: the change builds the missing half, contracts on the application page, opening it from the Applications list, and the stale field keys.", + "change": "landscape-application-page", + "decidedOn": "2026-09-27" + }, + { + "row": "land-guided-wizard", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (0 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "land-move-between-workspaces", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 2 competitors rate yes (BlueDolphin, GLPI); core area (landscape).", + "change": "landscape-move-between-organisations", + "decidedOn": "2026-09-27" + }, + { + "row": "land-register-application", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 4 competitors rate yes (GEMMA Softwarecatalogus, SAP LeanIX, BlueDolphin, GLPI); core area (landscape). Partial and built: the change builds the missing half, a lifecycle status for the application an organisation uses, set on a usage page.", + "change": "landscape-usage-registration", + "decidedOn": "2026-09-27" + }, + { + "row": "land-sectors", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Recorded non-goal: aanvullende-informatie.md:511 lists VNG issue #4 (classify packages on the reference architectures of the relevant sectors, so buyers from several sectors find them) with the analysis \"Classificeren op meerdere sectorale referentiearchitecturen, buiten scope\", and issues/4.md carries the label \"Buiten scope oplevering\" and is closed. Tagging applications with sectors is the entry to that out-of-scope classification. The unused sector schema (lib/Settings/softwarecatalogus_register.json:1036) is what the specified state pointed at; no change directory existed.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "land-version-carry-connections", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: featureRequest demand; core area (landscape).", + "change": "connections-derived-dependencies", + "decidedOn": "2026-09-27" + }, + { + "row": "life-dates-follow-status", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: no competitor rates yes, featureRequest demand without a competitor yes, outside the core areas (lifecycle).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "life-strategy-link", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: single competitor, no demand, outside the core areas (lifecycle).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "life-version-in-use", + "matrix": "stackiq", + "decision": "build", + "reason": "Build: 2 competitors rate yes (GEMMA Softwarecatalogus, GLPI). Partial and built: the change builds the missing half, a usage page where the organisation sets the version it runs.", + "change": "landscape-usage-registration", + "decidedOn": "2026-09-27" + }, + { + "row": "life-version-tolerance", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: no competitor rates yes, roadmap demand without a competitor yes, outside the core areas (lifecycle).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "mkt-contact-peers", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: single competitor, no demand, outside the core areas (market).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "mkt-contacts-per-product", + "matrix": "stackiq", + "decision": "build", + "reason": "Below the bar on its own (0 competitor yes, no tender, feature request or roadmap demand), and it rides with land-detail-page: its missing half is the stale contactpersoon key on the application page, which that change corrects.", + "change": "landscape-application-page", + "decidedOn": "2026-09-27" + }, + { + "row": "mkt-side-by-side-compare", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: no competitor rates yes, featureRequest demand without a competitor yes, outside the core areas (market).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "mkt-tender-product-info", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Recorded design: README.md:266 says stackiq runs \"with a separate React-based public frontend\", and README.md:273 names it (ConductionNL/tilburg-woo-ui) as the public search and detail pages. The missing half is the public page a buyer opens, which that frontend serves; stackiq publishes the data (module read rule for group public after publicationDate).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ops-agent-inventory", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" Agent-based discovery of installed software is a discovery agent.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ops-change", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" Change approval workflows are service desk change management.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ops-change-risk-score", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" A risk score for a planned change belongs to service desk change management, which stackiq does not run.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ops-mobile", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: single competitor, no demand, outside the core areas (operations).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ops-network-discovery", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" Network device discovery is a discovery agent.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ops-saas-discovery", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" Discovering unregistered SaaS use is a discovery agent.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ops-self-service", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" A self-service portal where end users request software is a service desk.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ops-sla", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" The competitor evidence tracks service desk response and resolution targets on calls and tickets.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "ops-tickets", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Recorded non-goal: openspec/parity/capabilities.json category: \"It is not a discovery agent, a service desk or a developer portal, and the operations area exists to record that on purpose.\" Logging incidents and requests is a service desk. Exchange with the organisation's service desk is specified under share-itsm-integration.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "org-act-as-user", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Platform capability: the Nextcloud Impersonate app lets an administrator sign in as another account, and stackiq relies on Nextcloud users. built.owner corrected to nextcloud/server, provider nextcloud.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "org-activation-mail", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (1 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "org-assigned-only-rights", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: single competitor, changelog only, which counts as the competitor it names, outside the core areas (organisations).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "org-directory-sync", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Platform capability: Nextcloud user_ldap keeps users and groups in step with a directory for every app, and stackiq users and groups are Nextcloud users and groups. built.owner corrected to nextcloud/server.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "org-merge", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (0 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "org-password-change", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Platform capability: Nextcloud personal settings let every user change their password and see their account details. The stackiq half (change password from your own contact row) stays as built evidence. built.owner corrected to nextcloud/server, provider nextcloud.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "org-sso", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Platform capability: Nextcloud signs users in through its identity provider apps (user_oidc, user_saml) for every app, and stackiq users are Nextcloud users. built.owner corrected to nextcloud/server.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "sec-affected-versions", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (0 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "sec-patch-status", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: no competitor rates yes, no demand, outside the core areas (security).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "sec-risk-score", + "matrix": "stackiq", + "decision": "defer", + "reason": "Defer: no competitor rates yes, no demand, outside the core areas (security).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "share-federation-peers", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (0 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "share-lifecycle-conditional-sync", + "matrix": "stackiq", + "decision": "existing", + "reason": "matrix corrected: stackiq's Flows page (src/manifest.json:1057, OpenRegister's flow store scoped to stackiq) composes an object trigger on object.updated (openregister lib/Service/Flow/Nodes/TriggerObjectNode.php:82), a per-item Filter on the lifecycle status (FilterNode.php:10, :186) and integriq's source call node (integriq lib/Flow/SourceCallNode.php on development). Rating set to yes, built.owner ConductionNL/openregister.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "share-publish", + "matrix": "stackiq", + "decision": "defer", + "reason": "partial, built, no demand (0 competitor yes, no tender, feature request or roadmap row).", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "sec-multi-factor-sign-in", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Owed to nextcloud/server: Nextcloud two-factor apps require a second factor at sign-in for every app, and stackiq users are Nextcloud users.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "sec-password-policy", + "matrix": "stackiq", + "decision": "decided-no", + "reason": "Owed to nextcloud/server: the Nextcloud password_policy app enforces password rules for every local account, including the ones stackiq creates.", + "change": null, + "decidedOn": "2026-09-27" + }, + { + "row": "conn-integration-registry", + "matrix": "stackiq", + "decision": "existing", + "reason": "Owned by ConductionNL/integriq. Its open change connection-registry covers the integrations overview; stackiq's half is the open change adopt-connection-registry (19 of 20 tasks). Recorded as instructed by the coordinator; the matrix row is the integriq lane's to set.", + "change": "connection-registry (ConductionNL/integriq)", + "decidedOn": "2026-09-27" + } +] From 988815a5424ce5bf7244f8eee3e40cac40d06cd0 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 27 Sep 2026 20:00:18 +0200 Subject: [PATCH 15/15] docs(openspec): landscape-ai-system-inventory, FRIA as a reference field like the DPIA --- .../changes/landscape-ai-system-inventory/design.md | 9 ++++++--- .../changes/landscape-ai-system-inventory/proposal.md | 2 +- .../specs/ai-system-inventory/spec.md | 10 +++++----- .../changes/landscape-ai-system-inventory/tasks.md | 8 ++++---- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/openspec/changes/landscape-ai-system-inventory/design.md b/openspec/changes/landscape-ai-system-inventory/design.md index 46e548dc1..3aff7e287 100644 --- a/openspec/changes/landscape-ai-system-inventory/design.md +++ b/openspec/changes/landscape-ai-system-inventory/design.md @@ -22,6 +22,7 @@ The catalogue holds applications (`module`, `lib/Settings/softwarecatalogus_regi | `aiActRole` | enum `provider`, `deployer` | | | `algorithmRegisterUrl` | string, format uri | the entry at algoritmes.overheid.nl | | `assessedOn` | date | | +| `friaDocumentRef` | string | reference to the fundamental rights impact assessment, the same pattern as `module.dpiaDocumentRef` | | `status` | enum `in development`, `in use`, `withdrawn`, with an `x-openregister-lifecycle` on those exact values | | Configuration: `allowFiles: true`, `allowedTags`: `FRIA`, `Technical documentation`, `Human oversight`, `Logging`. Authorization copied from `usage`: the organisation reads and edits its own AI systems (`_organisation` match); suppliers read those whose `provider` is their organisation. @@ -32,13 +33,15 @@ Rejected: a new value `AI system` in `module.type`. The act's fields (category, `src/manifest.d/ai-systems.json`: `AiSystems` (`/ai-systems`, index, columns name, kind, module, aiActRiskCategory, status, `filterMenu: true`, quick filters per risk category) and `AiSystemDetail` (`/ai-systems/:id`: data, files with the four tags, related, history). A menu child "AI systems" under Applications (ADR-097). On `ModuleDetail` (`src/manifest.json:491`) an `object-list` `md-ai-systems` with filter `{ "module": "@objectId" }`. -## D3. The missing assessment badge +## D3. The missing assessment warning -A computed flag through OpenRegister's calculation dialect if it can test for an attached file with a tag; otherwise the detail page shows a `CnStatusBadge` from a small check in a custom body widget `AiActChecklist` (`src/components/ai/AiActChecklist.vue`) that lists the four tags and marks which have a file. The index quick filter "High risk without FRIA" uses the same rule through the list's filter on a boolean `hasFria` that the widget cannot set; so the design writes `hasFria` from a listener on file add and remove (`lib/Listener/AiSystemFileListener.php`) only if the calculation dialect cannot express it. +The warning follows the pattern the module schema already uses for its DPIA (`module.dpiaDocumentRef`): the assessment is a reference field, `friaDocumentRef`, filled when the file is attached. The AI systems list gets a quick filter "High risk without FRIA" (`aiActRiskCategory` high risk and `friaDocumentRef` empty) and a warning badge column on the same rule. A body widget `AiActChecklist` (`src/components/ai/AiActChecklist.vue`) on the detail page lists the four evidence tags and marks which have a file, reading the entry's files through the library's files API. + +Rejected: a flag written by a file listener. OpenRegister raises no event when a file is added to an object (its `lib/Event` holds copy, lock, move, rename, unlock and version-restore events only), so a listener would miss the case that matters. ## Declarative versus imperative -The schema, lifecycle, tags and pages are declarative (ADR-031). The file-presence flag is the one piece that may need a listener, decided at build time by what OpenRegister's calculation dialect supports. +All declarative (ADR-031): the schema, lifecycle, tags, the quick filter and the pages. The checklist widget only reads. ## Seed data diff --git a/openspec/changes/landscape-ai-system-inventory/proposal.md b/openspec/changes/landscape-ai-system-inventory/proposal.md index 7f9132723..50f41d019 100644 --- a/openspec/changes/landscape-ai-system-inventory/proposal.md +++ b/openspec/changes/landscape-ai-system-inventory/proposal.md @@ -28,7 +28,7 @@ Rows from the stackiq matrix: 1. A schema `aiSystem`: name, description, kind (AI agent, AI model, AI feature), the application it runs in or supports, the supplier, the purpose, the EU AI Act risk category (prohibited, high risk, limited risk, minimal risk, not yet assessed), the organisation's role (provider or deployer), a link to the entry in the Dutch algorithm register, the date of the last assessment, and a status. 2. File tags on `aiSystem` for the documents the act asks of a deployer of a high-risk system: fundamental rights impact assessment, technical documentation from the provider, human oversight procedure, logging arrangement. 3. An AI systems list with filters on kind and risk category, and an AI systems section on the application page. -4. A warning badge on a high-risk AI system that has no fundamental rights impact assessment file. +4. A warning on a high-risk AI system that has no fundamental rights impact assessment, and a quick filter that lists them. ## Out of scope diff --git a/openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md b/openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md index a4d758997..197c6d641 100644 --- a/openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md +++ b/openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md @@ -35,12 +35,12 @@ An AI system SHALL record its EU AI Act risk category (prohibited, high risk, li ### Requirement: REQ-AIS-003 A high-risk AI system without a fundamental rights impact assessment is flagged -The detail page of an AI system SHALL show which of the four evidence tags have a file, and a high-risk AI system without a FRIA file SHALL show a warning on its page and in the list. +The detail page of an AI system SHALL show which of the four evidence tags have a file. A high-risk AI system whose FRIA reference is empty SHALL show a warning in the list, and the list SHALL offer a filter for exactly those systems. #### Scenario: The missing assessment shows @e2e tests/e2e/workflows/ai-systems.spec.ts -- **GIVEN** a high-risk AI system with technical documentation but no FRIA file -- **WHEN** the privacy officer opens its page -- **THEN** the evidence checklist marks FRIA as missing -- **AND** the AI systems list shows a warning on that row +- **GIVEN** a high-risk AI system with technical documentation but no FRIA +- **WHEN** the privacy officer filters the AI systems list on High risk without FRIA +- **THEN** that system is listed with a warning +- **AND** its page marks FRIA as missing in the evidence checklist diff --git a/openspec/changes/landscape-ai-system-inventory/tasks.md b/openspec/changes/landscape-ai-system-inventory/tasks.md index 83795d786..9f3be00de 100644 --- a/openspec/changes/landscape-ai-system-inventory/tasks.md +++ b/openspec/changes/landscape-ai-system-inventory/tasks.md @@ -21,12 +21,12 @@ ### Task 3: Evidence checklist and missing FRIA flag - **spec_ref**: openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md#requirement-req-ais-003-a-high-risk-ai-system-without-a-fundamental-rights-impact-assessment-is-flagged -- **files**: `src/components/ai/AiActChecklist.vue`, `src/customComponents.js`, `lib/Listener/AiSystemFileListener.php` (only if the calculation dialect cannot express it) +- **files**: `src/components/ai/AiActChecklist.vue`, `src/customComponents.js`, `src/manifest.d/ai-systems.json` (quick filter and badge column) - **acceptance_criteria**: - - GIVEN a high-risk AI system without a FRIA file WHEN its page opens THEN the checklist marks FRIA missing and the list shows the warning - - GIVEN a FRIA file is attached WHEN the page reloads THEN the warning is gone + - GIVEN a high-risk AI system without a FRIA reference WHEN the list is filtered on High risk without FRIA THEN it is listed with a warning + - GIVEN its FRIA reference is filled WHEN the list reloads THEN it is no longer listed - [ ] Implement -- [ ] Test (vitest `tests/vitest/aiActChecklist.spec.js`; PHPUnit for the listener if built) +- [ ] Test (vitest `tests/vitest/aiActChecklist.spec.js`; Playwright case in `tests/e2e/workflows/ai-systems.spec.ts`) ### Task 4: Documentation - **spec_ref**: openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md#requirement-req-ais-001-an-organisation-registers-the-ai-systems-it-uses-next-to-their-applications