From 6e6ab1f436dd2f05e0ae86e0f7290e58c4e1f55a Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 29 Sep 2026 20:03:05 +0200 Subject: [PATCH 1/5] test(ai-systems): the aiSystem schema, pages, checklist and seeds, red before the change --- tests/Unit/Settings/AiSystemFragmentTest.php | 163 +++++++++++++ tests/vitest/aiSystems.spec.js | 232 +++++++++++++++++++ 2 files changed, 395 insertions(+) create mode 100644 tests/Unit/Settings/AiSystemFragmentTest.php create mode 100644 tests/vitest/aiSystems.spec.js diff --git a/tests/Unit/Settings/AiSystemFragmentTest.php b/tests/Unit/Settings/AiSystemFragmentTest.php new file mode 100644 index 00000000..bbe29c65 --- /dev/null +++ b/tests/Unit/Settings/AiSystemFragmentTest.php @@ -0,0 +1,163 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://conduction.nl + * + * @spec openspec/specs/ai-system-inventory/spec.md#requirement-req-ais-001-an-organisation-registers-the-ai-systems-it-uses-next-to-their-applications + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + */ + +declare(strict_types=1); + +namespace OCA\Stackiq\Tests\Unit\Settings; + +use OCA\Stackiq\Service\SettingsService; +use PHPUnit\Framework\TestCase; +use ReflectionMethod; + +/** + * Merges every register.d fragment the way loadSettings() does and reads the + * aiSystem schema and the stackiq register from the result. + * + * @coversNothing + */ +class AiSystemFragmentTest extends TestCase { + + /** + * The register configuration after every fragment is merged in. + * + * @return array The merged configuration. + */ + private function merged(): array { + $dir = __DIR__ . '/../../../lib/Settings'; + $merged = json_decode((string) file_get_contents($dir . '/softwarecatalogus_register.json'), true); + $merge = new ReflectionMethod(SettingsService::class, 'deepMergeConfig'); + + $files = glob($dir . '/register.d/*.json'); + sort($files); + foreach ($files as $file) { + $fragment = json_decode((string) file_get_contents($file), true); + $merged = $merge->invoke(null, $merged, $fragment); + } + + return $merged; + }//end merged() + + /** + * The aiSystem schema. + * + * @return array The schema. + */ + private function schema(): array { + $schemas = $this->merged()['components']['schemas']; + $this->assertArrayHasKey('aiSystem', $schemas); + + return $schemas['aiSystem']; + }//end schema() + + /** + * The stackiq register lists the new schema, and keeps the ones it had. + * + * @return void + */ + public function testTheStackiqRegisterListsAiSystem(): void { + $list = $this->merged()['components']['registers']['stackiq']['schemas']; + + $this->assertContains('aiSystem', $list); + $this->assertContains('module', $list); + $this->assertContains('usage', $list); + $this->assertSame(count($list), count(array_unique($list))); + }//end testTheStackiqRegisterListsAiSystem() + + /** + * The record holds the fields of REQ-AIS-001 and REQ-AIS-002. + * + * @return void + */ + public function testTheRecordHoldsTheInventoryAndActFields(): void { + $props = $this->schema()['properties']; + + $this->assertSame(['AI agent', 'AI model', 'AI feature'], $props['kind']['enum']); + $this->assertSame( + ['prohibited', 'high risk', 'limited risk', 'minimal risk', 'not yet assessed'], + $props['aiActRiskCategory']['enum'] + ); + $this->assertSame('not yet assessed', $props['aiActRiskCategory']['default']); + $this->assertSame(['provider', 'deployer'], $props['aiActRole']['enum']); + $this->assertSame('#/components/schemas/module', $props['module']['$ref']); + $this->assertSame('#/components/schemas/organization', $props['provider']['$ref']); + $this->assertSame('uri', $props['algorithmRegisterUrl']['format']); + $this->assertSame('date', $props['assessedOn']['format']); + $this->assertSame('string', $props['friaDocumentRef']['type']); + $this->assertTrue($props['kind']['facetable']); + $this->assertTrue($props['aiActRiskCategory']['facetable']); + $this->assertSame(['name'], $this->schema()['required']); + + foreach (['kind', 'aiActRiskCategory', 'aiActRole', 'status'] as $field) { + $this->assertSame($props[$field]['enum'], array_keys($props[$field]['x-enum-labels']), $field); + } + }//end testTheRecordHoldsTheInventoryAndActFields() + + /** + * Evidence files carry the four tags the act asks of a deployer. + * + * @return void + */ + public function testEvidenceFilesCarryTheFourTags(): void { + $config = $this->schema()['configuration']; + + $this->assertTrue($config['allowFiles']); + $this->assertSame(['FRIA', 'Technical documentation', 'Human oversight', 'Logging'], $config['allowedTags']); + }//end testEvidenceFilesCarryTheFourTags() + + /** + * The lifecycle names exactly the status values, so every transition can match a row. + * + * @return void + */ + public function testTheLifecycleMatchesTheStatusValues(): void { + $schema = $this->schema(); + $lifecycle = $schema['configuration']['x-openregister-lifecycle']; + $values = $schema['properties']['status']['enum']; + + $this->assertSame('status', $lifecycle['field']); + $this->assertContains($lifecycle['initial'], $values); + foreach ($lifecycle['final'] as $final) { + $this->assertContains($final, $values); + } + + foreach ($lifecycle['transitions'] as $name => $transition) { + $this->assertContains($transition['to'], $values, $name); + foreach ($transition['from'] as $from) { + $this->assertContains($from, $values, $name); + } + } + }//end testTheLifecycleMatchesTheStatusValues() + + /** + * An organisation reads and edits only its own AI systems; a supplier reads those it provides. + * + * @return void + */ + public function testReadsAreScopedToTheOrganisation(): void { + $read = $this->schema()['authorization']['read']; + + $this->assertContains(['group' => 'gebruik-beheerder', 'match' => ['_organisation' => '$organisation']], $read); + $this->assertContains(['group' => 'aanbod-beheerder', 'match' => ['provider' => '$organisation']], $read); + foreach ($read as $rule) { + $this->assertIsArray($rule, 'no read rule may grant a whole group every AI system'); + } + }//end testReadsAreScopedToTheOrganisation() +}//end class diff --git a/tests/vitest/aiSystems.spec.js b/tests/vitest/aiSystems.spec.js new file mode 100644 index 00000000..19d4d7b9 --- /dev/null +++ b/tests/vitest/aiSystems.spec.js @@ -0,0 +1,232 @@ +/** + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * AI systems: the evidence checklist and the missing-FRIA rule, the pages as + * the app builds them (manifest.d merged the way src/main.js merges it), and + * the seeded AI systems validated against the real aiSystem schema from the + * register.d fragment. + * + * @spec openspec/specs/ai-system-inventory/spec.md + */ + +import addFormats from 'ajv-formats' +import Ajv2020 from 'ajv/dist/2020.js' +import * as fs from 'fs' +import * as path from 'path' +import { describe, expect, it } from 'vitest' +import fragment from '../../lib/Settings/register.d/ai-system-inventory.json' +import mock from '../../lib/Settings/stackiq_mock_register.json' +import manifestSchema from '../../node_modules/@conduction/nextcloud-vue/src/schemas/app-manifest-v2.schema.json' +import { buildManifest } from '../../node_modules/@conduction/nextcloud-vue/src/utils/buildManifest.js' +import base from '../../src/manifest.json' +import menuLayout from '../../src/menu-layout.json' +import { + EVIDENCE_TAGS, + evidenceChecklist, + friaMissing, + friaStatus, +} from '../../src/utils/aiAct.js' + +const dir = path.resolve(__dirname, '../../src/manifest.d') +const merged = buildManifest( + base, + fs + .readdirSync(dir) + .filter((f) => f.endsWith('.json')) + .sort() + .map((f) => JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'))), + menuLayout, +) +const page = (id) => merged.pages.find((p) => p.id === id) +const schema = fragment.components.schemas.aiSystem + +/** + * A validator built from the real aiSystem properties. A relation is checked + * as an object or an id string, the way OpenRegister accepts it. + * + * @return {Function} The compiled Ajv validator. + */ +function compileAiSystem() { + const ajv = new Ajv2020({ allErrors: true, strict: false }) + addFormats(ajv) + const properties = Object.fromEntries( + Object.entries(schema.properties).map(([key, prop]) => [ + key, + prop.$ref + ? { type: ['object', 'string'] } + : { type: prop.type, enum: prop.enum, format: prop.format }, + ]), + ) + return ajv.compile({ + type: 'object', + required: schema.required, + properties, + }) +} + +describe('the missing FRIA rule', () => { + it('flags a high-risk system without a FRIA reference', () => { + expect(friaMissing({ aiActRiskCategory: 'high risk' })).toBe(true) + expect( + friaMissing({ aiActRiskCategory: 'high risk', friaDocumentRef: ' ' }), + ).toBe(true) + }) + + it('does not flag a high-risk system with a FRIA, or a system of another category', () => { + expect( + friaMissing({ + aiActRiskCategory: 'high risk', + friaDocumentRef: '/AI/fria.pdf', + }), + ).toBe(false) + expect(friaMissing({ aiActRiskCategory: 'minimal risk' })).toBe(false) + expect(friaMissing({})).toBe(false) + }) + + it('reads FRIA missing in the list only for a flagged system', () => { + expect(friaStatus('', { aiActRiskCategory: 'high risk' })).toBe( + 'FRIA missing', + ) + expect( + friaStatus('/AI/fria.pdf', { + aiActRiskCategory: 'high risk', + friaDocumentRef: '/AI/fria.pdf', + }), + ).toBe('') + expect(friaStatus('', { aiActRiskCategory: 'limited risk' })).toBe('') + }) +}) + +describe('the evidence checklist', () => { + it('marks each of the four tags present or missing from the files', () => { + const list = evidenceChecklist([ + { name: 'tech.pdf', labels: ['Technical documentation'] }, + { name: 'untagged.pdf', labels: [] }, + { name: 'x.pdf' }, + ]) + expect(list.map((r) => r.tag)).toEqual(EVIDENCE_TAGS) + expect(list.find((r) => r.tag === 'FRIA').present).toBe(false) + expect( + list.find((r) => r.tag === 'Technical documentation').present, + ).toBe(true) + expect(list.find((r) => r.tag === 'Technical documentation').files).toEqual( + ['tech.pdf'], + ) + }) + + it('uses the same four tags the schema allows on files', () => { + expect(schema.configuration.allowedTags).toEqual(EVIDENCE_TAGS) + }) + + it('reads an empty or broken response as nothing present', () => { + expect(evidenceChecklist(undefined).every((r) => !r.present)).toBe(true) + }) +}) + +describe('the AI systems pages', () => { + it('the merged manifest is valid against the v2 schema', () => { + const ajv = new Ajv2020({ allErrors: true, strict: false }) + addFormats(ajv) + const validate = ajv.compile(manifestSchema) + validate(merged) + expect(validate.errors ?? []).toEqual([]) + }) + + it('lists AI systems with kind and risk category, filterable, under Applications', () => { + const index = page('AiSystems') + expect(index.route).toBe('/ai-systems') + expect(index.config.schema).toBe('aiSystem') + expect(index.config.filterMenu).toBe(true) + const keys = index.config.columns.map((c) => + typeof c === 'string' ? c : c.key, + ) + expect(keys).toEqual( + expect.arrayContaining(['name', 'kind', 'module', 'aiActRiskCategory']), + ) + const modules = merged.menu.find((m) => m.id === 'Modules') + expect(modules.children.map((c) => c.route)).toContain('AiSystems') + }) + + it('filters on high risk, and on high risk without a FRIA', () => { + const quick = page('AiSystems').config.quickFilters + const high = quick.find((q) => q.label === 'High risk') + expect(high.filter).toEqual({ aiActRiskCategory: 'high risk' }) + const noFria = quick.find((q) => q.label === 'High risk without FRIA') + expect(noFria.filter).toEqual({ + aiActRiskCategory: 'high risk', + friaDocumentRef: 'IS NULL', + }) + for (const q of quick) { + for (const [key, value] of Object.entries(q.filter)) { + expect(schema.properties, key).toHaveProperty(key) + if (schema.properties[key].enum && value !== 'IS NULL') { + expect(schema.properties[key].enum, key).toContain(value) + } + } + } + }) + + it('shows the FRIA warning column through the app formatter', () => { + const column = page('AiSystems').config.columns.find( + (c) => typeof c === 'object' && c.key === 'friaDocumentRef', + ) + expect(column.formatter).toBe('friaStatus') + }) + + it('opens an AI system with its lifecycle, evidence files and checklist', () => { + const detail = page('AiSystemDetail') + expect(detail.route).toBe('/ai-systems/:id') + expect(detail.config.lifecycleActions).toEqual({ field: 'status' }) + expect( + detail.config.widgets.some( + (w) => w.type === 'integration' && w.integrationId === 'files', + ), + ).toBe(true) + expect( + detail.config.bodyWidgets.map((w) => w.component), + ).toContain('AiActChecklist') + }) + + it('lists the AI systems on the application page', () => { + const widget = page('ModuleDetail').config.widgets.find( + (w) => w.id === 'md-ai-systems', + ) + expect(widget.content.schema).toBe('aiSystem') + expect(widget.content.filter).toEqual({ module: '@objectId' }) + expect(widget.content.rowRoute).toBe('AiSystemDetail') + expect( + page('ModuleDetail').config.layout.some( + (l) => l.widgetId === 'md-ai-systems', + ), + ).toBe(true) + }) +}) + +describe('the seeded AI systems', () => { + const seeded = mock.components.objects.filter( + (o) => o['@self'].register === 'stackiq' && o['@self'].schema === 'aiSystem', + ) + + it('are accepted by the real aiSystem schema', () => { + const validate = compileAiSystem() + expect(seeded.length).toBeGreaterThanOrEqual(2) + for (const o of seeded) { + const { '@self': self, ...fields } = o + expect(validate(fields), JSON.stringify(validate.errors)).toBe(true) + } + }) + + it('include one high-risk system without a FRIA, so the warning shows', () => { + expect(seeded.filter((o) => friaMissing(o))).toHaveLength(1) + expect(seeded.some((o) => o.aiActRiskCategory === 'limited risk')).toBe( + true, + ) + }) + + it('carry the schema copy the demo import validates against', () => { + expect(mock.components.schemas.aiSystem.properties).toEqual( + schema.properties, + ) + }) +}) From 62f49184494646f9e3ff7821254fa541ca7da468 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 29 Sep 2026 20:06:59 +0200 Subject: [PATCH 2/5] feat(ai-systems): register the AI systems you use and see the high-risk ones without a FRIA --- docs/features/ai-systems.md | 45 +++ l10n/en.js | 50 ++- l10n/en.json | 50 ++- l10n/nl.js | 49 ++- l10n/nl.json | 49 ++- .../register.d/ai-system-inventory.json | 325 +++++++++++++++++ lib/Settings/stackiq_mock_register.json | 345 ++++++++++++++++++ src/App.vue | 4 + src/components/ai/AiActChecklist.vue | 211 +++++++++++ src/customComponents.js | 5 + src/icons.js | 2 + src/manifest.d/ai-systems.json | 78 ++++ src/manifest.json | 6 +- src/utils/aiAct.js | 67 ++++ tests/Unit/Settings/AiSystemFragmentTest.php | 10 +- tests/e2e/workflows/ai-systems.spec.ts | 129 +++++++ 16 files changed, 1418 insertions(+), 7 deletions(-) create mode 100644 docs/features/ai-systems.md create mode 100644 lib/Settings/register.d/ai-system-inventory.json create mode 100644 src/components/ai/AiActChecklist.vue create mode 100644 src/manifest.d/ai-systems.json create mode 100644 src/utils/aiAct.js create mode 100644 tests/e2e/workflows/ai-systems.spec.ts diff --git a/docs/features/ai-systems.md b/docs/features/ai-systems.md new file mode 100644 index 00000000..55c94874 --- /dev/null +++ b/docs/features/ai-systems.md @@ -0,0 +1,45 @@ + + +# AI systems + +An AI system is an AI agent, an AI model or an AI feature that your organisation uses. You register it next to the application it runs in, classify it under the EU AI Act, and keep the documents the act asks for. + +Specification: [`openspec/specs/ai-system-inventory/spec.md`](https://github.com/ConductionNL/stackiq/blob/development/openspec/specs/ai-system-inventory/spec.md). + +## Registering an AI system + +Open **Applications** in the navigation menu, then **AI systems**, and click **Add**. Fill in: + +- **Name** and **Description**. +- **Kind**: an AI agent acts on its own, an AI model is a trained model, an AI feature is part of an application. +- **Application**: the application it runs in or supports. +- **Supplier** and **Purpose**: who supplies it and what it decides, recommends or produces. + +The page of the application shows its AI systems in the **AI systems** section. + +## Classifying it under the AI Act + +Each AI system records: + +- **AI Act risk category**: prohibited, high risk, limited risk, minimal risk, or not yet assessed. A new system starts as not yet assessed. +- **Role under the AI Act**: provider or deployer. +- **Last assessed on** and the **Algorithm register entry**, the link to the system in the Dutch algorithm register. + +The category is your organisation's own classification. Stackiq records it; it does not decide it. + +## Evidence + +Attach documents to the AI system under **Documents** and tag each one: FRIA (fundamental rights impact assessment), Technical documentation, Human oversight or Logging. The **AI Act evidence** panel on the page lists the four tags and shows which have a document. + +Fill in **Fundamental rights impact assessment** with a reference to the FRIA. A high-risk AI system without one: + +- reads **FRIA missing** in the list, +- shows a warning on its page, +- and appears under the **High risk without FRIA** filter above the list. + +The other filters above the list select one risk category each. + +Screenshots follow once the feature runs on the demo instance. diff --git a/l10n/en.js b/l10n/en.js index 9256fd62..ef51efbd 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -770,7 +770,55 @@ OC.L10N.register( "To national provision": "To national provision", "From application": "From application", "No connections start at this application": "No connections start at this application", - "No connections end at this application": "No connections end at this application" + "No connections end at this application": "No connections end at this application", + "AI system": "AI system", + "An AI agent, AI model or AI feature the organisation uses, with its EU AI Act classification.": "An AI agent, AI model or AI feature the organisation uses, with its EU AI Act classification.", + "The name the organisation uses for this AI system.": "The name the organisation uses for this AI system.", + "What the AI system is and how it is used.": "What the AI system is and how it is used.", + "Kind": "Kind", + "An AI agent acts on its own, an AI model is a trained model, an AI feature is part of an application.": "An AI agent acts on its own, an AI model is a trained model, an AI feature is part of an application.", + "AI agent": "AI agent", + "AI model": "AI model", + "AI feature": "AI feature", + "The application this AI system runs in or supports.": "The application this AI system runs in or supports.", + "The organisation that supplies the AI system.": "The organisation that supplies the AI system.", + "Purpose": "Purpose", + "What the AI system decides, recommends or produces.": "What the AI system decides, recommends or produces.", + "AI Act risk category": "AI Act risk category", + "The risk category under the EU AI Act, as the organisation classified it.": "The risk category under the EU AI Act, as the organisation classified it.", + "Prohibited": "Prohibited", + "High risk": "High risk", + "Limited risk": "Limited risk", + "Minimal risk": "Minimal risk", + "Not yet assessed": "Not yet assessed", + "Role under the AI Act": "Role under the AI Act", + "Whether the organisation provides the AI system or deploys it.": "Whether the organisation provides the AI system or deploys it.", + "Deployer": "Deployer", + "Algorithm register entry": "Algorithm register entry", + "The link to this system in the Dutch algorithm register.": "The link to this system in the Dutch algorithm register.", + "Last assessed on": "Last assessed on", + "The date the classification was last assessed.": "The date the classification was last assessed.", + "Fundamental rights impact assessment": "Fundamental rights impact assessment", + "A reference to the fundamental rights impact assessment (FRIA). A high-risk system without one is flagged.": "A reference to the fundamental rights impact assessment (FRIA). A high-risk system without one is flagged.", + "Where the AI system stands in its lifecycle.": "Where the AI system stands in its lifecycle.", + "AI Act evidence": "AI Act evidence", + "Attach a document under Documents and give it the matching tag.": "Attach a document under Documents and give it the matching tag.", + "FRIA missing": "FRIA missing", + "Fundamental rights impact assessment (FRIA)": "Fundamental rights impact assessment (FRIA)", + "Human oversight": "Human oversight", + "Loading the evidence": "Loading the evidence", + "Logging": "Logging", + "Missing": "Missing", + "Technical documentation": "Technical documentation", + "The evidence could not be loaded.": "The evidence could not be loaded.", + "This is a high-risk AI system without a fundamental rights impact assessment.": "This is a high-risk AI system without a fundamental rights impact assessment.", + "AI systems": "AI systems", + "The AI agents, AI models and AI features your organisation uses, with their EU AI Act risk category.": "The AI agents, AI models and AI features your organisation uses, with their EU AI Act risk category.", + "FRIA": "FRIA", + "High risk without FRIA": "High risk without FRIA", + "Application and supplier": "Application and supplier", + "History": "History", + "No AI systems registered for this application": "No AI systems registered for this application" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/en.json b/l10n/en.json index bc303814..fee62e12 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -769,6 +769,54 @@ "To national provision": "To national provision", "From application": "From application", "No connections start at this application": "No connections start at this application", - "No connections end at this application": "No connections end at this application" + "No connections end at this application": "No connections end at this application", + "AI system": "AI system", + "An AI agent, AI model or AI feature the organisation uses, with its EU AI Act classification.": "An AI agent, AI model or AI feature the organisation uses, with its EU AI Act classification.", + "The name the organisation uses for this AI system.": "The name the organisation uses for this AI system.", + "What the AI system is and how it is used.": "What the AI system is and how it is used.", + "Kind": "Kind", + "An AI agent acts on its own, an AI model is a trained model, an AI feature is part of an application.": "An AI agent acts on its own, an AI model is a trained model, an AI feature is part of an application.", + "AI agent": "AI agent", + "AI model": "AI model", + "AI feature": "AI feature", + "The application this AI system runs in or supports.": "The application this AI system runs in or supports.", + "The organisation that supplies the AI system.": "The organisation that supplies the AI system.", + "Purpose": "Purpose", + "What the AI system decides, recommends or produces.": "What the AI system decides, recommends or produces.", + "AI Act risk category": "AI Act risk category", + "The risk category under the EU AI Act, as the organisation classified it.": "The risk category under the EU AI Act, as the organisation classified it.", + "Prohibited": "Prohibited", + "High risk": "High risk", + "Limited risk": "Limited risk", + "Minimal risk": "Minimal risk", + "Not yet assessed": "Not yet assessed", + "Role under the AI Act": "Role under the AI Act", + "Whether the organisation provides the AI system or deploys it.": "Whether the organisation provides the AI system or deploys it.", + "Deployer": "Deployer", + "Algorithm register entry": "Algorithm register entry", + "The link to this system in the Dutch algorithm register.": "The link to this system in the Dutch algorithm register.", + "Last assessed on": "Last assessed on", + "The date the classification was last assessed.": "The date the classification was last assessed.", + "Fundamental rights impact assessment": "Fundamental rights impact assessment", + "A reference to the fundamental rights impact assessment (FRIA). A high-risk system without one is flagged.": "A reference to the fundamental rights impact assessment (FRIA). A high-risk system without one is flagged.", + "Where the AI system stands in its lifecycle.": "Where the AI system stands in its lifecycle.", + "AI Act evidence": "AI Act evidence", + "Attach a document under Documents and give it the matching tag.": "Attach a document under Documents and give it the matching tag.", + "FRIA missing": "FRIA missing", + "Fundamental rights impact assessment (FRIA)": "Fundamental rights impact assessment (FRIA)", + "Human oversight": "Human oversight", + "Loading the evidence": "Loading the evidence", + "Logging": "Logging", + "Missing": "Missing", + "Technical documentation": "Technical documentation", + "The evidence could not be loaded.": "The evidence could not be loaded.", + "This is a high-risk AI system without a fundamental rights impact assessment.": "This is a high-risk AI system without a fundamental rights impact assessment.", + "AI systems": "AI systems", + "The AI agents, AI models and AI features your organisation uses, with their EU AI Act risk category.": "The AI agents, AI models and AI features your organisation uses, with their EU AI Act risk category.", + "FRIA": "FRIA", + "High risk without FRIA": "High risk without FRIA", + "Application and supplier": "Application and supplier", + "History": "History", + "No AI systems registered for this application": "No AI systems registered for this application" } } diff --git a/l10n/nl.js b/l10n/nl.js index e5e393e6..69d5e47a 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -841,7 +841,54 @@ OC.L10N.register( "To national provision": "Naar landelijke voorziening", "From application": "Van applicatie", "No connections start at this application": "Er beginnen geen koppelingen bij deze applicatie", - "No connections end at this application": "Er eindigen geen koppelingen bij deze applicatie" + "No connections end at this application": "Er eindigen geen koppelingen bij deze applicatie", + "AI system": "AI-systeem", + "An AI agent, AI model or AI feature the organisation uses, with its EU AI Act classification.": "Een AI-agent, AI-model of AI-functie die de organisatie gebruikt, met de indeling onder de Europese AI-verordening.", + "The name the organisation uses for this AI system.": "De naam die de organisatie voor dit AI-systeem gebruikt.", + "What the AI system is and how it is used.": "Wat het AI-systeem is en hoe het wordt gebruikt.", + "Kind": "Soort", + "An AI agent acts on its own, an AI model is a trained model, an AI feature is part of an application.": "Een AI-agent handelt zelfstandig, een AI-model is een getraind model, een AI-functie is onderdeel van een applicatie.", + "AI agent": "AI-agent", + "AI model": "AI-model", + "AI feature": "AI-functie", + "The application this AI system runs in or supports.": "De applicatie waarin dit AI-systeem draait of die het ondersteunt.", + "The organisation that supplies the AI system.": "De organisatie die het AI-systeem levert.", + "Purpose": "Doel", + "What the AI system decides, recommends or produces.": "Wat het AI-systeem beslist, adviseert of maakt.", + "AI Act risk category": "Risicocategorie AI-verordening", + "The risk category under the EU AI Act, as the organisation classified it.": "De risicocategorie onder de Europese AI-verordening, zoals de organisatie die heeft vastgesteld.", + "Prohibited": "Verboden", + "High risk": "Hoog risico", + "Limited risk": "Beperkt risico", + "Minimal risk": "Minimaal risico", + "Not yet assessed": "Nog niet beoordeeld", + "Role under the AI Act": "Rol onder de AI-verordening", + "Whether the organisation provides the AI system or deploys it.": "Of de organisatie het AI-systeem aanbiedt of gebruikt.", + "Deployer": "Gebruiksverantwoordelijke", + "Algorithm register entry": "Vermelding in het algoritmeregister", + "The link to this system in the Dutch algorithm register.": "De link naar dit systeem in het Algoritmeregister van de Nederlandse overheid.", + "Last assessed on": "Laatst beoordeeld op", + "The date the classification was last assessed.": "De datum waarop de indeling voor het laatst is beoordeeld.", + "Fundamental rights impact assessment": "Grondrechteneffectbeoordeling", + "A reference to the fundamental rights impact assessment (FRIA). A high-risk system without one is flagged.": "Een verwijzing naar de grondrechteneffectbeoordeling (FRIA). Een systeem met hoog risico zonder beoordeling krijgt een waarschuwing.", + "Where the AI system stands in its lifecycle.": "Waar het AI-systeem staat in zijn levenscyclus.", + "AI Act evidence": "Bewijs voor de AI-verordening", + "Attach a document under Documents and give it the matching tag.": "Voeg een document toe onder Documenten en geef het de passende tag.", + "FRIA missing": "FRIA ontbreekt", + "Fundamental rights impact assessment (FRIA)": "Grondrechteneffectbeoordeling (FRIA)", + "Human oversight": "Menselijk toezicht", + "Loading the evidence": "Het bewijs wordt geladen", + "Logging": "Logging", + "Missing": "Ontbreekt", + "Technical documentation": "Technische documentatie", + "The evidence could not be loaded.": "Het bewijs kon niet worden geladen.", + "This is a high-risk AI system without a fundamental rights impact assessment.": "Dit is een AI-systeem met hoog risico zonder grondrechteneffectbeoordeling.", + "AI systems": "AI-systemen", + "The AI agents, AI models and AI features your organisation uses, with their EU AI Act risk category.": "De AI-agents, AI-modellen en AI-functies die je organisatie gebruikt, met hun risicocategorie onder de Europese AI-verordening.", + "FRIA": "FRIA", + "High risk without FRIA": "Hoog risico zonder FRIA", + "Application and supplier": "Applicatie en leverancier", + "No AI systems registered for this application": "Nog geen AI-systemen geregistreerd voor deze applicatie" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/nl.json b/l10n/nl.json index 5bddd25d..2342bd68 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -840,6 +840,53 @@ "To national provision": "Naar landelijke voorziening", "From application": "Van applicatie", "No connections start at this application": "Er beginnen geen koppelingen bij deze applicatie", - "No connections end at this application": "Er eindigen geen koppelingen bij deze applicatie" + "No connections end at this application": "Er eindigen geen koppelingen bij deze applicatie", + "AI system": "AI-systeem", + "An AI agent, AI model or AI feature the organisation uses, with its EU AI Act classification.": "Een AI-agent, AI-model of AI-functie die de organisatie gebruikt, met de indeling onder de Europese AI-verordening.", + "The name the organisation uses for this AI system.": "De naam die de organisatie voor dit AI-systeem gebruikt.", + "What the AI system is and how it is used.": "Wat het AI-systeem is en hoe het wordt gebruikt.", + "Kind": "Soort", + "An AI agent acts on its own, an AI model is a trained model, an AI feature is part of an application.": "Een AI-agent handelt zelfstandig, een AI-model is een getraind model, een AI-functie is onderdeel van een applicatie.", + "AI agent": "AI-agent", + "AI model": "AI-model", + "AI feature": "AI-functie", + "The application this AI system runs in or supports.": "De applicatie waarin dit AI-systeem draait of die het ondersteunt.", + "The organisation that supplies the AI system.": "De organisatie die het AI-systeem levert.", + "Purpose": "Doel", + "What the AI system decides, recommends or produces.": "Wat het AI-systeem beslist, adviseert of maakt.", + "AI Act risk category": "Risicocategorie AI-verordening", + "The risk category under the EU AI Act, as the organisation classified it.": "De risicocategorie onder de Europese AI-verordening, zoals de organisatie die heeft vastgesteld.", + "Prohibited": "Verboden", + "High risk": "Hoog risico", + "Limited risk": "Beperkt risico", + "Minimal risk": "Minimaal risico", + "Not yet assessed": "Nog niet beoordeeld", + "Role under the AI Act": "Rol onder de AI-verordening", + "Whether the organisation provides the AI system or deploys it.": "Of de organisatie het AI-systeem aanbiedt of gebruikt.", + "Deployer": "Gebruiksverantwoordelijke", + "Algorithm register entry": "Vermelding in het algoritmeregister", + "The link to this system in the Dutch algorithm register.": "De link naar dit systeem in het Algoritmeregister van de Nederlandse overheid.", + "Last assessed on": "Laatst beoordeeld op", + "The date the classification was last assessed.": "De datum waarop de indeling voor het laatst is beoordeeld.", + "Fundamental rights impact assessment": "Grondrechteneffectbeoordeling", + "A reference to the fundamental rights impact assessment (FRIA). A high-risk system without one is flagged.": "Een verwijzing naar de grondrechteneffectbeoordeling (FRIA). Een systeem met hoog risico zonder beoordeling krijgt een waarschuwing.", + "Where the AI system stands in its lifecycle.": "Waar het AI-systeem staat in zijn levenscyclus.", + "AI Act evidence": "Bewijs voor de AI-verordening", + "Attach a document under Documents and give it the matching tag.": "Voeg een document toe onder Documenten en geef het de passende tag.", + "FRIA missing": "FRIA ontbreekt", + "Fundamental rights impact assessment (FRIA)": "Grondrechteneffectbeoordeling (FRIA)", + "Human oversight": "Menselijk toezicht", + "Loading the evidence": "Het bewijs wordt geladen", + "Logging": "Logging", + "Missing": "Ontbreekt", + "Technical documentation": "Technische documentatie", + "The evidence could not be loaded.": "Het bewijs kon niet worden geladen.", + "This is a high-risk AI system without a fundamental rights impact assessment.": "Dit is een AI-systeem met hoog risico zonder grondrechteneffectbeoordeling.", + "AI systems": "AI-systemen", + "The AI agents, AI models and AI features your organisation uses, with their EU AI Act risk category.": "De AI-agents, AI-modellen en AI-functies die je organisatie gebruikt, met hun risicocategorie onder de Europese AI-verordening.", + "FRIA": "FRIA", + "High risk without FRIA": "Hoog risico zonder FRIA", + "Application and supplier": "Applicatie en leverancier", + "No AI systems registered for this application": "Nog geen AI-systemen geregistreerd voor deze applicatie" } } diff --git a/lib/Settings/register.d/ai-system-inventory.json b/lib/Settings/register.d/ai-system-inventory.json new file mode 100644 index 00000000..a07555d8 --- /dev/null +++ b/lib/Settings/register.d/ai-system-inventory.json @@ -0,0 +1,325 @@ +{ + "components": { + "registers": { + "stackiq": { + "schemas": [ + "aiSystem" + ] + } + }, + "schemas": { + "aiSystem": { + "slug": "aiSystem", + "title": "AI system", + "x-schema-org": "schema:SoftwareApplication", + "description": "An AI agent, AI model or AI feature the organisation uses, with its EU AI Act classification.", + "version": "0.1.0", + "icon": "RobotOutline", + "required": [ + "name" + ], + "source": "internal", + "hardValidation": false, + "immutable": false, + "searchable": true, + "maxDepth": 0, + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name the organisation uses for this AI system.", + "facetable": false, + "order": 1, + "table": { + "default": true + } + }, + "description": { + "type": "string", + "format": "markdown", + "title": "Description", + "description": "What the AI system is and how it is used.", + "facetable": false, + "order": 2 + }, + "kind": { + "type": "string", + "enum": [ + "AI agent", + "AI model", + "AI feature" + ], + "x-enum-labels": { + "AI agent": "AI agent", + "AI model": "AI model", + "AI feature": "AI feature" + }, + "title": "Kind", + "description": "An AI agent acts on its own, an AI model is a trained model, an AI feature is part of an application.", + "facetable": true, + "order": 3, + "table": { + "default": true + } + }, + "module": { + "type": "object", + "$ref": "#/components/schemas/module", + "objectConfiguration": { + "handling": "related-object" + }, + "inversedBy": "aiSystems", + "title": "Application", + "description": "The application this AI system runs in or supports.", + "facetable": true, + "order": 4, + "table": { + "default": true + } + }, + "provider": { + "type": "object", + "$ref": "#/components/schemas/organization", + "objectConfiguration": { + "handling": "related-object" + }, + "title": "Supplier", + "description": "The organisation that supplies the AI system.", + "facetable": true, + "order": 5 + }, + "purpose": { + "type": "string", + "title": "Purpose", + "description": "What the AI system decides, recommends or produces.", + "facetable": false, + "order": 6 + }, + "aiActRiskCategory": { + "type": "string", + "enum": [ + "prohibited", + "high risk", + "limited risk", + "minimal risk", + "not yet assessed" + ], + "x-enum-labels": { + "prohibited": "Prohibited", + "high risk": "High risk", + "limited risk": "Limited risk", + "minimal risk": "Minimal risk", + "not yet assessed": "Not yet assessed" + }, + "default": "not yet assessed", + "title": "AI Act risk category", + "description": "The risk category under the EU AI Act, as the organisation classified it.", + "facetable": true, + "order": 7, + "table": { + "default": true + } + }, + "aiActRole": { + "type": "string", + "enum": [ + "provider", + "deployer" + ], + "x-enum-labels": { + "provider": "Provider", + "deployer": "Deployer" + }, + "title": "Role under the AI Act", + "description": "Whether the organisation provides the AI system or deploys it.", + "facetable": true, + "order": 8 + }, + "algorithmRegisterUrl": { + "type": "string", + "format": "uri", + "title": "Algorithm register entry", + "description": "The link to this system in the Dutch algorithm register.", + "facetable": false, + "order": 9 + }, + "assessedOn": { + "type": "string", + "format": "date", + "title": "Last assessed on", + "description": "The date the classification was last assessed.", + "facetable": false, + "order": 10 + }, + "friaDocumentRef": { + "type": "string", + "title": "Fundamental rights impact assessment", + "description": "A reference to the fundamental rights impact assessment (FRIA). A high-risk system without one is flagged.", + "facetable": false, + "order": 11 + }, + "status": { + "type": "string", + "enum": [ + "in development", + "in use", + "withdrawn" + ], + "x-enum-labels": { + "in development": "In development", + "in use": "In use", + "withdrawn": "Withdrawn" + }, + "default": "in use", + "title": "Status", + "description": "Where the AI system stands in its lifecycle.", + "facetable": true, + "order": 12, + "table": { + "default": true + } + } + }, + "configuration": { + "objectNameField": "name", + "objectDescriptionField": "purpose", + "allowFiles": true, + "allowedTags": [ + "FRIA", + "Technical documentation", + "Human oversight", + "Logging" + ], + "autoPublish": false, + "x-openregister-lifecycle": { + "field": "status", + "initial": "in development", + "final": [ + "withdrawn" + ], + "transitions": { + "release": { + "from": [ + "in development" + ], + "to": "in use", + "description": "Take the AI system into use." + }, + "withdraw": { + "from": [ + "in development", + "in use" + ], + "to": "withdrawn", + "description": "Withdraw the AI system." + } + } + } + }, + "authorization": { + "create": [ + "software-catalog-admins", + "organisatie-beheerder", + "organisaties-beheerder", + "functioneel-beheerder", + "gebruik-beheerder", + "aanbod-beheerder" + ], + "read": [ + "software-catalog-admins", + { + "group": "organisatie-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "organisaties-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "functioneel-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "gebruik-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "aanbod-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "aanbod-beheerder", + "match": { + "provider": "$organisation" + } + } + ], + "update": [ + "software-catalog-admins", + { + "group": "organisatie-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "organisaties-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "functioneel-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "gebruik-beheerder", + "match": { + "_organisation": "$organisation" + } + } + ], + "delete": [ + "software-catalog-admins", + { + "group": "organisatie-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "organisaties-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "functioneel-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "gebruik-beheerder", + "match": { + "_organisation": "$organisation" + } + } + ] + } + } + } + } +} diff --git a/lib/Settings/stackiq_mock_register.json b/lib/Settings/stackiq_mock_register.json index 2fd93331..2833ddc7 100644 --- a/lib/Settings/stackiq_mock_register.json +++ b/lib/Settings/stackiq_mock_register.json @@ -7343,6 +7343,318 @@ "objectDescriptionField": "longDescription", "autoPublish": false } + }, + "aiSystem": { + "slug": "aiSystem", + "title": "AI system", + "x-schema-org": "schema:SoftwareApplication", + "description": "An AI agent, AI model or AI feature the organisation uses, with its EU AI Act classification.", + "version": "0.1.0", + "icon": "RobotOutline", + "required": [ + "name" + ], + "source": "internal", + "hardValidation": false, + "immutable": false, + "searchable": true, + "maxDepth": 0, + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name the organisation uses for this AI system.", + "facetable": false, + "order": 1, + "table": { + "default": true + } + }, + "description": { + "type": "string", + "format": "markdown", + "title": "Description", + "description": "What the AI system is and how it is used.", + "facetable": false, + "order": 2 + }, + "kind": { + "type": "string", + "enum": [ + "AI agent", + "AI model", + "AI feature" + ], + "x-enum-labels": { + "AI agent": "AI agent", + "AI model": "AI model", + "AI feature": "AI feature" + }, + "title": "Kind", + "description": "An AI agent acts on its own, an AI model is a trained model, an AI feature is part of an application.", + "facetable": true, + "order": 3, + "table": { + "default": true + } + }, + "module": { + "type": "object", + "$ref": "#/components/schemas/module", + "objectConfiguration": { + "handling": "related-object" + }, + "inversedBy": "aiSystems", + "title": "Application", + "description": "The application this AI system runs in or supports.", + "facetable": true, + "order": 4, + "table": { + "default": true + } + }, + "provider": { + "type": "object", + "$ref": "#/components/schemas/organization", + "objectConfiguration": { + "handling": "related-object" + }, + "title": "Supplier", + "description": "The organisation that supplies the AI system.", + "facetable": true, + "order": 5 + }, + "purpose": { + "type": "string", + "title": "Purpose", + "description": "What the AI system decides, recommends or produces.", + "facetable": false, + "order": 6 + }, + "aiActRiskCategory": { + "type": "string", + "enum": [ + "prohibited", + "high risk", + "limited risk", + "minimal risk", + "not yet assessed" + ], + "x-enum-labels": { + "prohibited": "Prohibited", + "high risk": "High risk", + "limited risk": "Limited risk", + "minimal risk": "Minimal risk", + "not yet assessed": "Not yet assessed" + }, + "default": "not yet assessed", + "title": "AI Act risk category", + "description": "The risk category under the EU AI Act, as the organisation classified it.", + "facetable": true, + "order": 7, + "table": { + "default": true + } + }, + "aiActRole": { + "type": "string", + "enum": [ + "provider", + "deployer" + ], + "x-enum-labels": { + "provider": "Provider", + "deployer": "Deployer" + }, + "title": "Role under the AI Act", + "description": "Whether the organisation provides the AI system or deploys it.", + "facetable": true, + "order": 8 + }, + "algorithmRegisterUrl": { + "type": "string", + "format": "uri", + "title": "Algorithm register entry", + "description": "The link to this system in the Dutch algorithm register.", + "facetable": false, + "order": 9 + }, + "assessedOn": { + "type": "string", + "format": "date", + "title": "Last assessed on", + "description": "The date the classification was last assessed.", + "facetable": false, + "order": 10 + }, + "friaDocumentRef": { + "type": "string", + "title": "Fundamental rights impact assessment", + "description": "A reference to the fundamental rights impact assessment (FRIA). A high-risk system without one is flagged.", + "facetable": false, + "order": 11 + }, + "status": { + "type": "string", + "enum": [ + "in development", + "in use", + "withdrawn" + ], + "x-enum-labels": { + "in development": "In development", + "in use": "In use", + "withdrawn": "Withdrawn" + }, + "default": "in use", + "title": "Status", + "description": "Where the AI system stands in its lifecycle.", + "facetable": true, + "order": 12, + "table": { + "default": true + } + } + }, + "configuration": { + "objectNameField": "name", + "objectDescriptionField": "purpose", + "allowFiles": true, + "allowedTags": [ + "FRIA", + "Technical documentation", + "Human oversight", + "Logging" + ], + "autoPublish": false, + "x-openregister-lifecycle": { + "field": "status", + "initial": "in development", + "final": [ + "withdrawn" + ], + "transitions": { + "release": { + "from": [ + "in development" + ], + "to": "in use", + "description": "Take the AI system into use." + }, + "withdraw": { + "from": [ + "in development", + "in use" + ], + "to": "withdrawn", + "description": "Withdraw the AI system." + } + } + } + }, + "authorization": { + "create": [ + "software-catalog-admins", + "organisatie-beheerder", + "organisaties-beheerder", + "functioneel-beheerder", + "gebruik-beheerder", + "aanbod-beheerder" + ], + "read": [ + "software-catalog-admins", + { + "group": "organisatie-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "organisaties-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "functioneel-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "gebruik-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "aanbod-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "aanbod-beheerder", + "match": { + "provider": "$organisation" + } + } + ], + "update": [ + "software-catalog-admins", + { + "group": "organisatie-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "organisaties-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "functioneel-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "gebruik-beheerder", + "match": { + "_organisation": "$organisation" + } + } + ], + "delete": [ + "software-catalog-admins", + { + "group": "organisatie-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "organisaties-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "functioneel-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "gebruik-beheerder", + "match": { + "_organisation": "$organisation" + } + } + ] + } } }, "objects": [ @@ -10520,6 +10832,39 @@ "longDescription": "Voorbeeld markdown 2", "cveCode": "CVE-2345-2345", "cvssScore": 2.0 + }, + { + "@self": { + "register": "stackiq", + "schema": "aiSystem", + "slug": "ai-system-chat-assistant" + }, + "name": "Chat assistant", + "kind": "AI feature", + "module": {}, + "provider": {}, + "purpose": "Answers residents' questions about permits on the website and hands over to an employee.", + "aiActRiskCategory": "limited risk", + "aiActRole": "deployer", + "assessedOn": "2026-06-01", + "algorithmRegisterUrl": "https://algoritmes.overheid.nl/", + "status": "in use" + }, + { + "@self": { + "register": "stackiq", + "schema": "aiSystem", + "slug": "ai-system-benefit-scoring-model" + }, + "name": "Benefit application scoring model", + "kind": "AI model", + "module": {}, + "provider": {}, + "purpose": "Ranks benefit applications for a manual check.", + "aiActRiskCategory": "high risk", + "aiActRole": "deployer", + "assessedOn": "2026-05-15", + "status": "in development" } ] } diff --git a/src/App.vue b/src/App.vue index 1987ef14..38d264a5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -20,6 +20,7 @@ :aiCompanion="true" :manifest="manifest" :customComponents="customComponents" + :formatters="formatters" :registry="registry" :pageTypes="pageTypes" appId="stackiq" @@ -75,6 +76,7 @@ import OrganisationSwitcher from './components/organisations/OrganisationSwitche import Dialogs from './dialogs/Dialogs.vue' import Modals from './modals/Modals.vue' import { setActiveOrganisationUuid } from './composables/orClient.js' +import { friaStatus } from './utils/aiAct.js' import { settingsStore } from './store/store.js' export default { @@ -148,6 +150,8 @@ export default { data() { return { + // App cell formatters for manifest columns (`columns[].formatter`). + formatters: { friaStatus }, objectSidebarState: reactive({ active: false, open: true, diff --git a/src/components/ai/AiActChecklist.vue b/src/components/ai/AiActChecklist.vue new file mode 100644 index 00000000..fad10e43 --- /dev/null +++ b/src/components/ai/AiActChecklist.vue @@ -0,0 +1,211 @@ + + + + + + diff --git a/src/customComponents.js b/src/customComponents.js index 3ca4565e..beda735c 100644 --- a/src/customComponents.js +++ b/src/customComponents.js @@ -19,6 +19,7 @@ import { generateUrl } from '@nextcloud/router' import OrganisatieCard from './components/cards/OrganisatieCard.vue' +import AiActChecklist from './components/ai/AiActChecklist.vue' import ApplicationContractsPanel from './components/contracts/ApplicationContractsPanel.vue' import ContractApprovalPanel from './components/contracts/ContractApprovalPanel.vue' import ContractSeatsPanel from './components/contracts/ContractSeatsPanel.vue' @@ -90,6 +91,10 @@ export default { // Licences in use against licences bought (contracts-licence-seats). ContractSeatsPanel, + // AI Act evidence per tag on the AI system page (landscape-ai-system-inventory): + // it reads the object's files and their tags, which no built-in widget lists per tag. + AiActChecklist, + // --- Admin-triggered organisation-merge (VNG Softwarecatalogus #141). --- // Dry-run preview + confirm dialog + execute for folding a source // organisation into a target (gemeentelijke herindeling / diff --git a/src/icons.js b/src/icons.js index be09aea7..78210e07 100644 --- a/src/icons.js +++ b/src/icons.js @@ -55,6 +55,7 @@ import PackageVariant from 'vue-material-design-icons/PackageVariant.vue' import PackageVariantClosed from 'vue-material-design-icons/PackageVariantClosed.vue' import PowerPlugOutline from 'vue-material-design-icons/PowerPlugOutline.vue' import PuzzleOutline from 'vue-material-design-icons/PuzzleOutline.vue' +import RobotOutline from 'vue-material-design-icons/RobotOutline.vue' import ShieldAlert from 'vue-material-design-icons/ShieldAlert.vue' import ShieldAlertOutline from 'vue-material-design-icons/ShieldAlertOutline.vue' import ShieldCheckOutline from 'vue-material-design-icons/ShieldCheckOutline.vue' @@ -114,6 +115,7 @@ export default { PackageVariantClosed, PowerPlugOutline, PuzzleOutline, + RobotOutline, ShieldAlert, ShieldAlertOutline, ShieldCheckOutline, diff --git a/src/manifest.d/ai-systems.json b/src/manifest.d/ai-systems.json new file mode 100644 index 00000000..63cb9fa5 --- /dev/null +++ b/src/manifest.d/ai-systems.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json", + "_note": "landscape-ai-system-inventory: the aiSystem schema from lib/Settings/register.d/ai-system-inventory.json. A menu child of Applications (ADR-097). The High risk without FRIA quick filter sends friaDocumentRef=IS NULL, which OpenRegister's property filter reads as a null check; the FRIA column uses the app formatter friaStatus (src/utils/aiAct.js), so the list warns on the same rule.", + "menu": [ + { + "id": "Modules", + "children": [ + { "id": "AiSystems", "label": "AI systems", "icon": "RobotOutline", "route": "AiSystems", "order": 11 } + ] + } + ], + "pages": [ + { + "id": "AiSystems", + "route": "/ai-systems", + "type": "index", + "title": "AI systems", + "config": { + "register": "@resolve:voorzieningen_register", + "schema": "aiSystem", + "description": "The AI agents, AI models and AI features your organisation uses, with their EU AI Act risk category.", + "columns": [ + "name", + "kind", + "module", + "aiActRiskCategory", + "status", + { "key": "friaDocumentRef", "label": "FRIA", "formatter": "friaStatus", "widget": "badge" } + ], + "filterMenu": true, + "quickFilters": [ + { "label": "All", "filter": {}, "default": true }, + { "label": "High risk", "filter": { "aiActRiskCategory": "high risk" }, "icon": "ShieldAlert" }, + { "label": "High risk without FRIA", "filter": { "aiActRiskCategory": "high risk", "friaDocumentRef": "IS NULL" }, "icon": "AlertCircle" }, + { "label": "Limited risk", "filter": { "aiActRiskCategory": "limited risk" } }, + { "label": "Minimal risk", "filter": { "aiActRiskCategory": "minimal risk" } }, + { "label": "Not yet assessed", "filter": { "aiActRiskCategory": "not yet assessed" } }, + { "label": "Prohibited", "filter": { "aiActRiskCategory": "prohibited" } } + ], + "sidebar": { "enabled": true, "showMetadata": true }, + "documentationUrl": "https://stackiq.conduction.nl" + } + }, + { + "id": "AiSystemDetail", + "route": "/ai-systems/:id", + "type": "detail", + "title": "AI system", + "config": { + "register": "@resolve:voorzieningen_register", + "schema": "aiSystem", + "_note": "Data 8 wide with the AI Act fields, documents 4 wide (the four evidence tags), the related panel, then the evidence checklist as a body widget. Status transitions come from the schema's x-openregister-lifecycle (release, withdraw).", + "lifecycleActions": { "field": "status" }, + "widgets": [ + { "id": "ai-data", "type": "data", "title": "AI system", "icon": "RobotOutline", "content": { "columns": 2, "include": [ "name", "kind", "module", "provider", "purpose", "status", "aiActRiskCategory", "aiActRole", "assessedOn", "friaDocumentRef", "algorithmRegisterUrl", "description" ] } }, + { "id": "ai-files", "type": "integration", "integrationId": "files", "title": "Documents", "icon": "FolderOutline" }, + { "id": "ai-related", "type": "related", "title": "Application and supplier", "icon": "LinkVariant" } + ], + "layout": [ + { "id": "1", "widgetId": "ai-data", "gridX": 0, "gridY": 0, "gridWidth": 8, "gridHeight": 8 }, + { "id": "2", "widgetId": "ai-files", "gridX": 8, "gridY": 0, "gridWidth": 4, "gridHeight": 4 }, + { "id": "3", "widgetId": "ai-related", "gridX": 8, "gridY": 4, "gridWidth": 4, "gridHeight": 4 } + ], + "bodyWidgets": [ + { "id": "ai-checklist", "component": "AiActChecklist", "props": { "objectId": "@objectId" }, "placement": "end", "colSpan": 12 } + ], + "sidebar": { + "enabled": true, + "showMetadata": true, + "tabs": [ + { "id": "audit", "label": "History", "icon": "History", "widgets": [ { "type": "audit" } ] } + ] + }, + "documentationUrl": "https://stackiq.conduction.nl" + } + } + ] +} diff --git a/src/manifest.json b/src/manifest.json index 2ab235b4..5f5bd1cc 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -504,7 +504,8 @@ { "id": "md-versions", "type": "object-list", "title": "Application versions", "icon": "SourceBranch", "content": { "register": "@resolve:voorzieningen_register", "schema": "moduleVersion", "filter": { "module": "@objectId" }, "columns": [ { "key": "version", "label": "Version" }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "ModuleversieDetail", "allowCreate": false, "emptyText": "No versions registered yet" } }, { "id": "md-usages", "type": "object-list", "title": "Usages", "icon": "OfficeBuilding", "content": { "register": "@resolve:voorzieningen_register", "schema": "usage", "filter": { "module": "@objectId" }, "columns": [ { "key": "consumer", "label": "Organisation" }, { "key": "moduleVersion", "label": "Version" }, { "key": "status", "label": "Status" } ], "limit": 50, "allowCreate": false, "emptyText": "No organisation registered a usage yet" } }, { "id": "md-connections-out", "type": "object-list", "title": "Connections from this application", "icon": "LinkVariant", "content": { "register": "@resolve:voorzieningen_register", "schema": "connection", "filter": { "moduleA": "@objectId" }, "columns": [ { "key": "moduleB", "label": "To application" }, { "key": "nonMunicipalProvision", "label": "To national provision" }, { "key": "type", "label": "Type" }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "KoppelingDetail", "viewAllRoute": "Koppelingen", "viewAllQuery": { "moduleA": "@objectId" }, "allowCreate": false, "emptyText": "No connections start at this application" } }, - { "id": "md-connections-in", "type": "object-list", "title": "Connections to this application", "icon": "LinkVariant", "content": { "register": "@resolve:voorzieningen_register", "schema": "connection", "filter": { "moduleB": "@objectId" }, "columns": [ { "key": "moduleA", "label": "From application" }, { "key": "type", "label": "Type" }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "KoppelingDetail", "viewAllRoute": "Koppelingen", "viewAllQuery": { "moduleB": "@objectId" }, "allowCreate": false, "emptyText": "No connections end at this application" } } + { "id": "md-connections-in", "type": "object-list", "title": "Connections to this application", "icon": "LinkVariant", "content": { "register": "@resolve:voorzieningen_register", "schema": "connection", "filter": { "moduleB": "@objectId" }, "columns": [ { "key": "moduleA", "label": "From application" }, { "key": "type", "label": "Type" }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "KoppelingDetail", "viewAllRoute": "Koppelingen", "viewAllQuery": { "moduleB": "@objectId" }, "allowCreate": false, "emptyText": "No connections end at this application" } }, + { "id": "md-ai-systems", "type": "object-list", "title": "AI systems", "icon": "RobotOutline", "content": { "register": "@resolve:voorzieningen_register", "schema": "aiSystem", "filter": { "module": "@objectId" }, "columns": [ { "key": "kind", "label": "Kind" }, { "key": "aiActRiskCategory", "label": "AI Act risk category" }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "AiSystemDetail", "viewAllRoute": "AiSystems", "viewAllQuery": { "module": "@objectId" }, "allowCreate": false, "emptyText": "No AI systems registered for this application" } } ], "layout": [ { "id": "1", "widgetId": "md-data", "gridX": 0, "gridY": 0, "gridWidth": 8, "gridHeight": 8 }, @@ -514,7 +515,8 @@ { "id": "5", "widgetId": "md-usages", "gridX": 6, "gridY": 8, "gridWidth": 6, "gridHeight": 4 }, { "id": "6", "widgetId": "md-compliance", "gridX": 0, "gridY": 12, "gridWidth": 12, "gridHeight": 4 }, { "id": "7", "widgetId": "md-connections-out", "gridX": 0, "gridY": 16, "gridWidth": 6, "gridHeight": 4 }, - { "id": "8", "widgetId": "md-connections-in", "gridX": 6, "gridY": 16, "gridWidth": 6, "gridHeight": 4 } + { "id": "8", "widgetId": "md-connections-in", "gridX": 6, "gridY": 16, "gridWidth": 6, "gridHeight": 4 }, + { "id": "9", "widgetId": "md-ai-systems", "gridX": 0, "gridY": 20, "gridWidth": 12, "gridHeight": 4 } ], "bodyWidgets": [ { "id": "md-contracts", "component": "ApplicationContractsPanel", "props": { "objectId": "@objectId" }, "placement": "end", "colSpan": 12 }, diff --git a/src/utils/aiAct.js b/src/utils/aiAct.js new file mode 100644 index 00000000..f17c3796 --- /dev/null +++ b/src/utils/aiAct.js @@ -0,0 +1,67 @@ +/** + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * EU AI Act helpers for the AI systems pages: the missing-FRIA rule the list + * warns on, and the evidence checklist the detail page shows. + * + * @spec openspec/specs/ai-system-inventory/spec.md#requirement-req-ais-003-a-high-risk-ai-system-without-a-fundamental-rights-impact-assessment-is-flagged + */ + +import { translate as t } from '@nextcloud/l10n' + +/** + * The evidence tags a deployer of a high-risk AI system keeps, in the order + * the checklist shows them. The same list is the schema's `allowedTags`. + */ +export const EVIDENCE_TAGS = [ + 'FRIA', + 'Technical documentation', + 'Human oversight', + 'Logging', +] + +/** + * Whether an AI system is high risk and has no FRIA reference. + * + * @param {object} system The aiSystem object. + * @return {boolean} True when the FRIA is missing. + * @spec openspec/specs/ai-system-inventory/spec.md#requirement-req-ais-003-a-high-risk-ai-system-without-a-fundamental-rights-impact-assessment-is-flagged + */ +export function friaMissing(system) { + if (!system || system.aiActRiskCategory !== 'high risk') { + return false + } + const ref = system.friaDocumentRef + return typeof ref !== 'string' || ref.trim() === '' +} + +/** + * Cell formatter for the FRIA column of the AI systems list: reads + * "FRIA missing" on a flagged system and nothing otherwise. + * + * @param {unknown} _value The friaDocumentRef value (the row is read instead). + * @param {object} row The aiSystem row. + * @return {string} The warning, or an empty string. + * @spec openspec/specs/ai-system-inventory/spec.md#requirement-req-ais-003-a-high-risk-ai-system-without-a-fundamental-rights-impact-assessment-is-flagged + */ +export function friaStatus(_value, row) { + return friaMissing(row) ? t('stackiq', 'FRIA missing') : '' +} + +/** + * The evidence checklist: one entry per tag, present when a file carries it. + * + * @param {Array|undefined} files The object's files, each with `labels`. + * @return {Array<{tag: string, present: boolean, files: Array}>} The checklist. + * @spec openspec/specs/ai-system-inventory/spec.md#requirement-req-ais-003-a-high-risk-ai-system-without-a-fundamental-rights-impact-assessment-is-flagged + */ +export function evidenceChecklist(files) { + const list = Array.isArray(files) ? files : [] + return EVIDENCE_TAGS.map((tag) => { + const names = list + .filter((f) => Array.isArray(f?.labels) && f.labels.includes(tag)) + .map((f) => f.name) + return { tag, present: names.length > 0, files: names } + }) +} diff --git a/tests/Unit/Settings/AiSystemFragmentTest.php b/tests/Unit/Settings/AiSystemFragmentTest.php index bbe29c65..95cf8b6e 100644 --- a/tests/Unit/Settings/AiSystemFragmentTest.php +++ b/tests/Unit/Settings/AiSystemFragmentTest.php @@ -157,7 +157,15 @@ public function testReadsAreScopedToTheOrganisation(): void { $this->assertContains(['group' => 'gebruik-beheerder', 'match' => ['_organisation' => '$organisation']], $read); $this->assertContains(['group' => 'aanbod-beheerder', 'match' => ['provider' => '$organisation']], $read); foreach ($read as $rule) { - $this->assertIsArray($rule, 'no read rule may grant a whole group every AI system'); + if (is_string($rule) === true) { + $this->assertSame('software-catalog-admins', $rule, 'only the catalogue admins read every AI system'); + } + } + + foreach ($this->schema()['authorization']['update'] as $rule) { + if (is_string($rule) === true) { + $this->assertSame('software-catalog-admins', $rule, 'only the catalogue admins edit every AI system'); + } } }//end testReadsAreScopedToTheOrganisation() }//end class diff --git a/tests/e2e/workflows/ai-systems.spec.ts b/tests/e2e/workflows/ai-systems.spec.ts new file mode 100644 index 00000000..a8203c76 --- /dev/null +++ b/tests/e2e/workflows/ai-systems.spec.ts @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2026 Conduction B.V. +/** + * AI systems: an AI feature listed on its application's page, the high-risk + * quick filter, and the missing FRIA warning in the list and on the page. + * + * Seeds one application and three AI systems carrying this run's RUN_ID + * through the objects API (the call the Add form makes), and removes exactly + * those rows afterwards. The FRIA rule and the checklist logic are covered by + * tests/vitest/aiSystems.spec.js. + * + * @spec openspec/specs/ai-system-inventory/spec.md + */ +import type { APIRequestContext } from '@playwright/test' +import type { VoorzieningenConfig } from './_fixtures.ts' + +import { expect, test } from '@playwright/test' +import { + createObject, + deleteObject, + newApiContext, + resolveConfig, + RUN_ID, +} from './_fixtures.ts' +import { dismissSupportDialog, gotoAppRoute } from './_ui.ts' + +let apiCtx: APIRequestContext +let cfg: VoorzieningenConfig +const seeded: Array<[string, string]> = [] +const ids: Record = {} +const chat = `${RUN_ID} chat assistant` +const scoring = `${RUN_ID} scoring model` +const checked = `${RUN_ID} checked model` + +/** + * Create a row and remember it for cleanup. + * + * @param schema The schema slug. + * @param data The object. + * @return The new id. + */ +async function seed(schema: string, data: Record): Promise { + const id = await createObject(apiCtx, cfg.register, schema, data) + seeded.push([schema, id]) + return id +} + +test.beforeAll(async () => { + apiCtx = await newApiContext() + cfg = await resolveConfig(apiCtx) + ids.x = await seed('module', { name: `${RUN_ID} application X` }) + ids.chat = await seed('aiSystem', { + name: chat, + kind: 'AI feature', + module: ids.x, + aiActRiskCategory: 'minimal risk', + status: 'in use', + }) + ids.scoring = await seed('aiSystem', { + name: scoring, + kind: 'AI model', + aiActRiskCategory: 'high risk', + status: 'in use', + }) + ids.checked = await seed('aiSystem', { + name: checked, + kind: 'AI model', + aiActRiskCategory: 'high risk', + friaDocumentRef: '/AI/fria.pdf', + status: 'in use', + }) +}) + +test.afterAll(async () => { + if (!apiCtx) return + for (const [schema, id] of seeded.reverse()) { + await deleteObject(apiCtx, cfg.register, schema, id) + } + await apiCtx.dispose() +}) + +// @e2e ai-system-inventory::an-information-manager-registers-a-chat-assistant +test('the application page lists its AI feature in the AI systems section', async ({ + page, +}) => { + await gotoAppRoute(page, `/modules/${ids.x}`) + await dismissSupportDialog(page) + await expect(page.getByText('AI systems').first()).toBeVisible({ + timeout: 30000, + }) + await expect(page.getByText(chat).first()).toBeVisible({ timeout: 30000 }) + await page.getByText(chat).first().click() + await expect(page).toHaveURL(new RegExp(`/ai-systems/${ids.chat}`)) +}) + +// @e2e ai-system-inventory::a-privacy-officer-lists-the-high-risk-systems +test('filtering on high risk leaves only the high-risk systems', async ({ + page, +}) => { + await gotoAppRoute(page, '/ai-systems') + await dismissSupportDialog(page) + await expect(page.getByText(chat).first()).toBeVisible({ timeout: 30000 }) + await page.getByRole('tab', { name: 'High risk', exact: true }).click() + await expect(page.getByText(scoring).first()).toBeVisible({ timeout: 30000 }) + await expect(page.getByText(chat)).toHaveCount(0) +}) + +// @e2e ai-system-inventory::the-missing-assessment-shows +test('a high-risk system without a FRIA is listed with a warning and its page marks FRIA missing', async ({ + page, +}) => { + await gotoAppRoute(page, '/ai-systems') + await dismissSupportDialog(page) + await page + .getByRole('tab', { name: 'High risk without FRIA', exact: true }) + .click() + const row = page.getByRole('row').filter({ hasText: scoring }) + await expect(row).toContainText('FRIA missing', { timeout: 30000 }) + await expect(page.getByText(checked)).toHaveCount(0) + + await gotoAppRoute(page, `/ai-systems/${ids.scoring}`) + await expect(page.getByTestId('ai-act-fria-warning')).toBeVisible({ + timeout: 30000, + }) + await expect(page.getByTestId('ai-act-evidence-FRIA')).toHaveAttribute( + 'data-present', + 'false', + ) +}) From 12ec428105a24b4d272286803f501a76a334c265 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 29 Sep 2026 20:07:42 +0200 Subject: [PATCH 3/5] docs(openspec): archive landscape-ai-system-inventory, two rows are built --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/ai-system-inventory/spec.md | 0 .../tasks.md | 23 +++++++---- openspec/parity/capabilities.json | 32 ++++++++------- openspec/specs/ai-system-inventory/spec.md | 40 +++++++++++++++++++ 7 files changed, 73 insertions(+), 22 deletions(-) rename openspec/changes/{landscape-ai-system-inventory => archive/2026-09-29-landscape-ai-system-inventory}/.openspec.yaml (100%) rename openspec/changes/{landscape-ai-system-inventory => archive/2026-09-29-landscape-ai-system-inventory}/design.md (100%) rename openspec/changes/{landscape-ai-system-inventory => archive/2026-09-29-landscape-ai-system-inventory}/proposal.md (100%) rename openspec/changes/{landscape-ai-system-inventory => archive/2026-09-29-landscape-ai-system-inventory}/specs/ai-system-inventory/spec.md (100%) rename openspec/changes/{landscape-ai-system-inventory => archive/2026-09-29-landscape-ai-system-inventory}/tasks.md (66%) create mode 100644 openspec/specs/ai-system-inventory/spec.md diff --git a/openspec/changes/landscape-ai-system-inventory/.openspec.yaml b/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/.openspec.yaml similarity index 100% rename from openspec/changes/landscape-ai-system-inventory/.openspec.yaml rename to openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/.openspec.yaml diff --git a/openspec/changes/landscape-ai-system-inventory/design.md b/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/design.md similarity index 100% rename from openspec/changes/landscape-ai-system-inventory/design.md rename to openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/design.md diff --git a/openspec/changes/landscape-ai-system-inventory/proposal.md b/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/proposal.md similarity index 100% rename from openspec/changes/landscape-ai-system-inventory/proposal.md rename to openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/proposal.md diff --git a/openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md b/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/specs/ai-system-inventory/spec.md similarity index 100% rename from openspec/changes/landscape-ai-system-inventory/specs/ai-system-inventory/spec.md rename to openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/specs/ai-system-inventory/spec.md diff --git a/openspec/changes/landscape-ai-system-inventory/tasks.md b/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/tasks.md similarity index 66% rename from openspec/changes/landscape-ai-system-inventory/tasks.md rename to openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/tasks.md index 9f3be00d..e32730ed 100644 --- a/openspec/changes/landscape-ai-system-inventory/tasks.md +++ b/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/tasks.md @@ -7,8 +7,8 @@ - **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`) +- [x] Implement +- [x] 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 @@ -16,8 +16,8 @@ - **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`) +- [x] Implement +- [x] 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 @@ -25,19 +25,26 @@ - **acceptance_criteria**: - 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`; Playwright case in `tests/e2e/workflows/ai-systems.spec.ts`) +- [x] Implement +- [x] 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 - **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) +- [x] Implement +- [x] 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). + +## As built (2026-09-29) + +- The schema, pages, checklist rule and seeds are tested in `tests/Unit/Settings/AiSystemFragmentTest.php` and `tests/vitest/aiSystems.spec.js` (the vitest file also covers what `aiActChecklist.spec.js` was to hold). +- The High risk without FRIA filter sends `friaDocumentRef=IS NULL`, which OpenRegister's property filter reads as a null check. The warning column uses an app cell formatter `friaStatus` (src/utils/aiAct.js), passed to CnAppRoot as `formatters`. +- `tests/e2e/workflows/ai-systems.spec.ts` seeds the AI systems through the objects API, the call the Add form makes, rather than typing into the form. It lists but was not run: no local instance has a seeded stackiq register. The docs screenshot waits for that instance; `docs/images/ai-systems.png` is not added. +- Seeds: a chat assistant (AI feature, limited risk) and a scoring model (AI model, high risk, no FRIA). Like every other demo object they carry no relation, so the chat assistant is not linked to a demo application. diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index 8015594d..e6634f94 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -5283,13 +5283,14 @@ "name": "Register the AI agents and AI models the organisation uses and link them to the applications and processes they support.", "origin": "changelog", "originUrl": "https://updates.leanix.net/announcements/discover-verify-and-govern-ai-assets-with-sap-ai-agent-hub", - "stackiq": "no", + "stackiq": "partial", "built": { - "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" + "state": "built", + "evidence": "lib/Settings/register.d/ai-system-inventory.json schema aiSystem (kind AI agent/AI model/AI feature, module, provider, purpose, status with lifecycle) in the stackiq register; src/manifest.d/ai-systems.json pages AiSystems (/ai-systems) and AiSystemDetail; ModuleDetail widget md-ai-systems; tests/Unit/Settings/AiSystemFragmentTest.php, tests/vitest/aiSystems.spec.js", + "owner": "ConductionNL/stackiq", + "change": "2026-09-29-landscape-ai-system-inventory" }, - "reachedOn": "nothing reaches it", + "reachedOn": "Applications > AI systems (/ai-systems), AI system page, and the AI systems section on the application page", "provider": "stackiq", "providerHow": "read-from-code", "featureConfidence": "medium", @@ -5299,9 +5300,10 @@ "vng-softwarecatalogus": "unknown: AI agents and models are not described; searched the public manuals and FAQ at https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier (read 2026-09-26)", "bluedolphin": "not checked: row added in wave 5 (2026-09-26), no reading of this system for it yet", "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)" + "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)", + "stackiq": "lib/Settings/register.d/ai-system-inventory.json schema aiSystem (kind AI agent/AI model/AI feature, module, provider, purpose, status with lifecycle) in the stackiq register; src/manifest.d/ai-systems.json pages AiSystems (/ai-systems) and AiSystemDetail; ModuleDetail widget md-ai-systems; tests/Unit/Settings/AiSystemFragmentTest.php, tests/vitest/aiSystems.spec.js" }, - "note": "Mined from sap-leanix (changelog) on 2026-09-26. Specified in openspec/changes/landscape-ai-system-inventory (OpenSpec pass 2026-09-27).", + "note": "Mined from sap-leanix (changelog) on 2026-09-26. Specified in openspec/changes/landscape-ai-system-inventory (OpenSpec pass 2026-09-27). AI systems are registered and linked to the application they run in; linking them to processes waits for architecture-process-mapping (built by openspec/changes/archive/2026-09-29-landscape-ai-system-inventory).", "vng-softwarecatalogus": "unknown", "bluedolphin": "unknown", "glpi": "partial", @@ -5373,13 +5375,14 @@ "name": "Classify the AI systems in the landscape by EU AI Act risk category and keep the evidence the act requires.", "origin": "roadmap", "originUrl": "https://roadmap.leanix.net/c/812-meta-model-eu-ai-act-extension", - "stackiq": "no", + "stackiq": "yes", "built": { - "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" + "state": "built", + "evidence": "aiSystem.aiActRiskCategory (prohibited, high risk, limited risk, minimal risk, not yet assessed), aiActRole, assessedOn, algorithmRegisterUrl, friaDocumentRef; evidence files tagged FRIA, Technical documentation, Human oversight, Logging; AiActChecklist on AiSystemDetail and the High risk without FRIA quick filter (src/utils/aiAct.js); tests/vitest/aiSystems.spec.js", + "owner": "ConductionNL/stackiq", + "change": "2026-09-29-landscape-ai-system-inventory" }, - "reachedOn": "nothing reaches it", + "reachedOn": "Applications > AI systems (/ai-systems), AI system page, and the AI systems section on the application page", "provider": "stackiq", "providerHow": "read-from-code", "featureConfidence": "medium", @@ -5389,9 +5392,10 @@ "vng-softwarecatalogus": "unknown: the EU AI Act is not described; searched the public manuals and FAQ at https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier (read 2026-09-26)", "bluedolphin": "not checked: row added in wave 5 (2026-09-26), no reading of this system for it yet", "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)" + "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)", + "stackiq": "aiSystem.aiActRiskCategory (prohibited, high risk, limited risk, minimal risk, not yet assessed), aiActRole, assessedOn, algorithmRegisterUrl, friaDocumentRef; evidence files tagged FRIA, Technical documentation, Human oversight, Logging; AiActChecklist on AiSystemDetail and the High risk without FRIA quick filter (src/utils/aiAct.js); tests/vitest/aiSystems.spec.js" }, - "note": "Mined from sap-leanix (roadmap) on 2026-09-26. Specified in openspec/changes/landscape-ai-system-inventory (OpenSpec pass 2026-09-27).", + "note": "Mined from sap-leanix (roadmap) on 2026-09-26. Specified in openspec/changes/landscape-ai-system-inventory (OpenSpec pass 2026-09-27). An AI system records its AI Act risk category, role and evidence, and a high-risk system without a FRIA is flagged (built by openspec/changes/archive/2026-09-29-landscape-ai-system-inventory).", "vng-softwarecatalogus": "unknown", "bluedolphin": "unknown", "glpi": "no", diff --git a/openspec/specs/ai-system-inventory/spec.md b/openspec/specs/ai-system-inventory/spec.md new file mode 100644 index 00000000..f96f3cc4 --- /dev/null +++ b/openspec/specs/ai-system-inventory/spec.md @@ -0,0 +1,40 @@ +# ai-system-inventory Specification + +## 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`. + +## 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. 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 +- **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 From a28c8a9800b306458df80992bb026ab3b5a529f0 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 29 Sep 2026 20:38:04 +0200 Subject: [PATCH 4/5] fix(ai-systems): the aiSystem schema lives in the register itself, three demo systems, formatters in their own module --- .../register.d/ai-system-inventory.json | 325 ------------------ lib/Settings/softwarecatalogus_register.json | 320 ++++++++++++++++- lib/Settings/stackiq_mock_register.json | 16 + .../design.md | 4 +- .../tasks.md | 6 +- openspec/parity/capabilities.json | 4 +- src/App.vue | 4 +- src/components/ai/AiActChecklist.vue | 46 ++- src/customComponents.js | 2 +- src/formatters.js | 14 + src/manifest.d/ai-systems.json | 2 +- tests/Unit/Settings/AiSystemFragmentTest.php | 4 +- tests/vitest/aiSystems.spec.js | 28 +- tests/vitest/connectionRegistry.spec.js | 14 +- 14 files changed, 423 insertions(+), 366 deletions(-) delete mode 100644 lib/Settings/register.d/ai-system-inventory.json create mode 100644 src/formatters.js diff --git a/lib/Settings/register.d/ai-system-inventory.json b/lib/Settings/register.d/ai-system-inventory.json deleted file mode 100644 index a07555d8..00000000 --- a/lib/Settings/register.d/ai-system-inventory.json +++ /dev/null @@ -1,325 +0,0 @@ -{ - "components": { - "registers": { - "stackiq": { - "schemas": [ - "aiSystem" - ] - } - }, - "schemas": { - "aiSystem": { - "slug": "aiSystem", - "title": "AI system", - "x-schema-org": "schema:SoftwareApplication", - "description": "An AI agent, AI model or AI feature the organisation uses, with its EU AI Act classification.", - "version": "0.1.0", - "icon": "RobotOutline", - "required": [ - "name" - ], - "source": "internal", - "hardValidation": false, - "immutable": false, - "searchable": true, - "maxDepth": 0, - "properties": { - "name": { - "type": "string", - "title": "Name", - "description": "The name the organisation uses for this AI system.", - "facetable": false, - "order": 1, - "table": { - "default": true - } - }, - "description": { - "type": "string", - "format": "markdown", - "title": "Description", - "description": "What the AI system is and how it is used.", - "facetable": false, - "order": 2 - }, - "kind": { - "type": "string", - "enum": [ - "AI agent", - "AI model", - "AI feature" - ], - "x-enum-labels": { - "AI agent": "AI agent", - "AI model": "AI model", - "AI feature": "AI feature" - }, - "title": "Kind", - "description": "An AI agent acts on its own, an AI model is a trained model, an AI feature is part of an application.", - "facetable": true, - "order": 3, - "table": { - "default": true - } - }, - "module": { - "type": "object", - "$ref": "#/components/schemas/module", - "objectConfiguration": { - "handling": "related-object" - }, - "inversedBy": "aiSystems", - "title": "Application", - "description": "The application this AI system runs in or supports.", - "facetable": true, - "order": 4, - "table": { - "default": true - } - }, - "provider": { - "type": "object", - "$ref": "#/components/schemas/organization", - "objectConfiguration": { - "handling": "related-object" - }, - "title": "Supplier", - "description": "The organisation that supplies the AI system.", - "facetable": true, - "order": 5 - }, - "purpose": { - "type": "string", - "title": "Purpose", - "description": "What the AI system decides, recommends or produces.", - "facetable": false, - "order": 6 - }, - "aiActRiskCategory": { - "type": "string", - "enum": [ - "prohibited", - "high risk", - "limited risk", - "minimal risk", - "not yet assessed" - ], - "x-enum-labels": { - "prohibited": "Prohibited", - "high risk": "High risk", - "limited risk": "Limited risk", - "minimal risk": "Minimal risk", - "not yet assessed": "Not yet assessed" - }, - "default": "not yet assessed", - "title": "AI Act risk category", - "description": "The risk category under the EU AI Act, as the organisation classified it.", - "facetable": true, - "order": 7, - "table": { - "default": true - } - }, - "aiActRole": { - "type": "string", - "enum": [ - "provider", - "deployer" - ], - "x-enum-labels": { - "provider": "Provider", - "deployer": "Deployer" - }, - "title": "Role under the AI Act", - "description": "Whether the organisation provides the AI system or deploys it.", - "facetable": true, - "order": 8 - }, - "algorithmRegisterUrl": { - "type": "string", - "format": "uri", - "title": "Algorithm register entry", - "description": "The link to this system in the Dutch algorithm register.", - "facetable": false, - "order": 9 - }, - "assessedOn": { - "type": "string", - "format": "date", - "title": "Last assessed on", - "description": "The date the classification was last assessed.", - "facetable": false, - "order": 10 - }, - "friaDocumentRef": { - "type": "string", - "title": "Fundamental rights impact assessment", - "description": "A reference to the fundamental rights impact assessment (FRIA). A high-risk system without one is flagged.", - "facetable": false, - "order": 11 - }, - "status": { - "type": "string", - "enum": [ - "in development", - "in use", - "withdrawn" - ], - "x-enum-labels": { - "in development": "In development", - "in use": "In use", - "withdrawn": "Withdrawn" - }, - "default": "in use", - "title": "Status", - "description": "Where the AI system stands in its lifecycle.", - "facetable": true, - "order": 12, - "table": { - "default": true - } - } - }, - "configuration": { - "objectNameField": "name", - "objectDescriptionField": "purpose", - "allowFiles": true, - "allowedTags": [ - "FRIA", - "Technical documentation", - "Human oversight", - "Logging" - ], - "autoPublish": false, - "x-openregister-lifecycle": { - "field": "status", - "initial": "in development", - "final": [ - "withdrawn" - ], - "transitions": { - "release": { - "from": [ - "in development" - ], - "to": "in use", - "description": "Take the AI system into use." - }, - "withdraw": { - "from": [ - "in development", - "in use" - ], - "to": "withdrawn", - "description": "Withdraw the AI system." - } - } - } - }, - "authorization": { - "create": [ - "software-catalog-admins", - "organisatie-beheerder", - "organisaties-beheerder", - "functioneel-beheerder", - "gebruik-beheerder", - "aanbod-beheerder" - ], - "read": [ - "software-catalog-admins", - { - "group": "organisatie-beheerder", - "match": { - "_organisation": "$organisation" - } - }, - { - "group": "organisaties-beheerder", - "match": { - "_organisation": "$organisation" - } - }, - { - "group": "functioneel-beheerder", - "match": { - "_organisation": "$organisation" - } - }, - { - "group": "gebruik-beheerder", - "match": { - "_organisation": "$organisation" - } - }, - { - "group": "aanbod-beheerder", - "match": { - "_organisation": "$organisation" - } - }, - { - "group": "aanbod-beheerder", - "match": { - "provider": "$organisation" - } - } - ], - "update": [ - "software-catalog-admins", - { - "group": "organisatie-beheerder", - "match": { - "_organisation": "$organisation" - } - }, - { - "group": "organisaties-beheerder", - "match": { - "_organisation": "$organisation" - } - }, - { - "group": "functioneel-beheerder", - "match": { - "_organisation": "$organisation" - } - }, - { - "group": "gebruik-beheerder", - "match": { - "_organisation": "$organisation" - } - } - ], - "delete": [ - "software-catalog-admins", - { - "group": "organisatie-beheerder", - "match": { - "_organisation": "$organisation" - } - }, - { - "group": "organisaties-beheerder", - "match": { - "_organisation": "$organisation" - } - }, - { - "group": "functioneel-beheerder", - "match": { - "_organisation": "$organisation" - } - }, - { - "group": "gebruik-beheerder", - "match": { - "_organisation": "$organisation" - } - } - ] - } - } - } - } -} diff --git a/lib/Settings/softwarecatalogus_register.json b/lib/Settings/softwarecatalogus_register.json index 907a326a..427bfb69 100644 --- a/lib/Settings/softwarecatalogus_register.json +++ b/lib/Settings/softwarecatalogus_register.json @@ -3,8 +3,8 @@ "info": { "title": "Software Catalog Register", "description": "Register containing AMEF and Voorzieningen schemas for the VNG Software Catalog application. This configuration includes schemas for applications, services, organizations, and compliance tracking.", - "version": "2.5.2", - "changelog": "2.5.2: the connection schema (0.3.3) asked its national provision picker for gemmaType Buitengemeentenlijke voorziening, a spelling the GEMMA model does not use (it says Buitengemeentelijke voorziening), so the picker found nothing; its name template named the keys gegevensuitwisselingRichting and buitengemeentelijkVoorziening and the values AnaarB, BnaarA and bi-directioneel, all renamed since, so a connection had no readable name; and type, status and dataExchangeDirection become facetable for the new Connections page (connections-catalogue-pages). 2.5.1: the x-openregister-lifecycle blocks of usage, catalogContract, connection and moduleVersion still named the Dutch states (Verwerving/Gepland/In productie/Uit te faseren/Uitgefaseerd, In onderhandeling/Actief/Verlopen, in ontwikkeling/in gebruik/einde ondersteuning/teruggetrokken) while their status enums and the rows RenameDutchCatalogValues migrated are English, so no transition was ever offered on those records (stackiq#1140). The states now use the enum values, and the four schema versions are bumped (usage 1.5.1, catalogContract 0.1.2, connection 0.3.2, moduleVersion 0.1.5) because a lifecycle-only edit does not deploy without one, as 2.4.4 records. 2.4.4: organization.status was left behind by #520's enum translation — its `default` was still 'Concept' and its whole x-openregister-lifecycle block still named Concept/Actief/Deactief, while the enum and the migrated rows are Draft/Active/Inactive/merged. A default outside its own enum makes every newly created organisation fall out of the Organisations index filter, and a lifecycle whose from/to values match no row offers no transition at all — neither raises an error. The schema version is bumped with it because a deployed version >= the declared one makes the import SKIP, and OpenRegister's schemaContentDiffers() escape hatch compares only properties/required/authorization — never `configuration` — so a lifecycle-only edit would never have deployed. 2.4.3: Re-authored Dutch schema-level titles to English (dienst, kwetsbaarheid, contactpersoon, organisatie, gebruik, koppeling, beoordeeling, module, bioMaatregel, moduleVersie, sbomComponent); schema keys unchanged, Dutch labels now come from the app's l10n translation files. 2.4.2: Moved SBOM provenance properties (sbomLastImportedAt, sbomFormat, sbomFileName, sbomComponents) from the organisatie schema to moduleVersie, where SBOM imports actually record them; without this the moduleVersie magic table lacked the columns so recordProvenance() writes were silently dropped and the import-status endpoint always reported 'never imported'. 2.4.1: Re-authored Dutch schema property titles to English (property keys unchanged); Dutch labels now come from the app's l10n translation files." + "version": "2.5.3", + "changelog": "2.5.3: the aiSystem schema (0.1.0) joins the stackiq register, for the AI systems an organisation uses and their EU AI Act classification (landscape-ai-system-inventory). 2.5.2: the connection schema (0.3.3) asked its national provision picker for gemmaType Buitengemeentenlijke voorziening, a spelling the GEMMA model does not use (it says Buitengemeentelijke voorziening), so the picker found nothing; its name template named the keys gegevensuitwisselingRichting and buitengemeentelijkVoorziening and the values AnaarB, BnaarA and bi-directioneel, all renamed since, so a connection had no readable name; and type, status and dataExchangeDirection become facetable for the new Connections page (connections-catalogue-pages). 2.5.1: the x-openregister-lifecycle blocks of usage, catalogContract, connection and moduleVersion still named the Dutch states (Verwerving/Gepland/In productie/Uit te faseren/Uitgefaseerd, In onderhandeling/Actief/Verlopen, in ontwikkeling/in gebruik/einde ondersteuning/teruggetrokken) while their status enums and the rows RenameDutchCatalogValues migrated are English, so no transition was ever offered on those records (stackiq#1140). The states now use the enum values, and the four schema versions are bumped (usage 1.5.1, catalogContract 0.1.2, connection 0.3.2, moduleVersion 0.1.5) because a lifecycle-only edit does not deploy without one, as 2.4.4 records. 2.4.4: organization.status was left behind by #520's enum translation — its `default` was still 'Concept' and its whole x-openregister-lifecycle block still named Concept/Actief/Deactief, while the enum and the migrated rows are Draft/Active/Inactive/merged. A default outside its own enum makes every newly created organisation fall out of the Organisations index filter, and a lifecycle whose from/to values match no row offers no transition at all — neither raises an error. The schema version is bumped with it because a deployed version >= the declared one makes the import SKIP, and OpenRegister's schemaContentDiffers() escape hatch compares only properties/required/authorization — never `configuration` — so a lifecycle-only edit would never have deployed. 2.4.3: Re-authored Dutch schema-level titles to English (dienst, kwetsbaarheid, contactpersoon, organisatie, gebruik, koppeling, beoordeeling, module, bioMaatregel, moduleVersie, sbomComponent); schema keys unchanged, Dutch labels now come from the app's l10n translation files. 2.4.2: Moved SBOM provenance properties (sbomLastImportedAt, sbomFormat, sbomFileName, sbomComponents) from the organisatie schema to moduleVersie, where SBOM imports actually record them; without this the moduleVersie magic table lacked the columns so recordProvenance() writes were silently dropped and the import-status endpoint always reported 'never imported'. 2.4.1: Re-authored Dutch schema property titles to English (property keys unchanged); Dutch labels now come from the app's l10n translation files." }, "x-openregister": { "type": "application", @@ -835,7 +835,8 @@ "compliancy", "moduleVersion", "sbomComponent", - "bioMeasure" + "bioMeasure", + "aiSystem" ], "source": "internal", "tablePrefix": "", @@ -8054,6 +8055,319 @@ "objectDescriptionField": "purl", "autoPublish": true } + }, + "aiSystem": { + "uri": null, + "slug": "aiSystem", + "title": "AI system", + "x-schema-org": "schema:SoftwareApplication", + "description": "An AI agent, AI model or AI feature the organisation uses, with its EU AI Act classification.", + "version": "0.1.0", + "icon": "RobotOutline", + "required": [ + "name" + ], + "source": "internal", + "hardValidation": false, + "immutable": false, + "searchable": true, + "maxDepth": 0, + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name the organisation uses for this AI system.", + "facetable": false, + "order": 1, + "table": { + "default": true + } + }, + "description": { + "type": "string", + "format": "markdown", + "title": "Description", + "description": "What the AI system is and how it is used.", + "facetable": false, + "order": 2 + }, + "kind": { + "type": "string", + "enum": [ + "AI agent", + "AI model", + "AI feature" + ], + "x-enum-labels": { + "AI agent": "AI agent", + "AI model": "AI model", + "AI feature": "AI feature" + }, + "title": "Kind", + "description": "An AI agent acts on its own, an AI model is a trained model, an AI feature is part of an application.", + "facetable": true, + "order": 3, + "table": { + "default": true + } + }, + "module": { + "type": "object", + "$ref": "#/components/schemas/module", + "objectConfiguration": { + "handling": "related-object" + }, + "inversedBy": "aiSystems", + "title": "Application", + "description": "The application this AI system runs in or supports.", + "facetable": true, + "order": 4, + "table": { + "default": true + } + }, + "provider": { + "type": "object", + "$ref": "#/components/schemas/organization", + "objectConfiguration": { + "handling": "related-object" + }, + "title": "Supplier", + "description": "The organisation that supplies the AI system.", + "facetable": true, + "order": 5 + }, + "purpose": { + "type": "string", + "title": "Purpose", + "description": "What the AI system decides, recommends or produces.", + "facetable": false, + "order": 6 + }, + "aiActRiskCategory": { + "type": "string", + "enum": [ + "prohibited", + "high risk", + "limited risk", + "minimal risk", + "not yet assessed" + ], + "x-enum-labels": { + "prohibited": "Prohibited", + "high risk": "High risk", + "limited risk": "Limited risk", + "minimal risk": "Minimal risk", + "not yet assessed": "Not yet assessed" + }, + "default": "not yet assessed", + "title": "AI Act risk category", + "description": "The risk category under the EU AI Act, as the organisation classified it.", + "facetable": true, + "order": 7, + "table": { + "default": true + } + }, + "aiActRole": { + "type": "string", + "enum": [ + "provider", + "deployer" + ], + "x-enum-labels": { + "provider": "Provider", + "deployer": "Deployer" + }, + "title": "Role under the AI Act", + "description": "Whether the organisation provides the AI system or deploys it.", + "facetable": true, + "order": 8 + }, + "algorithmRegisterUrl": { + "type": "string", + "format": "uri", + "title": "Algorithm register entry", + "description": "The link to this system in the Dutch algorithm register.", + "facetable": false, + "order": 9 + }, + "assessedOn": { + "type": "string", + "format": "date", + "title": "Last assessed on", + "description": "The date the classification was last assessed.", + "facetable": false, + "order": 10 + }, + "friaDocumentRef": { + "type": "string", + "title": "Fundamental rights impact assessment", + "description": "A reference to the fundamental rights impact assessment (FRIA). A high-risk system without one is flagged.", + "facetable": false, + "order": 11 + }, + "status": { + "type": "string", + "enum": [ + "in development", + "in use", + "withdrawn" + ], + "x-enum-labels": { + "in development": "In development", + "in use": "In use", + "withdrawn": "Withdrawn" + }, + "default": "in use", + "title": "Status", + "description": "Where the AI system stands in its lifecycle.", + "facetable": true, + "order": 12, + "table": { + "default": true + } + } + }, + "configuration": { + "objectNameField": "name", + "objectDescriptionField": "purpose", + "allowFiles": true, + "allowedTags": [ + "FRIA", + "Technical documentation", + "Human oversight", + "Logging" + ], + "autoPublish": false, + "x-openregister-lifecycle": { + "field": "status", + "initial": "in development", + "final": [ + "withdrawn" + ], + "transitions": { + "release": { + "from": [ + "in development" + ], + "to": "in use", + "description": "Take the AI system into use." + }, + "withdraw": { + "from": [ + "in development", + "in use" + ], + "to": "withdrawn", + "description": "Withdraw the AI system." + } + } + } + }, + "authorization": { + "create": [ + "software-catalog-admins", + "organisatie-beheerder", + "organisaties-beheerder", + "functioneel-beheerder", + "gebruik-beheerder", + "aanbod-beheerder" + ], + "read": [ + "software-catalog-admins", + { + "group": "organisatie-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "organisaties-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "functioneel-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "gebruik-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "aanbod-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "aanbod-beheerder", + "match": { + "provider": "$organisation" + } + } + ], + "update": [ + "software-catalog-admins", + { + "group": "organisatie-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "organisaties-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "functioneel-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "gebruik-beheerder", + "match": { + "_organisation": "$organisation" + } + } + ], + "delete": [ + "software-catalog-admins", + { + "group": "organisatie-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "organisaties-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "functioneel-beheerder", + "match": { + "_organisation": "$organisation" + } + }, + { + "group": "gebruik-beheerder", + "match": { + "_organisation": "$organisation" + } + } + ] + } } }, "objects": [ diff --git a/lib/Settings/stackiq_mock_register.json b/lib/Settings/stackiq_mock_register.json index 2833ddc7..99a11cbf 100644 --- a/lib/Settings/stackiq_mock_register.json +++ b/lib/Settings/stackiq_mock_register.json @@ -7345,6 +7345,7 @@ } }, "aiSystem": { + "uri": null, "slug": "aiSystem", "title": "AI system", "x-schema-org": "schema:SoftwareApplication", @@ -10865,6 +10866,21 @@ "aiActRole": "deployer", "assessedOn": "2026-05-15", "status": "in development" + }, + { + "@self": { + "register": "stackiq", + "schema": "aiSystem", + "slug": "ai-system-mail-sorting-agent" + }, + "name": "Mail sorting agent", + "kind": "AI agent", + "module": {}, + "provider": {}, + "purpose": "Sorts incoming mail to the right team.", + "aiActRiskCategory": "minimal risk", + "aiActRole": "deployer", + "status": "in use" } ] } diff --git a/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/design.md b/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/design.md index 3aff7e28..de5f8e44 100644 --- a/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/design.md +++ b/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/design.md @@ -4,11 +4,11 @@ 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`). +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. As built, the schema is in the monolith, not in a fragment: in this repo a `register.d` fragment only overlays a schema the monolith declares (`tests/Unit/Service/ReviewModerationOverlayReachesTheSchemaTest.php`), and a page may only read a schema its register attaches in the monolith (`tests/Unit/AppInfo/ManifestRegisterSentinelTest.php`). The register goes to 2.5.3. ## D1. The aiSystem schema -`lib/Settings/register.d/ai-system-inventory.json`, schema.org type `SoftwareApplication` with `applicationCategory` AI: +`lib/Settings/softwarecatalogus_register.json` (as built; see below), schema.org type `SoftwareApplication` with `applicationCategory` AI: | property | type | notes | |---|---|---| diff --git a/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/tasks.md b/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/tasks.md index e32730ed..d9dc9f77 100644 --- a/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/tasks.md +++ b/openspec/changes/archive/2026-09-29-landscape-ai-system-inventory/tasks.md @@ -4,7 +4,7 @@ ### 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` +- **files**: `lib/Settings/softwarecatalogus_register.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 - [x] Implement @@ -45,6 +45,8 @@ ## As built (2026-09-29) - The schema, pages, checklist rule and seeds are tested in `tests/Unit/Settings/AiSystemFragmentTest.php` and `tests/vitest/aiSystems.spec.js` (the vitest file also covers what `aiActChecklist.spec.js` was to hold). +- The aiSystem schema lives in `lib/Settings/softwarecatalogus_register.json` (register 2.5.3), not in a fragment: the repo's own tests allow fragments only to overlay existing schemas. +- The app cell formatters live in `src/formatters.js`; `tests/vitest/connectionRegistry.spec.js` now asserts none shadows a library built-in, instead of asserting App.vue passes none. - The High risk without FRIA filter sends `friaDocumentRef=IS NULL`, which OpenRegister's property filter reads as a null check. The warning column uses an app cell formatter `friaStatus` (src/utils/aiAct.js), passed to CnAppRoot as `formatters`. - `tests/e2e/workflows/ai-systems.spec.ts` seeds the AI systems through the objects API, the call the Add form makes, rather than typing into the form. It lists but was not run: no local instance has a seeded stackiq register. The docs screenshot waits for that instance; `docs/images/ai-systems.png` is not added. -- Seeds: a chat assistant (AI feature, limited risk) and a scoring model (AI model, high risk, no FRIA). Like every other demo object they carry no relation, so the chat assistant is not linked to a demo application. +- Seeds: a chat assistant (AI feature, limited risk), a scoring model (AI model, high risk, no FRIA) and a mail sorting agent (AI agent, minimal risk); gate 101 asks three per schema. Like every other demo object they carry no relation, so the chat assistant is not linked to a demo application. diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index e6634f94..7abc3c71 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -5286,7 +5286,7 @@ "stackiq": "partial", "built": { "state": "built", - "evidence": "lib/Settings/register.d/ai-system-inventory.json schema aiSystem (kind AI agent/AI model/AI feature, module, provider, purpose, status with lifecycle) in the stackiq register; src/manifest.d/ai-systems.json pages AiSystems (/ai-systems) and AiSystemDetail; ModuleDetail widget md-ai-systems; tests/Unit/Settings/AiSystemFragmentTest.php, tests/vitest/aiSystems.spec.js", + "evidence": "lib/Settings/softwarecatalogus_register.json schema aiSystem (kind AI agent/AI model/AI feature, module, provider, purpose, status with lifecycle) in the stackiq register; src/manifest.d/ai-systems.json pages AiSystems (/ai-systems) and AiSystemDetail; ModuleDetail widget md-ai-systems; tests/Unit/Settings/AiSystemFragmentTest.php, tests/vitest/aiSystems.spec.js", "owner": "ConductionNL/stackiq", "change": "2026-09-29-landscape-ai-system-inventory" }, @@ -5301,7 +5301,7 @@ "bluedolphin": "not checked: row added in wave 5 (2026-09-26), no reading of this system for it yet", "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)", - "stackiq": "lib/Settings/register.d/ai-system-inventory.json schema aiSystem (kind AI agent/AI model/AI feature, module, provider, purpose, status with lifecycle) in the stackiq register; src/manifest.d/ai-systems.json pages AiSystems (/ai-systems) and AiSystemDetail; ModuleDetail widget md-ai-systems; tests/Unit/Settings/AiSystemFragmentTest.php, tests/vitest/aiSystems.spec.js" + "stackiq": "lib/Settings/softwarecatalogus_register.json schema aiSystem (kind AI agent/AI model/AI feature, module, provider, purpose, status with lifecycle) in the stackiq register; src/manifest.d/ai-systems.json pages AiSystems (/ai-systems) and AiSystemDetail; ModuleDetail widget md-ai-systems; tests/Unit/Settings/AiSystemFragmentTest.php, tests/vitest/aiSystems.spec.js" }, "note": "Mined from sap-leanix (changelog) on 2026-09-26. Specified in openspec/changes/landscape-ai-system-inventory (OpenSpec pass 2026-09-27). AI systems are registered and linked to the application they run in; linking them to processes waits for architecture-process-mapping (built by openspec/changes/archive/2026-09-29-landscape-ai-system-inventory).", "vng-softwarecatalogus": "unknown", diff --git a/src/App.vue b/src/App.vue index 38d264a5..89ee4fb9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -76,7 +76,7 @@ import OrganisationSwitcher from './components/organisations/OrganisationSwitche import Dialogs from './dialogs/Dialogs.vue' import Modals from './modals/Modals.vue' import { setActiveOrganisationUuid } from './composables/orClient.js' -import { friaStatus } from './utils/aiAct.js' +import appFormatters from './formatters.js' import { settingsStore } from './store/store.js' export default { @@ -151,7 +151,7 @@ export default { data() { return { // App cell formatters for manifest columns (`columns[].formatter`). - formatters: { friaStatus }, + formatters: appFormatters, objectSidebarState: reactive({ active: false, open: true, diff --git a/src/components/ai/AiActChecklist.vue b/src/components/ai/AiActChecklist.vue index fad10e43..45e46e46 100644 --- a/src/components/ai/AiActChecklist.vue +++ b/src/components/ai/AiActChecklist.vue @@ -24,7 +24,12 @@ v-if="missingFria" type="warning" data-testid="ai-act-fria-warning"> - {{ t('stackiq', 'This is a high-risk AI system without a fundamental rights impact assessment.') }} + {{ + t( + 'stackiq', + 'This is a high-risk AI system without a fundamental rights impact assessment.', + ) + }}
  • - {{ tagLabel(item.tag) }} + :class=" + item.present + ? 'ai-act-checklist__icon--ok' + : 'ai-act-checklist__icon--missing' + " /> + {{ + tagLabel(item.tag) + }} - {{ item.present ? item.files.join(', ') : t('stackiq', 'Missing') }} + {{ + item.present + ? item.files.join(', ') + : t('stackiq', 'Missing') + }}

- {{ t('stackiq', 'Attach a document under Documents and give it the matching tag.') }} + {{ + t( + 'stackiq', + 'Attach a document under Documents and give it the matching tag.', + ) + }}

@@ -157,13 +177,23 @@ export default { id: String(this.objectId), } const [object, files] = await Promise.all([ - axios.get(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}', params)), - axios.get(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}/files', params)), + axios.get( + generateUrl( + '/apps/openregister/api/objects/{register}/{schema}/{id}', + params, + ), + ), + axios.get( + generateUrl( + '/apps/openregister/api/objects/{register}/{schema}/{id}/files', + params, + ), + ), ]) this.system = object.data?.object || object.data || {} this.files = files.data?.results ?? [] } - } catch (e) { + } catch { this.error = true } finally { this.loading = false diff --git a/src/customComponents.js b/src/customComponents.js index beda735c..3d75c2da 100644 --- a/src/customComponents.js +++ b/src/customComponents.js @@ -18,8 +18,8 @@ // - @conduction/nextcloud-vue → docs/migrating-to-manifest.md import { generateUrl } from '@nextcloud/router' -import OrganisatieCard from './components/cards/OrganisatieCard.vue' import AiActChecklist from './components/ai/AiActChecklist.vue' +import OrganisatieCard from './components/cards/OrganisatieCard.vue' import ApplicationContractsPanel from './components/contracts/ApplicationContractsPanel.vue' import ContractApprovalPanel from './components/contracts/ContractApprovalPanel.vue' import ContractSeatsPanel from './components/contracts/ContractSeatsPanel.vue' diff --git a/src/formatters.js b/src/formatters.js new file mode 100644 index 00000000..66fcd17e --- /dev/null +++ b/src/formatters.js @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: EUPL-1.2 +// Copyright (C) 2026 Conduction B.V. +// +// App cell formatters, handed to CnAppRoot as `formatters` and resolved by +// name from a manifest column's `formatter`. CnAppRoot spreads these OVER the +// library built-ins, so a name here must never equal a built-in's name: +// tests/vitest/connectionRegistry.spec.js holds that. + +import { friaStatus } from './utils/aiAct.js' + +export default { + // The FRIA column of the AI systems list (landscape-ai-system-inventory). + friaStatus, +} diff --git a/src/manifest.d/ai-systems.json b/src/manifest.d/ai-systems.json index 63cb9fa5..3f8e3b61 100644 --- a/src/manifest.d/ai-systems.json +++ b/src/manifest.d/ai-systems.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json", - "_note": "landscape-ai-system-inventory: the aiSystem schema from lib/Settings/register.d/ai-system-inventory.json. A menu child of Applications (ADR-097). The High risk without FRIA quick filter sends friaDocumentRef=IS NULL, which OpenRegister's property filter reads as a null check; the FRIA column uses the app formatter friaStatus (src/utils/aiAct.js), so the list warns on the same rule.", + "_note": "landscape-ai-system-inventory: the aiSystem schema in lib/Settings/softwarecatalogus_register.json. A menu child of Applications (ADR-097). The High risk without FRIA quick filter sends friaDocumentRef=IS NULL, which OpenRegister's property filter reads as a null check; the FRIA column uses the app formatter friaStatus (src/utils/aiAct.js), so the list warns on the same rule.", "menu": [ { "id": "Modules", diff --git a/tests/Unit/Settings/AiSystemFragmentTest.php b/tests/Unit/Settings/AiSystemFragmentTest.php index 95cf8b6e..3c5844c4 100644 --- a/tests/Unit/Settings/AiSystemFragmentTest.php +++ b/tests/Unit/Settings/AiSystemFragmentTest.php @@ -1,8 +1,8 @@ merged.pages.find((p) => p.id === id) -const schema = fragment.components.schemas.aiSystem +const schema = register.components.schemas.aiSystem /** * A validator built from the real aiSystem properties. A relation is checked @@ -107,12 +107,12 @@ describe('the evidence checklist', () => { ]) expect(list.map((r) => r.tag)).toEqual(EVIDENCE_TAGS) expect(list.find((r) => r.tag === 'FRIA').present).toBe(false) - expect( - list.find((r) => r.tag === 'Technical documentation').present, - ).toBe(true) - expect(list.find((r) => r.tag === 'Technical documentation').files).toEqual( - ['tech.pdf'], + expect(list.find((r) => r.tag === 'Technical documentation').present).toBe( + true, ) + expect(list.find((r) => r.tag === 'Technical documentation').files).toEqual([ + 'tech.pdf', + ]) }) it('uses the same four tags the schema allows on files', () => { @@ -183,9 +183,9 @@ describe('the AI systems pages', () => { (w) => w.type === 'integration' && w.integrationId === 'files', ), ).toBe(true) - expect( - detail.config.bodyWidgets.map((w) => w.component), - ).toContain('AiActChecklist') + expect(detail.config.bodyWidgets.map((w) => w.component)).toContain( + 'AiActChecklist', + ) }) it('lists the AI systems on the application page', () => { @@ -219,9 +219,7 @@ describe('the seeded AI systems', () => { it('include one high-risk system without a FRIA, so the warning shows', () => { expect(seeded.filter((o) => friaMissing(o))).toHaveLength(1) - expect(seeded.some((o) => o.aiActRiskCategory === 'limited risk')).toBe( - true, - ) + expect(seeded.some((o) => o.aiActRiskCategory === 'limited risk')).toBe(true) }) it('carry the schema copy the demo import validates against', () => { diff --git a/tests/vitest/connectionRegistry.spec.js b/tests/vitest/connectionRegistry.spec.js index 3e25f0e3..38f61a90 100644 --- a/tests/vitest/connectionRegistry.spec.js +++ b/tests/vitest/connectionRegistry.spec.js @@ -26,6 +26,7 @@ import { BUILT_IN_FORMATTERS } from '@conduction/nextcloud-vue/src/utils/builtIn import * as fs from 'fs' import * as path from 'path' import { describe, expect, it } from 'vitest' +import appFormatters from '../../src/formatters.js' import { createConnectionHandlers, INTEGRIQ_CONNECTIONS_PATH, @@ -40,7 +41,8 @@ const menu = fragment.menu.find((m) => m.id === 'IntegrationsMenu') /** * The formatter registry CnAppRoot provides, built the way CnAppRoot builds it: * the library's built-ins under whatever the app passes in its `formatters` - * prop. A same-named local formatter wins, which is why stackiq passes none. + * prop. A same-named local formatter wins, which is why stackiq's own names + * (src/formatters.js) never equal a built-in's. * * @param {object} appFormatters What the app hands CnAppRoot. Empty by default. * @return {object} The merged registry, keyed by formatter name. @@ -156,13 +158,19 @@ describe('the Integrations page declaration', () => { // so a local formatter under either name silently replaces the built-in and // nothing logs. stackiq passes no formatters at all, and this states what // that buys: the built-in is what the Status column resolves. - it('passes CnAppRoot no formatters, so nothing shadows the built-ins', () => { + it('passes CnAppRoot no formatter that shadows a built-in', () => { const shadow = shellFormatterRegistry({ connectionStatus: () => 'a local copy answered', }) expect(shadow.connectionStatus('disabled')).toBe('a local copy answered') - expect(read('src', 'App.vue')).not.toContain(':formatters=') + const shadowed = Object.keys(appFormatters).filter( + (name) => name in BUILT_IN_FORMATTERS, + ) + expect(shadowed).toEqual([]) + expect(shellFormatterRegistry(appFormatters).connectionStatus).toBe( + BUILT_IN_FORMATTERS.connectionStatus, + ) }) it('names an icon src/icons.js registers', () => { From 97500a20fc4f1c17e58653a902b2c20fd7dafbca Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 29 Sep 2026 20:50:07 +0200 Subject: [PATCH 5/5] fix(ai-systems): seed the demo AI systems for both registers, as gate 101 counts them --- lib/Settings/stackiq_mock_register.json | 48 +++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/lib/Settings/stackiq_mock_register.json b/lib/Settings/stackiq_mock_register.json index 99a11cbf..3d2ab16c 100644 --- a/lib/Settings/stackiq_mock_register.json +++ b/lib/Settings/stackiq_mock_register.json @@ -10881,6 +10881,54 @@ "aiActRiskCategory": "minimal risk", "aiActRole": "deployer", "status": "in use" + }, + { + "@self": { + "register": "vng-gemma", + "schema": "aiSystem", + "slug": "ai-system-chat-assistant-gemma" + }, + "name": "Chat assistant", + "kind": "AI feature", + "module": {}, + "provider": {}, + "purpose": "Answers residents' questions about permits on the website and hands over to an employee.", + "aiActRiskCategory": "limited risk", + "aiActRole": "deployer", + "assessedOn": "2026-06-01", + "algorithmRegisterUrl": "https://algoritmes.overheid.nl/", + "status": "in use" + }, + { + "@self": { + "register": "vng-gemma", + "schema": "aiSystem", + "slug": "ai-system-benefit-scoring-model-gemma" + }, + "name": "Benefit application scoring model", + "kind": "AI model", + "module": {}, + "provider": {}, + "purpose": "Ranks benefit applications for a manual check.", + "aiActRiskCategory": "high risk", + "aiActRole": "deployer", + "assessedOn": "2026-05-15", + "status": "in development" + }, + { + "@self": { + "register": "vng-gemma", + "schema": "aiSystem", + "slug": "ai-system-mail-sorting-agent-gemma" + }, + "name": "Mail sorting agent", + "kind": "AI agent", + "module": {}, + "provider": {}, + "purpose": "Sorts incoming mail to the right team.", + "aiActRiskCategory": "minimal risk", + "aiActRole": "deployer", + "status": "in use" } ] }