feat(ms-ai-architect): v1.15.0 — playground v3 project-view integration

Erstatter v2 project-surface (screen-tabs + category-tabs + per-command paste-cards)
med v3 renderProjectView (sidebar med 17 artifacts + main-area + import-modal overlay).
renderActive() ruter project-surface til renderProjectSurfaceV3() som wrapper
renderProjectView + topbar + app-shell.

V2-surface helt fjernet:
- renderProjectSurface (152 linjer)
- renderCommandSubCard (87 linjer)
- rehydratePasteImports (15 linjer)
- ACTIONS['project-screen'], currentProjectScreen
- 5 v2-CSS-klasser: .project-tabs, .project-tab*, .sub-zone, .paste-import-row, .project-header__*, .command-cards

Zombie-handlers beholdt for test-back-compat:
currentProjectTab, ACTIONS['project-tab'], ACTIONS['parse'],
handlePasteImport, window.__handlePasteImport. Unreachable fra v3 DOM
men nødvendige for test-playground-v3.sh + test-playground-parsers.sh.

2 fingerprint-gap lukket:
- requirements.headers: utvidet med "EU AI Act — Krav" pattern
- license.headers: utvidet med "Lisens-kapabilitetsmatrise" pattern
- KNOWN_GAP_FIXTURES = {} i test-playground-fingerprints.sh

migrateDataVersion utvidet med parserFor (3. arg):
- Demo-state med kun raw_markdown auto-parses til project.artifacts[cid]
- defaultParserFor(cmdId) resolverer PARSERS[archetypeFor(cmdId)]
- 3 bootstrap-callsites oppdatert (cold-load, import, load-demo)

Ship-QA bugfixes funnet via browser-dogfood:
- components-tier4-project-view.css lagt til i <link>-kjeden (var ikke loaded
  -> modal-overlay og two-column layout virket ikke)
- renderImportModal setter data-open="true" (DS-kontrakt for display: flex)

Bundler også sesjon 2-4 deliverables som ikke ble committed tidligere:
- shared/playground-design-system v0.6.0 (Tier 4 project-view CSS + 6 tokens)
- ms-ai-architect/playground/vendor/ re-sync til DS v0.6.0
- tests/test-playground-fingerprints.sh (sesjon 4 NY - 32 PASS)
- tests/test-playground-projectview.sh (sesjon 4 NY - 30 PASS)
- tests/test-playground-actions.sh (sesjon 4 NY - 19 PASS)
- tests/test-playground-migrations.sh utvidet (7 -> 16 PASS)
- tests/run-e2e.sh wirer alle 6 playground-suiter

Stats:
- bash tests/run-e2e.sh --playground: 386 PASS, 0 FAIL, 2 WARN (pre-eks)
- bash tests/run-e2e.sh (full): All E2E suites passed
- bash tests/validate-plugin.sh: 219 PASS

Screenshots regenerert til playground/screenshots/v1.15.0/ (24 PNG-er, 12
surfaces x 2 tema). Nye v3-surfaces: project-overview, project-artifact-*,
project-import-modal (viewport-only), project-search.

Docs oppdatert (3 nivåer): README.md (badge + version history),
CHANGELOG.md, CLAUDE.md (playground-seksjon + valideringstabell),
rot-README.md + rot-CLAUDE.md (marketplace-landingen + plugin-index).

.gitignore: ny pattern *.local.html + *.local.json for sesjon-state-filer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-16 20:58:51 +02:00
commit d8882f5220
47 changed files with 3722 additions and 409 deletions

View file

@ -11,12 +11,12 @@ plugins/
graceful-handoff/ v2.1.0 — Auto-trigger handoff via Stop hook (skill + JSON pipeline + 4-step model-aware context resolution)
linkedin-thought-leadership/ v1.2.0 — LinkedIn content pipeline + analytics
llm-security/ v6.0.0 — Security scanning, auditing, threat modeling
ms-ai-architect/ v1.13.1 — Microsoft AI architecture (Cosmo Skyberg persona) + manual KB-refresh slash command
ms-ai-architect/ v1.15.0 — Microsoft AI architecture (Cosmo Skyberg persona) + manual KB-refresh slash command + v3 project-view (sidebar med 17 artifacts + main + import-modal overlay, v2-surface fjernet i v1.15.0)
okr/ v1.0.0 — OKR guidance for Norwegian public sector
voyage/ v5.0.3 — Brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline (six-command universal pipeline + multi-session resumption + --gates autonomy chain). /trekbrief, /trekplan, and /trekreview each end by running scripts/annotate.mjs against the just-written .md and printing the file:// link to a self-contained operator-annotation HTML modelled on claude-code-100x/build-site.js: pencil-toggle annotation mode, select text or click any element, choose intent (Fiks/Endre/Spørsmål), comment, sidebar groups by section with delete + Copy Prompt, localStorage persistence per artifact path. v5.0.0 removed the v4.2/v4.3 bespoke playground + /trekrevise + Handover 8; v5.0.1 pointed at /playground document-critique (wrong direction); v5.0.2 was operator-led but too thin; v5.0.3 matches the reference the operator pointed at from day one.
shared/
playground-design-system/ v0.1 — Aksel/Digdir-aligned CSS design system + JSON schemas + self-hosted Inter/JetBrains Mono/Source Serif 4 fonts (Tier 1+2+3 wave 1+wave 2 = 20 Tier 3 components total). Consumed by ms-ai-architect, okr, llm-security, voyage, config-audit
playground-design-system/ v0.6.0 — Aksel/Digdir-aligned CSS design system + JSON schemas + self-hosted Inter/JetBrains Mono/Source Serif 4 fonts. Tier 1 base + Tier 2 + Tier 3 wave 1+2 (20 components) + Tier 4 project-view-arketype (v0.6.0 — sidebar + main + import-modal overlay). Consumed by ms-ai-architect, okr, llm-security, voyage, config-audit.
playground-examples/ — Reference scenarios (ROS-Lier, OKR-Bærum, security-Direktorat) + showcase landing + 12 isolated Tier 3 wave 2 component demos under components/
```
@ -53,3 +53,20 @@ Disse trackes IKKE i git. Oppdater ved sesjonsslutt.
3. Les REMEMBER.md og TODO.md for sesjonsstatus
4. Jobb innenfor scope
5. Oppdater REMEMBER.md ved avslutning
## Communication patterns
### Linking to local files
When pointing to local files in responses, always use markdown link syntax with a descriptive name:
- Use `[Human-friendly name](file:///absolute/path)` — never bare `file:///...` URLs or autolinks `<file://...>`.
- Always use absolute paths. Never `~/` or relative paths.
- For multiple files, render as a bullet list of named markdown links.
Why: bare `file://` URLs only render the first as clickable across multiple lines. Named markdown links make each entry independently clickable and look cleaner.
Example:
- [Brief](file:///Users/ktg/.../brief.html)
- [Research summary](file:///Users/ktg/.../research/summary.md)

View file

@ -172,7 +172,7 @@ Key command: `/graceful-handoff [topic-slug] [--no-commit] [--no-push] [--dry-ru
---
### [MS AI Architect — Azure AI and Microsoft Foundry](plugins/ms-ai-architect/) `v1.14.0` `🇳🇴 Norwegian`
### [MS AI Architect — Azure AI and Microsoft Foundry](plugins/ms-ai-architect/) `v1.15.0` `🇳🇴 Norwegian`
Microsoft AI solution architecture guidance for Norwegian public sector and enterprise.
@ -187,11 +187,11 @@ Key commands: `/architect`, `/architect:ros`, `/architect:security`, `/architect
12 specialized agents · 25 commands · 5 skills (387 reference docs) · 2 hooks · manual sitemap-driven KB refresh
**One-click demo (v1.14.0, 2026-05-08):** "Last inn demo-data"-knappen på onboarding bootstrapper en ferdig "Acme Kommune" med demo-prosjektet "Acme: Kunde-chatbot" og alle 17 rapport-typer pre-importert som `raw_markdown` (konsistente navn på tvers av alle fixtures). Visualisering rehydreres automatisk på project-surface mount. 24 retina-screenshots committed under `playground/screenshots/v1.14.0/` (12 surfaces × 2 tema), så forkere ser pluginen uten å kjøre noe. Standalone Playwright-runner under `tests/screenshot/` (egen `package.json`).
**One-click demo (v1.15.0, 2026-05-16):** "Last inn demo-data"-knappen på onboarding bootstrapper en ferdig "Acme Kommune" med demo-prosjektet "Acme: Kunde-chatbot" og alle 17 rapport-typer pre-importert. v2→v3 migrasjon auto-parser `raw_markdown` til `project.artifacts[cid]` så project-view viser aggregert verdict (BLOKKERT), key stats (17/17 artifacts), top-risks-liste, og navigerbart artifact-sidebar i én navigasjon. 24 retina-screenshots committed under `playground/screenshots/v1.15.0/` (12 surfaces × 2 tema), så forkere ser pluginen uten å kjøre noe. Standalone Playwright-runner under `tests/screenshot/` (egen `package.json`).
**Playground (v3, v1.14.0 — root-cause refaktor, 2026-05-08):** Multi-surface decision-builder + report viewer. The single-file HTML app lives at `playground/ms-ai-architect-playground.html` (~3870+ lines). v1.14.0 leverer DS-konvensjon-adopsjon på 14 renderere over 6 sesjoner: B-DS-1/2/3 fikset i shared/ DS v0.4.0 (kanban-card word-break, expansion title-block, matrix-bubble cursor); 3 risk-renderere til DS-summary-grid + ros-layout; 6 compliance/govern-renderere bytter `.report-meta`-wrapper mot DS-konvensjon; renderMigrate + renderPoc til expansion-list per fase; 5b-fixes i renderCost/renderCompare/renderUtredning. Lokal `<style>`-blokk: 191 → 122 effektive linjer (~36% reduksjon siden v1.13.1).
**Playground (v3, v1.15.0 — project-view integration, 2026-05-16):** Multi-surface decision-builder + report viewer. The single-file HTML app lives at `playground/ms-ai-architect-playground.html` (~3870+ lines). v1.15.0 erstatter v2 project-surface (screen-tabs + category-tabs + per-command paste-cards) med v3 `renderProjectView` (sidebar med 17 artifacts gruppert i 4 kategorier + main-area med per-artifact view eller overview + import-modal som DS-overlay). V2-surface helt fjernet (`renderProjectSurface`, `renderCommandSubCard`, `rehydratePasteImports`, 5 v2-CSS-klasser). 2 fingerprint-gap lukket (requirements + license headers). `migrateDataVersion` utvidet med `parserFor` slik at demo-state og persisted localStorage auto-parses. Ship-QA: `components-tier4-project-view.css` lagt til i `<link>`-kjeden (var ikke loaded → modal-overlay og two-column layout virket ikke). 386 E2E PASS, 0 FAIL, 2 WARN.
- **4 surfaces:** Onboarding (4 strukturerte / 14 fritekst, prefill alle command-skjemaer) → Home (project list + 3 entry tracks) → Catalog (24 commands grouped in 5 expansion categories with search) → Project (per-project tabs, command-form prefill, paste-back report import + visualization)
- **4 surfaces:** Onboarding (4 strukturerte / 14 fritekst, prefill alle command-skjemaer) → Home (project list + 3 entry tracks) → Catalog (24 commands grouped in 5 expansion categories with search) → **Project v3** (sidebar med 17 artifacts + søk + main-area med per-artifact view eller aggregate overview + import-modal overlay)
- **Persistence:** IndexedDB primary + localStorage fallback, schema-versioned (`STATE_KEY = 'ms-ai-architect-state-v1'`) with eager migrations pipeline. v1.10.0 adds idempotent `dataVersion v1→v2` migration that backfills `verdict` + `keyStats` on existing reports.
- **17 inline report renderers (felles grunnskjelett)** — all wrap output through `renderPageShell()` with eyebrow + h1 + optional verdict-pill + optional key-stats-grid + archetype body (pyramid, 5×5/6×5/7×5 matrix, radar, kanban, mat-ladder, scenario-cards, screen-tabs, residual-pair, top-risks, recommendation-card, suppressed-panel, critique-card, read-more, traffic-light).
- **Foundation helpers**`renderPageShell`, `renderVerdictPill`, `renderKeyStatsGrid`, `inferVerdict`, `inferKeyStats`, `KEY_STATS_CONFIG`.

View file

@ -1,6 +1,6 @@
{
"name": "ms-ai-architect",
"version": "1.14.0",
"version": "1.15.0",
"description": "Microsoft AI Solution Architect - structured architecture guidance for the full Microsoft AI stack",
"author": {
"name": "Kjell Tore Guttormsen"

View file

@ -1,4 +1,6 @@
*.local.md
*.local.html
*.local.json
.mcp.json
.DS_Store
.claude/

View file

@ -5,6 +5,67 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.15.0] - 2026-05-16
### Changed — playground v3 project-view integration
V2 project-surface (screen-tabs + category-tabs + per-command paste-cards) erstattet av v3 project-view (sidebar + main + import-modal). Sluttproduktet av 5 sesjoner: V2-PROJECT-VIEW-SPEC, JS-foundation, renderProjectView/renderProjectArtifact/renderArtifactNav, testing (97 nye assertions), og nå integration + ship.
#### Sesjon 5: integration + ship
- `renderActive()` ruter `project`-surface til ny `renderProjectSurfaceV3()` som wrapper renderProjectView + topbar + app-shell.
- `renderProjectSurface` (152 linjer), `renderCommandSubCard` (87 linjer), `rehydratePasteImports` (15 linjer) slettet.
- `currentProjectScreen` modul-var slettet; `currentProjectTab` beholdt som zombie for `ACTIONS['project-tab']`/`ACTIONS['parse']`/`handlePasteImport` (test-back-compat).
- `ACTIONS['project-screen']` slettet.
- 5 v2-CSS-klasser slettet: `.project-tabs`, `.project-tab`, `.project-tab__count`, `.sub-zone`, `.paste-import-row`, `.project-header__*`, `.command-cards`.
#### Fingerprint-gap lukket
- `requirements.headers`: utvidet til `/^\s*#\s*(EU\s*AI\s*Act\s*[—-]\s*Krav|AI\s*Act-?krav|Krav per|Requirements)/im` (matcher "EU AI Act — Krav for høyrisiko provider+deployer").
- `license.headers`: utvidet til `/^\s*#\s*(Lisens(kart|kapabilitets|-kapabilitets)?(legging|matrise)?|License\s*Mapping)/im` (matcher "Lisens-kapabilitetsmatrise").
- `KNOWN_GAP_FIXTURES = {}` i `tests/test-playground-fingerprints.sh` (var `{ requirements: true, license: true }`).
#### Migrasjon utvidet (v2→v3) med parserFor
- `migrateDataVersion(state, archetypeFor, parserFor)` tredje arg lagt til.
- Hvis `reports[cid].parsed` mangler men `raw_markdown` finnes, kjøres `parserFor(cid)` automatisk.
- `defaultParserFor(cmdId)` resolverer `PARSERS[archetypeFor(cmdId)]`.
- Tre callsites oppdatert: cold-load, import-state, load-demo.
#### Browser-fixes funnet via dogfood
- `components-tier4-project-view.css` lagt til i `<link>`-chain (filen var vendored, men ikke loaded → modal-overlay og two-column layout virket ikke).
- `renderImportModal` setter `data-open="true"``.import-modal`-div (DS-kontrakt: `display: flex` aktiveres kun ved `[data-open="true"]`).
#### Tester
- `bash tests/run-e2e.sh --playground`**386 PASS**, 0 FAIL, 2 WARN (pre-eksisterende: `.cmd-pipeline` reservert; multiselect form-felt).
- v3-static: 219 PASS (var 202 — la til 17 nye renderer-routing-asserts)
- parsers: 70 PASS
- migrations: 16 PASS
- fingerprints: 32 PASS (var 30, 2 WARN → 32, 0 WARN)
- project-view: 30 PASS
- actions: 19 PASS
- `bash tests/validate-plugin.sh` → 219 PASS.
#### Screenshots regenerert til v1.15.0/
24 PNG-er (12 surfaces × 2 tema, retina, fullPage der applicable). Nye surfaces: project-overview, project-artifact-{classify, security, ros, cost, summary}, project-import-modal (viewport-only), project-search.
#### Demo-flyt verifisert i nettleser
- "Last inn demo-data" → 17 artifacts loaded + migrasjonen v2→v3 fyller `parsed`/`verdict`/`keyStats`.
- Sidebar viser alle 17 grupperte commands med severity-badges.
- Aggregate verdict (BLOKKERT) + key stats (17/17, 5/5, 2026-05-04) i header.
- Importer/Re-importer-modal åpner som overlay med backdrop.
- Per-artifact navigasjon (klikk i sidebar) → mounter riktig renderer i main-area.
### Notes on 1.15.0
- Sesjon 5 fullført i én pass — token-budsjettet (~80-100k) holdt.
- v2-mockup.local.html + V2-PROJECT-VIEW-SPEC.local.md beholdt inntil sesjon 8 ship (per scope grenser).
- Pre-eksisterende kosmetiske issues (duplisert artifact-title, "Manglende rapporter"-heading-feil) ikke fikset — utenfor scope for sesjon 5 (integration), planlagt v1.16.0.
## [1.14.0] - 2026-05-08
### Changed — playground root-cause refaktor (6 sesjoner)

View file

@ -187,12 +187,14 @@ claude --plugin ./plugins/ms-ai-architect
/architect:help
```
## Playground (v3 / v1.14.0)
## Playground (v3 / v1.15.0)
Interaktiv decision-builder + rapport-viewer for Microsoft AI-beslutninger. Erstatter v2 5-stegs-pipelinen med en multi-surface-app som persisterer state og visualiserer importerte rapporter inline. Spec: v3-arkitektur dokumentert under `.claude/projects/2026-05-03-playground-v3-architecture/`. v1.10.0-utvidelser dokumentert under `.claude/projects/2026-05-03-ms-ai-architect-v1-10-playground/`. v1.11.0 leverer design-system 100%-adoption (PARALLEL-CSS-migrasjon til DS-konvensjon, inline `<style>`-trim 37%, severity-coded card borders, app-header-restruktur, `.stack-lg` body spacing, AI Act-pyramide bredde-fix). v1.13.0/.1 patchet 10+ symptomatiske visuelle bugs. v1.14.0 leverer root-cause refaktor over 6 sesjoner: B-DS-1/2/3 fikset i shared/ DS v0.4.0 (kanban-card word-break, expansion title-block, matrix-bubble cursor); 3 risk-renderere (renderDpia/Security/Ros) til DS-summary-grid + ros-layout; 6 compliance/govern-renderere bytter lokal `.report-meta`-wrapper mot DS-konvensjon; renderMigrate + renderPoc til expansion-list per fase (slett `.phase-detail`-CSS); 5b-fixes: renderCost p50/p90-objekter ekstrahert via `.monthly` (var "[object Object]"), renderCompare distinctive-token-matching erstatter firstWord-heuristikk, renderUtredning droppet misvisende `role="tab"`. Lokal `<style>`-blokk: 191 → 122 effektive linjer (~36% reduksjon). Alle 17 renderere PASS visuell QA.
Interaktiv decision-builder + rapport-viewer for Microsoft AI-beslutninger. Erstatter v2 5-stegs-pipelinen med en multi-surface-app som persisterer state og visualiserer importerte rapporter inline. Spec: v3-arkitektur dokumentert under `.claude/projects/2026-05-03-playground-v3-architecture/`. v1.10.0-utvidelser dokumentert under `.claude/projects/2026-05-03-ms-ai-architect-v1-10-playground/`. v1.11.0 leverer design-system 100%-adoption. v1.13.0/.1 patchet 10+ symptomatiske visuelle bugs. v1.14.0 leverer root-cause refaktor over 6 sesjoner (DS-konvensjon-adopsjon på 14 renderere, lokal CSS halvert).
**v1.15.0 (sesjon 5 av ~8 i v2-prosjektet):** Project-surface byttet fra v2 `renderProjectSurface` (screen-tabs + category-tabs + per-command paste-cards) til v3 `renderProjectView` (sidebar med 17 artifacts + main-area + import-modal overlay). `renderActive()` ruter `project`-surface til `renderProjectSurfaceV3()` som wrapper renderProjectView + topbar + app-shell. V2-surface helt fjernet: `renderProjectSurface` (152 linjer), `renderCommandSubCard` (87 linjer), `rehydratePasteImports` (15 linjer), `currentProjectScreen`, `ACTIONS['project-screen']`, 5 v2-CSS-klasser. Zombie-handlers beholdt for test-back-compat: `currentProjectTab`, `ACTIONS['project-tab']`, `ACTIONS['parse']`, `handlePasteImport`, `window.__handlePasteImport`. 2 fingerprint-gap lukket: requirements.headers + license.headers. `migrateDataVersion` utvidet med `parserFor` → demo-state (kun `raw_markdown`) auto-parses til `project.artifacts[cid]`. Ship-QA-bugfixes: `components-tier4-project-view.css` lagt til i `<link>`-kjeden (manglet → modal-overlay og two-column layout virket ikke); `renderImportModal` setter `data-open="true"` (DS-kontrakt).
- **Fil:** `playground/ms-ai-architect-playground.html` (~3870+ linjer, single-file v3-arkitektur)
- **4 surfaces:** Onboarding (18 felles felt — 4 strukturerte / 14 fritekst etter v1.10.0) → Home (prosjekt-liste + 3 entry-tracks) → Catalog (25 commands gruppert i 5 expansion-grupper med søk) → Project (per-prosjekt tabs, command-form-prefill fra felles state, paste-back-import med rapport-visualisering)
- **4 surfaces:** Onboarding (18 felles felt — 4 strukturerte / 14 fritekst etter v1.10.0) → Home (prosjekt-liste + 3 entry-tracks) → Catalog (25 commands gruppert i 5 expansion-grupper med søk) → **Project v3** (sidebar med 17 artifacts gruppert i 4 kategorier + søk + main-area med per-artifact view eller overview med top-risks/next-actions + import-modal som DS-overlay)
- **Persistens:** IndexedDB-primær med localStorage-fallback. Schema-versjonert (`STATE_KEY = 'ms-ai-architect-state-v1'`) med eager `MIGRATIONS`-pipeline. v1.10.0 introduserer `dataVersion v1→v2`-migrasjon (idempotent) som backfill-er `verdict`+`keyStats`.
- **17 rapport-renderers (felles grunnskjelett):** Alle wrapper output via `renderPageShell()` med eyebrow + h1 + valgfri verdict-pill + valgfri key-stats-grid + arketype-spesifikk body. Parser → struktur → HTML rutet via kanonisk archetype-routing-tabell.
- **Foundation-helpers:** `renderPageShell`, `renderVerdictPill`, `renderKeyStatsGrid`, `inferVerdict`, `inferKeyStats`, `KEY_STATS_CONFIG`.
@ -200,23 +202,26 @@ Interaktiv decision-builder + rapport-viewer for Microsoft AI-beslutninger. Erst
- **Theme:** Mørk default + lys theme-toggle med Aksel-tokens i begge moduser (lagt til i v1.10.0). Persistert i `localStorage('ms-ai-architect-theme')`. Theme-bootstrap-script i `<head>` unngår FOUC.
- **Eksport/import:** JSON Decision Record-envelope (Blob + FileReader), schema-versjon-bevisst på import.
### Validering (v1.14.0-tall)
### Validering (v1.15.0-tall)
| Test | Kommando | Dekning |
|------|----------|---------|
| Statisk struktur | `bash tests/test-playground-v3.sh` | 202 PASS — vendored CSS, surfaces, 25 commands, 14 parsere, 17 renderers (felles grunnskjelett), design-system-klasser, action-handlers, Tier 3-bruk, onboarding field-distribution |
| Statisk struktur | `bash tests/test-playground-v3.sh` | 219 PASS, 2 WARN (pre-eks.) — vendored CSS, surfaces, 25 commands, 14 parsere, 17 renderers via PROJECT_VIEW_CONFIG.renderers-routing, action-handlers |
| Parser-fixtures | `bash tests/test-playground-parsers.sh` | 70 PASS — 17 fixtures × parser-routing |
| Migrasjon | `bash tests/test-playground-migrations.sh` | 7 PASS — v1→v2 idempotent migrasjon |
| Kombinert (E2E) | `bash tests/run-e2e.sh --playground` | 272 PASS — statisk + parser-suiter |
| Migrasjon | `bash tests/test-playground-migrations.sh` | 16 PASS — v1→v2 + v2→v3 idempotent migrasjon |
| Fingerprints | `bash tests/test-playground-fingerprints.sh` | 32 PASS — 17-fixture true-positive + 4 anti-match + API-sanity |
| Project-view | `bash tests/test-playground-projectview.sh` | 30 PASS — 4 view-states + nav-søk + null-guard |
| ACTIONS | `bash tests/test-playground-actions.sh` | 19 PASS — 6 pure-state-handlers + projectViewUiState |
| Kombinert (E2E) | `bash tests/run-e2e.sh --playground` | 386 PASS, 0 FAIL, 2 WARN |
| Plugin-validering | `bash tests/validate-plugin.sh` | 219 PASS |
| Manuell A11Y QA | Se `playground/MANUAL-CHECKLIST.md` | 10 seksjoner inkl. axe-core-kjøring per surface |
| A11Y-rapport | `playground/A11Y-RAPPORT.md` | Statisk vurdering klar — browser-axe-kjøring pending |
### Demo system (v1.11.0)
### Demo system (v1.11.0 → v1.15.0)
`scripts/build-demo-state.mjs` leser alle 17 fixture-filer fra `playground/test-fixtures/` og injiserer dem som en `<script type="application/json" id="demo-state-v1">`-blokk i playground HTML (idempotent — erstatter eksisterende blokk). "Last inn demo-data"-knappen på onboarding-overflaten kaller `ACTIONS['load-demo']` som leser blokken, erstatter alle state-grener via Proxy-mutasjon, og navigerer til project-surface med 17 pre-importerte rapporter. `rehydratePasteImports()` kjøres via `queueMicrotask` etter project-surface render — fyller textareas fra `project.reports[id].raw_markdown` og kaller `handlePasteImport` for hver. `handlePasteImport` har equal-value-guard for å unngå render-loop.
`scripts/build-demo-state.mjs` leser alle 17 fixture-filer fra `playground/test-fixtures/` og injiserer dem som en `<script type="application/json" id="demo-state-v1">`-blokk i playground HTML (idempotent — erstatter eksisterende blokk). "Last inn demo-data"-knappen på onboarding-overflaten kaller `ACTIONS['load-demo']` som leser blokken, erstatter alle state-grener via Proxy-mutasjon, kjører `migrateDataVersion` (v2→v3 auto-parser raw_markdown til artifacts), og navigerer til project-surface. Demo viser 17 artifacts gruppert i sidebar med severity-badges, aggregate verdict (BLOKKERT), top-risks-liste, og fungerende re-importer/slett-knapper per artifact.
`tests/screenshot/` inneholder en frittstående Playwright-runner med egen `package.json` (gitignored `node_modules`). `node run.mjs` produserer 24 PNG-er (12 surfaces × 2 tema, retina, fullPage) under `playground/screenshots/v1.14.0/` (v1.10.0 + v1.11.0 beholdt som historisk referanse). Disse committes så forkere ser pluginen uten å installere noe. Demo-org er "Acme Kommune" og demo-prosjekt er "Acme: Kunde-chatbot" — konsistente navn på tvers av alle 17 fixtures (etter v1.11.0 rename fra "Acme AS" / "Demosystem").
`tests/screenshot/` inneholder en frittstående Playwright-runner med egen `package.json` (gitignored `node_modules`). `node run.mjs` produserer 24 PNG-er (12 surfaces × 2 tema) under `playground/screenshots/v1.15.0/`. v1.15.0-surfaces: onboarding-empty, project-overview, project-artifact-{classify,security,ros,cost,summary}, project-import-modal (viewport-only — modal er position:fixed overlay), project-search, home, catalog, onboarding-prefilled. v1.10.0/v1.11.0/v1.14.0 beholdt som historisk referanse. Disse committes så forkere ser pluginen uten å installere noe. Demo-org er "Acme Kommune" og demo-prosjekt er "Acme: Kunde-chatbot".
### Design-system 100%-adoption (v1.11.0 → v1.14.0)

View file

@ -6,7 +6,7 @@
*AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)*
![Version](https://img.shields.io/badge/version-1.14.0-blue)
![Version](https://img.shields.io/badge/version-1.15.0-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Docs](https://img.shields.io/badge/reference_docs-387-green)
![Agents](https://img.shields.io/badge/agents-12-orange)
@ -638,6 +638,7 @@ Category-to-skill routing is defined in `scripts/skill-gen/category-skill-map.js
| Version | Date | Highlights |
|---------|------|-----------|
| **1.15.0** | 2026-05-16 | Playground v3 project-view integration — `renderProjectSurface` (v2 screen-tabs + category-tabs + per-command paste-cards) erstattet av `renderProjectView` (sidebar med 17 artifacts + main-area + import-modal overlay). `renderActive()` delegerer nå til `renderProjectSurfaceV3()`. Dead code fjernet: `renderCommandSubCard`, `rehydratePasteImports`, `currentProjectScreen`, `ACTIONS['project-screen']`, 5 v2-CSS-klasser (`.project-tabs`, `.project-tab`, `.project-tab__count`, `.sub-zone`, `.paste-import-row`, `.project-header__*`, `.command-cards`). 2 fingerprint-gap lukket: `requirements.headers` matcher nå "EU AI Act — Krav for høyrisiko..."; `license.headers` matcher "Lisens-kapabilitetsmatrise...". v2→v3 migrasjon utvidet med `parserFor` slik at demo-state med kun `raw_markdown` auto-parses inn i `project.artifacts[cid]`. `components-tier4-project-view.css` wired inn (var ikke loaded — modal-overlay og two-column layout virket ikke). `renderImportModal` setter `data-open="true"` (DS-kontrakt). 219 plugin-validering, 386 E2E playground (32 fingerprints, 219 v3-static, 70 parsers, 16 migrations, 30 project-view, 19 actions), 0 FAIL, 2 WARN (pre-eksisterende). 24 screenshots regenerert til `playground/screenshots/v1.15.0/`. Demo viser nå 17 artifacts navigerbare i sidebar, aggregate verdict (BLOKKERT), top-risks-liste, og fungerende re-importer/slett-knapper per artifact. |
| **1.14.0** | 2026-05-08 | Playground root-cause refaktor — DS-konvensjon-adopsjon på tvers av 14 renderere over 6 sesjoner. Sesjon 2: B-DS-1/2/3 fikset i shared/ DS v0.4.0 (kanban-card word-break, expansion title-block, matrix-bubble cursor). Sesjon 3: renderDpia/Security/Ros til DS-summary-grid + ros-layout. Sesjon 4: 6 compliance/govern-renderere bytter `.report-meta`-wrapper mot DS-konvensjon (renderAiActPyramid, renderRequirements, renderConformity, renderTransparency, renderFria, renderReview). Sesjon 5: renderMigrate + renderPoc → expansion-list per fase (slett `.phase-detail`-CSS). Sesjon 5b: renderCost key-stats viste "[object Object]" (parser-output har p50/p90 = {monthly,yearly}-objekter — nå ekstrahert via `.monthly`); renderCompare distinctive-token-matching erstatter firstWord-heuristikk; renderUtredning droppet misvisende `role="tab"`-attributter. Lokal `<style>`-blokk: 191 → 122 effektive linjer (~36% reduksjon). 17 renderere PASS visuell QA mot demo-data. 219 plugin-validering, 272 E2E playground, 7 migrations PASS. 24 screenshots regenerert. |
| **1.13.1** | 2026-05-06 | Playground visual bugs patch — 10 bugs identifisert post-v1.13.0 av maintainer i nettleser. Fixet: (B7) classify Forpliktelser indent via `.report-meta` CSS-reset; (B8a) `requirement-expand` ACTIONS-handler manglet — R-01..R-09-rader i AI Act-krav var ikke klikkbare; (B8b) expansion title-main + title-sub display:block så de stables; (B10) kanban-card `word-break:break-word` override DS' break-all; (B11) DPIA matrix-bobler match by description (Pass 1 first-cell exact + Pass 2 any-cell substring); (B12, B13, B15) defensive `display:block; clear:both; width:100%` på top-risks/suppressed-panel/phase-detail/aiact-timeline; (B14) Migrate/POC phases-summary-tabell over phase-detail-seksjoner. 23/23 smoke + 271 E2E + 219 plugin-validering. |
| **1.13.0** | 2026-05-06 | Playground visual DS-fixes — 5 bugs identifisert og fikset i fix-pakke som speiler llm-security v7.6.1: (B1) `renderFindingsBlock` + `renderRequirements` outer-wrapper byttet fra `<div class="findings">` (DS grid 360px+1fr klemte indre struktur) til `<section class="report-meta">`; (B2) lokal `.report-table` CSS for 6+ rapporter (Trusler, Kostnadsoversikt, TCO, Risiko, Key Metrics) som manglet styling; (B3) ROS-matrise-bobler byttet `<span>``<button>` med `data-threat-id` + click-handler som scroller til Trusler-tabell-rad og highlighter; (B4) `renderRadarSvg` bumpet 300×300→380×380, R=125, dynamisk `text-anchor` for å unngå label-overlap ved 6+ akser; (B5) `recommendation-card__body` overflow-wrap. 22/22 smoke-test PASS. 219 plugin-validering. 272 E2E. |

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

View file

@ -1,5 +1,65 @@
# playground-design-system — CHANGELOG
## 0.6.0 — 2026-05-15
### Added — Project-view archetype (Tier 4)
Generic "project as artifact-collection" archetype for plugins where a project owns 0-N read-only report artifacts grouped by category. Default view is an aggregated dashboard; clicking a sidebar item swaps the main panel to the per-artifact render. Edit-mode is paste-import only (no inline editor).
- **New file `components-tier4-project-view.css`** — 11 sections covering:
- `.project-view` + `.project-view__layout` (grid: nav 280px + main 1fr, responsive collapse at 1280 / 960px)
- `.project-view__header` (CSS Grid with eyebrow/title/lede/verdict/key-stats/actions areas)
- `.verdict-pill` (small pill variant — companion to existing `.verdict-pill-lg` in tier2)
- `.project-view__nav` + `.project-view__nav-search` (sticky sidebar with search)
- `.artifact-list` + `__group` / `__group-label` / `__group-count` / `__group-items` / `__item` / `__item-marker` / `__item-body` / `__item-name` / `__item-meta` (grouped, severity-coded sidebar)
- `.artifact-status[data-severity]` (mini-pill: positive | medium | critical)
- `.project-view__main` (main column container)
- `.project-overview` + `__intro` / `__verdict-grid` / `__verdict-tile[data-severity]` / `__section` / `__top-risks` / `__next-actions` / `__missing-reports` (aggregated dashboard)
- `.project-view__artifact` + `__artifact-header` / `__artifact-title` / `__artifact-meta` / `__artifact-actions` / `__artifact-body` (single-rapport viewer wrapper)
- `.empty-artifact-prompt` + `__icon` / `__title` / `__text` / `__actions` (empty-state)
- `.import-modal` + `__backdrop` / `__panel` / `__head` / `__title` / `__close` / `__form` / `__detect` / `__preview` / `__preview-label` / `__footer` (overlay modal for paste-import)
- **6 new tokens in `tokens.css`:**
- `--project-view-nav-width: 280px` — sidebar width at full layout
- `--project-view-collapse-bp: 960px` — doc-only token referenced by responsive breakpoints
- `--artifact-list-item-pad-y: var(--space-2)` — sidebar row vertical padding
- `--artifact-list-item-pad-x: var(--space-3)` — sidebar row horizontal padding
- `--artifact-marker-size: 14px` — sidebar status marker diameter
- `--artifact-marker-border: 1.5px` — sidebar status marker border thickness
### Påvirkning
Endringen er **additiv**: ny komponent-fil + 6 nye tokens, ingen eksisterende selectors eller verdier endres. Plugin-konsumenter (`ms-ai-architect`, `llm-security`, `okr`, `config-audit`, `voyage`) får silent drift mot ny source-commit, men kan re-sync på eget tempo. Bare `ms-ai-architect` og `llm-security` re-syncer i samme commit som denne DS-bumpen (forberedelse til koordinert v1.15.0 / v7.7.0-release etter ~8 sesjoner med JS-implementasjon).
Førsteadoptere: `ms-ai-architect` v1.15.0 (17 artefakter, 5 kategorier) + `llm-security` v7.7.0 (≥18 artefakter, 6 kategorier). State-driven visibility håndteres i plugin-JS, ikke i denne CSS-en — kun aktiv state rendres per pass.
### Plugins som må laste den nye filen
Etter `<link>` til `components-tier3-supplement.css`, legg til:
```html
<link rel="stylesheet" href="vendor/playground-design-system/components-tier4-project-view.css">
```
### For å adoptere v0.6.0
```bash
node scripts/sync-design-system.mjs <plugin-name>
# --force hvis drift detected
```
## 0.5.0 — 2026-05-10
### Added
- **voyage scope tokens (B-DS-4):** `--color-scope-voyage` (aqua-blue `#1B5FB8`), `--color-scope-voyage-soft` (`#E5EFFA`), `--color-scope-voyage-strong` (`#143E78`) appended to scope-color group in `tokens.css`. Matches the existing `--color-scope-{architect,okr,security,ultraplan,config}` family so voyage-playground can use the canonical badge convention.
- **`.badge--scope-voyage`** in `base.css`: white-on-aqua-blue badge variant matching the existing scope-badge family.
### Påvirkning
Endringen er **additiv**: legger TIL voyage-scope-tokens og en ny badge-modifier. Ingen eksisterende selectors eller token-verdier endres. Plugin-konsumenter (llm-security, ms-ai-architect, okr, config-audit) får stale vendor-state mot ny source-commit, men det er silent drift — re-sync skjer på eget tempo neste playground-touch. Bare `voyage` re-syncer i denne commit-en.
Førsteadopter: `voyage` v4.3.0 (multi-sesjons-løp 2026-05-10, sesjon 1 = Wave 0+1 Foundation).
## 0.4.0 — 2026-05-08
### Bug fixes

View file

@ -2,16 +2,17 @@
"generated_by": "scripts/sync-design-system.mjs",
"do_not_edit": true,
"source": "shared/playground-design-system/",
"source_commit": "9f806469f37742be65f778059bf364308c9d2811",
"sync_date": "2026-05-08T17:57:53.412Z",
"file_count": 26,
"source_commit": "c1b7bad3899c5cfe9ff90663003609b018aa79a0",
"sync_date": "2026-05-15T14:11:07.444Z",
"file_count": 27,
"files": {
"CHANGELOG.md": "dfbd75552c94848acba3e2503bfad56c1c4bc8cfdcbd638d9409149010913d28",
"CHANGELOG.md": "b5018b46cd0830334109e915d23b5554c060412c2b7e132f97f2933e5dd5d79c",
"README.md": "83de0e29b207c979b7b2a3327b7a4ec0c2e1b4d3705ee2677f26c28c3a3ee643",
"base.css": "604fe6839e2ed304bc0ba112a4e045f208b4b3f084f449a1abdb94ce0a1e5263",
"base.css": "df0db874473412eb771b7355b589f7478042987756898f0921584286bd5ba70a",
"components-tier2.css": "c2cb7e9d76d6af28d50db654030413777feb2f2f2b93213e598de8b686b14523",
"components-tier3-supplement.css": "51fab10377d80029d6552613069d46fd14ce66af77fe6705b1c6bdf5c9e6481e",
"components-tier3.css": "c391ea387298ce864bc35078e7e044b2cdd4187e3130456347d91876599ff4b1",
"components-tier4-project-view.css": "f8f784df70044ecc9bdc862a327b1ee58b201d056581316808a9b60632c5a993",
"components.css": "56fa7392b8b20b567a46f72a8fe9e0205d78ce475eae6b22fc3f50b39b235545",
"fonts.css": "e3c3df581c6e4d66e25c555f125c745f6512a33038401089d2519a94ea63ee3d",
"fonts/Inter-Bold.woff2": "220976705fbec109f43c5cfdceca639e99ace7e51f3eb67292b105d3575eb39b",
@ -31,6 +32,6 @@
"schemas/finding.schema.json": "0b24797373650582bac232d31a4dd9260593375a0d17259e18f1141a20de8d0c",
"schemas/okr-set.schema.json": "aa27347fb232a956ec9dcee1775115710e2715a665c8d729ac50b90c6884de26",
"schemas/ros-threat.schema.json": "e16497c1a6b79d6e78149d6cf1c28ac9df1e93234627a0c546814fb24d6c96d9",
"tokens.css": "1499bc2eea0178e35935413c79a10bbee7d49fdfa91bd33eeba3bb9e9acab809"
"tokens.css": "63dca13f8341937169fc8e84d3f37ae0c714901fa006c865ea377bd448f87644"
}
}

View file

@ -146,6 +146,7 @@ button { font-family: inherit; }
.badge--scope-security { background: var(--color-scope-security); color: #fff; border-color: transparent; }
.badge--scope-ultraplan { background: var(--color-scope-ultraplan); color: #fff; border-color: transparent; }
.badge--scope-config { background: var(--color-scope-config); color: #fff; border-color: transparent; }
.badge--scope-voyage { background: var(--color-scope-voyage); color: #fff; border-color: transparent; }
/* ---------- Cards / surfaces ---------- */
.card {

View file

@ -0,0 +1,666 @@
/* Code generated by sync-design-system.mjs; DO NOT EDIT. */
/* =============================================================================
Playground Design System components-tier4-project-view.css
v0.6.0 Tier 4 project-view archetype
============================================================================
Generic "project as artifact-collection" archetype. Default-view is an
aggregated overview dashboard; clicking a sidebar item swaps main to a
per-artifact render. Tracks 0-N read-only artifacts; edit-mode is paste-
import only (markdown from terminal parser store).
First adopters: ms-ai-architect v1.15.0 (17 artifacts, 5 categories) +
llm-security v7.7.0 (18 artifacts, 6 categories). Each plugin injects a
PROJECT_VIEW_CONFIG object that maps commands renderers, categories,
verdict-aggregators, missing-report heuristics.
The CSS in this file is plugin-agnostic. Plugin-specific shape (category
names, artifact ordering, custom severity-mappings) lives in JS config.
State-driven visibility is NOT handled here production playgrounds emit
only the active state (overview | artifact | empty | import) per render
pass. The mockup uses body[data-state="..."] for prototyping; production
renders one branch at a time.
============================================================================= */
/* === 1. Project-view top-level layout ===================================== */
.project-view {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.project-view__layout {
display: grid;
grid-template-columns: var(--project-view-nav-width) 1fr;
gap: var(--space-6);
align-items: start;
}
@media (max-width: 1279px) {
.project-view__layout { grid-template-columns: 240px 1fr; }
}
@media (max-width: 959px) {
.project-view__layout { grid-template-columns: 1fr; }
}
/* === 2. Project-view header =============================================== */
.project-view__header {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-5) var(--space-6);
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas:
"title verdict"
"title keystats"
"actions actions";
gap: var(--space-4) var(--space-6);
align-items: start;
}
.project-view__title-block { grid-area: title; }
.project-view__verdict { grid-area: verdict; justify-self: end; }
.project-view__key-stats { grid-area: keystats; justify-self: end; }
.project-view__actions { grid-area: actions; display: flex; gap: var(--space-2); justify-content: flex-end; }
.project-view__eyebrow {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-text-tertiary);
font-weight: var(--font-weight-semibold);
margin: 0 0 var(--space-2) 0;
}
.project-view__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
margin: 0 0 var(--space-2) 0;
}
.project-view__lede {
color: var(--color-text-secondary);
margin: 0;
max-width: 60ch;
}
.project-view__key-stats {
display: flex;
gap: var(--space-5);
}
.project-view__key-stat-label {
font-size: 10px;
text-transform: uppercase;
color: var(--color-text-tertiary);
letter-spacing: 0.06em;
}
.project-view__key-stat-value {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
font-variant-numeric: tabular-nums;
}
/* === 3. Verdict-pill (small) ==============================================
Companion to .verdict-pill-lg (Tier 2). Inline-flex pill used in project
header + sidebar status badges. The larger -lg variant lives in
components-tier2.css; both share the same severity-band semantics. */
.verdict-pill {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: 4px 12px;
border-radius: 999px;
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-sm);
}
.verdict-pill--positive { background: var(--color-state-success); color: #fff; }
.verdict-pill--medium { background: var(--color-severity-medium); color: var(--color-severity-medium-on); }
.verdict-pill--critical { background: var(--color-severity-critical); color: #fff; }
.verdict-pill--in-progress {
background: var(--color-bg-soft);
color: var(--color-text-secondary);
border: 1px dashed var(--color-border-moderate);
}
/* === 4. Sidebar nav ======================================================= */
.project-view__nav {
position: sticky;
top: var(--space-6);
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.project-view__nav-search input {
width: 100%;
box-sizing: border-box;
padding: 6px 10px;
font-size: var(--font-size-sm);
background: var(--color-bg);
color: var(--color-text-primary);
border: 1px solid var(--color-border-moderate);
border-radius: var(--radius-sm);
}
/* === 5. Artifact-list ===================================================== */
.artifact-list {
display: flex;
flex-direction: column;
gap: var(--space-4);
margin: 0;
padding: 0;
list-style: none;
}
.artifact-list__group {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.artifact-list__group-label {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-tertiary);
font-weight: var(--font-weight-semibold);
padding: 0 var(--space-2);
}
.artifact-list__group-count {
background: var(--color-bg-soft);
color: var(--color-text-tertiary);
font-family: var(--font-family-mono);
font-size: 10px;
padding: 1px 6px;
border-radius: 999px;
}
.artifact-list__group-items {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.artifact-list__item {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: var(--space-2);
padding: var(--artifact-list-item-pad-y) var(--artifact-list-item-pad-x);
border-radius: var(--radius-sm);
cursor: pointer;
background: transparent;
border: 1px solid transparent;
transition: background 120ms ease, border-color 120ms ease;
}
.artifact-list__item:hover { background: var(--color-bg-soft); }
.artifact-list__item[data-state="active"] {
background: var(--color-bg-soft);
border-color: var(--color-primary-500);
box-shadow: inset 3px 0 0 var(--color-primary-500);
padding-left: calc(var(--artifact-list-item-pad-x) - 3px);
}
.artifact-list__item-marker {
width: var(--artifact-marker-size);
height: var(--artifact-marker-size);
border-radius: 50%;
border: var(--artifact-marker-border) solid var(--color-border-moderate);
background: transparent;
flex-shrink: 0;
}
.artifact-list__item[data-state="filled"][data-severity="positive"] .artifact-list__item-marker {
background: var(--color-state-success);
border-color: var(--color-state-success);
}
.artifact-list__item[data-state="filled"][data-severity="medium"] .artifact-list__item-marker {
background: var(--color-severity-medium);
border-color: var(--color-severity-medium);
}
.artifact-list__item[data-state="filled"][data-severity="critical"] .artifact-list__item-marker {
background: var(--color-severity-critical);
border-color: var(--color-severity-critical);
}
.artifact-list__item-body { min-width: 0; }
.artifact-list__item-name {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.artifact-list__item[data-state="empty"] .artifact-list__item-name {
color: var(--color-text-tertiary);
font-weight: var(--font-weight-regular);
}
.artifact-list__item-meta {
font-size: 11px;
color: var(--color-text-tertiary);
}
/* === 6. Artifact-status (mini pill in sidebar) =========================== */
.artifact-status {
font-family: var(--font-family-mono);
font-size: 10px;
font-weight: var(--font-weight-semibold);
padding: 1px 5px;
border-radius: var(--radius-sm);
letter-spacing: 0.04em;
}
.artifact-status[data-severity="positive"] { background: var(--color-severity-low-soft); color: var(--color-severity-low-on); }
.artifact-status[data-severity="medium"] { background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on); }
.artifact-status[data-severity="critical"] { background: var(--color-severity-critical-soft); color: var(--color-severity-critical-on); }
/* === 7. Project-view main panel ========================================== */
.project-view__main {
min-width: 0;
display: flex;
flex-direction: column;
gap: var(--space-5);
}
/* === 8. Project-overview (default dashboard) ============================= */
.project-overview {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.project-overview__intro {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-5);
}
.project-overview__intro h2 {
font-size: var(--font-size-lg);
margin: 0 0 var(--space-2) 0;
}
.project-overview__intro p {
color: var(--color-text-secondary);
margin: 0;
}
.project-overview__verdict-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--space-3);
}
.project-overview__verdict-tile {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-left: 4px solid var(--color-border-moderate);
border-radius: var(--radius-md);
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.project-overview__verdict-tile[data-severity="positive"] { border-left-color: var(--color-state-success); }
.project-overview__verdict-tile[data-severity="medium"] { border-left-color: var(--color-severity-medium); }
.project-overview__verdict-tile[data-severity="critical"] { border-left-color: var(--color-severity-critical); }
.project-overview__verdict-tile[data-severity="empty"] { border-left-style: dashed; }
.project-overview__verdict-tile-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-tertiary);
font-weight: var(--font-weight-semibold);
}
.project-overview__verdict-tile-value {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
}
.project-overview__verdict-tile-meta {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
}
.project-overview__section h3 {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-text-tertiary);
font-weight: var(--font-weight-semibold);
margin: 0 0 var(--space-3) 0;
}
.project-overview__top-risks,
.project-overview__next-actions {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-5);
}
.project-overview__top-risks ol,
.project-overview__next-actions ol {
list-style: none;
counter-reset: rank;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.project-overview__top-risks li,
.project-overview__next-actions li {
counter-increment: rank;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
background: var(--color-bg-soft);
}
.project-overview__top-risks li::before,
.project-overview__next-actions li::before {
content: counter(rank);
font-family: var(--font-family-mono);
font-weight: var(--font-weight-bold);
color: var(--color-text-tertiary);
font-size: var(--font-size-sm);
min-width: 20px;
}
.project-overview__missing-reports {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-5);
}
.project-overview__missing-reports ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.project-overview__missing-reports li {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) var(--space-3);
background: var(--color-bg-soft);
border-radius: var(--radius-sm);
border-left: 3px dashed var(--color-border-moderate);
}
/* === 9. Artifact-view (one report rendered) ============================== */
.project-view__artifact {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.project-view__artifact-header {
display: flex;
justify-content: space-between;
align-items: start;
gap: var(--space-4);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--color-border-subtle);
}
.project-view__artifact-title {
font-size: var(--font-size-xl);
margin: 0 0 var(--space-1) 0;
}
.project-view__artifact-meta {
font-size: var(--font-size-sm);
color: var(--color-text-tertiary);
margin: 0;
}
.project-view__artifact-actions {
display: flex;
gap: var(--space-2);
flex-shrink: 0;
}
.project-view__artifact-body {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
/* === 10. Empty-artifact-prompt (no report imported yet) ================== */
.empty-artifact-prompt {
background: var(--color-surface);
border: 2px dashed var(--color-border-moderate);
border-radius: var(--radius-md);
padding: var(--space-8);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
text-align: center;
}
.empty-artifact-prompt__icon {
font-size: 48px;
opacity: 0.5;
}
.empty-artifact-prompt__title {
font-size: var(--font-size-lg);
margin: 0;
}
.empty-artifact-prompt__text {
color: var(--color-text-secondary);
margin: 0;
max-width: 50ch;
}
.empty-artifact-prompt__actions {
display: flex;
gap: var(--space-2);
margin-top: var(--space-2);
}
/* === 11. Import-modal (overlay) ========================================== */
.import-modal {
position: fixed;
inset: 0;
z-index: 200;
display: none;
}
.import-modal[data-open="true"] {
display: flex;
align-items: center;
justify-content: center;
}
.import-modal__backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
}
.import-modal__panel {
position: relative;
width: min(720px, 92vw);
max-height: 90vh;
overflow: auto;
background: var(--color-surface);
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
display: flex;
flex-direction: column;
}
.import-modal__head {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-3);
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--color-border-subtle);
}
.import-modal__title {
margin: 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
}
.import-modal__close {
background: transparent;
border: none;
cursor: pointer;
padding: 4px 10px;
color: var(--color-text-tertiary);
font-size: 20px;
line-height: 1;
border-radius: var(--radius-sm);
}
.import-modal__close:hover {
background: var(--color-bg-soft);
color: var(--color-text-primary);
}
.import-modal__form {
padding: var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.import-modal__form .field {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.import-modal__form label {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
}
.import-modal__form select,
.import-modal__form textarea {
width: 100%;
box-sizing: border-box;
padding: var(--space-2) var(--space-3);
background: var(--color-bg);
color: var(--color-text-primary);
border: 1px solid var(--color-border-moderate);
border-radius: var(--radius-sm);
font-family: var(--font-family-mono);
font-size: var(--font-size-sm);
}
.import-modal__form textarea {
resize: vertical;
min-height: 180px;
}
.import-modal__detect {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
background: var(--color-severity-low-soft);
color: var(--color-severity-low-on);
font-size: var(--font-size-sm);
}
.import-modal__preview {
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
padding: var(--space-3);
background: var(--color-bg);
max-height: 200px;
overflow: auto;
}
.import-modal__preview-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-tertiary);
margin-bottom: var(--space-2);
}
.import-modal__footer {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
padding: var(--space-3) var(--space-5);
border-top: 1px solid var(--color-border-subtle);
background: var(--color-bg-soft);
}

View file

@ -102,6 +102,9 @@
--color-scope-security: #A40E26; /* llm-security — crimson */
--color-scope-ultraplan: #4338CA; /* ultraplan-local — indigo */
--color-scope-config: #3F5963; /* config-audit — slate */
--color-scope-voyage: #1B5FB8; /* voyage — aqua-blue */
--color-scope-voyage-soft: #E5EFFA; /* voyage — light tint */
--color-scope-voyage-strong: #143E78; /* voyage — dark strong */
/* ---------- Spacing -------------------------------------------------- */
--space-1: 4px;
@ -140,6 +143,14 @@
--container-default: 1080px;
--container-wide: 1280px;
--sidebar-width: 280px;
/* ---------- Project-view (Tier 4 — v0.6.0) --------------------------- */
--project-view-nav-width: 280px;
--project-view-collapse-bp: 960px; /* doc-only — referenced by media queries */
--artifact-list-item-pad-y: var(--space-2);
--artifact-list-item-pad-x: var(--space-3);
--artifact-marker-size: 14px;
--artifact-marker-border: 1.5px;
}
:root { color-scheme: light; }

View file

@ -76,6 +76,10 @@ fi
if $RUN_PLAYGROUND; then
bash "$SCRIPT_DIR/test-playground-v3.sh" || FAILURES=$((FAILURES + 1))
bash "$SCRIPT_DIR/test-playground-parsers.sh" || FAILURES=$((FAILURES + 1))
bash "$SCRIPT_DIR/test-playground-migrations.sh" || FAILURES=$((FAILURES + 1))
bash "$SCRIPT_DIR/test-playground-fingerprints.sh" || FAILURES=$((FAILURES + 1))
bash "$SCRIPT_DIR/test-playground-projectview.sh" || FAILURES=$((FAILURES + 1))
bash "$SCRIPT_DIR/test-playground-actions.sh" || FAILURES=$((FAILURES + 1))
fi
if $RUN_KB_UPDATE; then

View file

@ -1,14 +1,12 @@
#!/usr/bin/env node
// Capture playground screenshots for v1.14.0 documentation.
// Capture playground screenshots for v1.15.0 documentation.
//
// Opens the single-file playground HTML via file://, drives it through:
// - Initial onboarding (empty state)
// - "Last inn demo-data" → project surface with all 17 reports rehydrated
// - All 4 project screen-tabs (oversikt / rapporter / kontekst / eksport)
// - Each rapport-tab category (regulatory / security / economy / docs / tool)
// - Both themes (dark + light)
// v1.15.0: v2 project-surface (renderProjectSurface med screen-tabs +
// category-tabs) erstattet av renderProjectView (sidebar med 17 artifacts +
// main-area med per-artifact view + import-modal). Skjermbilder oppdatert
// til å fange v3-surfaces.
//
// Output: playground/screenshots/v1.14.0/<surface>-<theme>.png
// Output: playground/screenshots/v1.15.0/<surface>-<theme>.png
//
// Usage:
// cd tests/screenshot
@ -25,7 +23,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PLUGIN_ROOT = resolve(__dirname, '..', '..');
const HTML_PATH = join(PLUGIN_ROOT, 'playground', 'ms-ai-architect-playground.html');
const OUT_DIR = join(PLUGIN_ROOT, 'playground', 'screenshots', 'v1.14.0');
const OUT_DIR = join(PLUGIN_ROOT, 'playground', 'screenshots', 'v1.15.0');
const HTML_URL = 'file://' + HTML_PATH;
const VIEWPORT = { width: 1440, height: 900 };
@ -49,7 +47,6 @@ async function clearState(page) {
await page.evaluate(() => {
try { localStorage.clear(); } catch (e) {}
try {
// Best-effort: clear IndexedDB databases.
const dbs = ['ms-ai-architect-state-v1', 'ms-ai-architect-playground'];
dbs.forEach((n) => indexedDB.deleteDatabase(n));
} catch (e) {}
@ -61,9 +58,9 @@ async function loadDemo(page) {
const action = document.querySelector('[data-action="load-demo"]');
if (action) action.click();
});
// Wait for project surface to render + rehydrate paste-imports.
await page.waitForSelector('[data-surface="project"]:not([hidden])', { timeout: 5000 });
await page.waitForTimeout(800); // settle rehydrate microtasks
// Settle migrasjon (v2→v3 auto-parse) + render.
await page.waitForTimeout(1200);
}
async function clickAction(page, action) {
@ -71,28 +68,46 @@ async function clickAction(page, action) {
const el = document.querySelector('[data-action="' + a + '"]');
if (el) el.click();
}, action);
await page.waitForTimeout(300);
}
async function clickProjectTab(page, tabId) {
await page.evaluate((t) => {
const el = document.querySelector('[data-action="project-tab"][data-tab="' + t + '"]');
if (el) el.click();
}, tabId);
await page.waitForTimeout(400);
}
async function clickProjectScreen(page, screenId) {
await page.evaluate((s) => {
const el = document.querySelector('[data-action="project-screen"][data-screen="' + s + '"]');
async function selectArtifact(page, artifactId) {
await page.evaluate((id) => {
const el = document.querySelector('[data-action="project-select-artifact"][data-artifact-id="' + id + '"]');
if (el) el.click();
}, screenId);
}, artifactId);
await page.waitForTimeout(500);
}
async function openImportModal(page, prefillCmd) {
await page.evaluate((cid) => {
// Foretrukket: artifact-reimport-knappen (har eksisterende markdown).
if (cid) {
const el = document.querySelector('[data-action="artifact-reimport"][data-command="' + cid + '"]');
if (el) { el.click(); return; }
}
// Fallback: generisk "Importer rapport"-knapp.
const open = document.querySelector('[data-action="import-open"]');
if (open) open.click();
}, prefillCmd);
await page.waitForSelector('[data-import-modal]', { timeout: 3000 });
await page.waitForTimeout(400);
}
async function shoot(page, name) {
async function setSearchQuery(page, query) {
await page.evaluate((q) => {
const input = document.querySelector('[data-project-search]');
if (!input) return;
input.value = q;
input.dispatchEvent(new Event('input', { bubbles: true }));
}, query);
await page.waitForTimeout(400);
}
async function shoot(page, name, opts) {
const path = join(OUT_DIR, name + '.png');
await page.screenshot({ path, fullPage: FULL_PAGE });
const useFullPage = (opts && opts.fullPage != null) ? opts.fullPage : FULL_PAGE;
await page.screenshot({ path, fullPage: useFullPage });
console.log(' → ' + name + '.png');
}
@ -106,52 +121,58 @@ async function captureAllSurfaces(page, theme) {
await setTheme(page, theme);
await shoot(page, '01-onboarding-empty-' + theme);
// 2. Load demo → project surface (rapporter screen, regulatory tab default)
// 2. Load demo → project-view overview (default — ingen artifact valgt)
await loadDemo(page);
await setTheme(page, theme);
await shoot(page, '02-project-rapporter-regulatory-' + theme);
await shoot(page, '02-project-overview-' + theme);
// 3. Project tab cycle (5 categories)
const TABS = [
{ id: 'security', label: 'security' },
{ id: 'economy', label: 'economy' },
{ id: 'documentation', label: 'documentation' },
{ id: 'tool', label: 'tool' }
// 3-7. 5 sample artifacts som dekker arketype-bredden
const SAMPLE_ARTIFACTS = [
{ id: 'classify', label: 'classify' }, // AI Act-pyramide
{ id: 'security', label: 'security' }, // 6×5 sikkerhets-matrise
{ id: 'ros', label: 'ros' }, // ROS matrise + radar
{ id: 'cost', label: 'cost' }, // P10/P50/P90 distribusjon
{ id: 'summary', label: 'summary' } // Beslutningsnotat
];
for (const tab of TABS) {
await clickProjectTab(page, tab.id);
await page.waitForTimeout(500);
await shoot(page, '03-project-rapporter-' + tab.label + '-' + theme);
for (let i = 0; i < SAMPLE_ARTIFACTS.length; i++) {
const a = SAMPLE_ARTIFACTS[i];
await selectArtifact(page, a.id);
const num = String(3 + i).padStart(2, '0');
await shoot(page, num + '-project-artifact-' + a.label + '-' + theme);
}
// 4. Project screen-tabs (oversikt / kontekst / eksport)
await clickProjectScreen(page, 'oversikt');
await shoot(page, '04-project-oversikt-' + theme);
await clickProjectScreen(page, 'kontekst');
await shoot(page, '05-project-kontekst-' + theme);
await clickProjectScreen(page, 'eksport');
await shoot(page, '06-project-eksport-' + theme);
// 8. Import-modal åpen (med prefill fra eksisterende ros-artifact)
// Viewport-only (ikke fullPage) — modal er position:fixed; fullPage
// skroller forbi overlay-en og kaster bort kontekst.
await openImportModal(page, 'ros');
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(200);
await shoot(page, '08-project-import-modal-' + theme, { fullPage: false });
await clickAction(page, 'import-close');
await page.waitForTimeout(300);
// Back to rapporter for nav screenshots
await clickProjectScreen(page, 'rapporter');
// 9. Sidebar-søk aktivt (filtrer på "ros")
await setSearchQuery(page, 'ros');
await shoot(page, '09-project-search-' + theme);
await setSearchQuery(page, ''); // reset
// 5. Home surface
// 10. Home surface
await clickAction(page, 'goto-home');
await page.waitForSelector('[data-surface="home"]:not([hidden])');
await page.waitForTimeout(300);
await shoot(page, '07-home-' + theme);
await shoot(page, '10-home-' + theme);
// 6. Catalog surface
// 11. Catalog surface
await clickAction(page, 'goto-catalog');
await page.waitForSelector('[data-surface="catalog"]:not([hidden])');
await page.waitForTimeout(300);
await shoot(page, '08-catalog-' + theme);
await shoot(page, '11-catalog-' + theme);
// 7. Onboarding (with prefilled state from demo)
// 12. Onboarding prefilled (post-demo med org-felter fylt)
await clickAction(page, 'goto-onboarding');
await page.waitForSelector('[data-surface="onboarding"]:not([hidden])');
await page.waitForTimeout(300);
await shoot(page, '09-onboarding-prefilled-' + theme);
await shoot(page, '12-onboarding-prefilled-' + theme);
}
async function main() {
@ -160,7 +181,7 @@ async function main() {
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: VIEWPORT,
deviceScaleFactor: 2 // crisper screenshots for retina
deviceScaleFactor: 2
});
const page = await context.newPage();
page.on('console', (msg) => {

View file

@ -0,0 +1,248 @@
#!/bin/bash
# test-playground-actions.sh — Playground v3 ACTIONS handler-state-effekter
#
# Verifiserer at de 6 pure-state-ACTIONS-handlerne (project-select-artifact,
# project-show-overview, import-open, import-close, artifact-reimport,
# artifact-delete) muterer state korrekt.
#
# Handlerne import-detect og import-save krever document/DOM og dekkes ikke
# her — de testes implisitt via browser-walkthrough før release og via
# manuell QA per playground/MANUAL-CHECKLIST.md.
#
# Bash 3.2-kompatibel. Bruker node til JS-eval. Ingen npm-deps.
set -euo pipefail
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HTML_FILE="$PLUGIN_ROOT/playground/ms-ai-architect-playground.html"
# shellcheck disable=SC1091
source "$PLUGIN_ROOT/tests/lib/e2e-helpers.sh"
init_suite "Playground v3 — ACTIONS handler state-effekter"
if [ ! -f "$HTML_FILE" ]; then
fail "HTML-fila finnes ikke: $HTML_FILE"
print_summary; exit 1
fi
pass "HTML-fil finnes: $(basename "$HTML_FILE")"
NODE_OUT=$(node -e '
const fs = require("fs");
const htmlPath = process.argv[1];
const htmlSrc = fs.readFileSync(htmlPath, "utf8");
// Ekstraher ACTIONS-blokken fra "// PROJECT-VIEW V2 ACTIONS" (sesjon 3-kommentaren)
// til starten av smart-detect-blokken. Tar med projectViewUiState + 6 handlere.
const startMarker = "// PROJECT-VIEW V2 ACTIONS (Sesjon 3)";
const endMarker = "// Smart-detect på textarea-input i import-modal.";
const startIdx = htmlSrc.indexOf(startMarker);
const endIdx = htmlSrc.indexOf(endMarker);
if (startIdx < 0 || endIdx < 0) {
console.error("MARKERS_MISSING start=" + startIdx + " end=" + endIdx);
process.exit(2);
}
const block = htmlSrc.substring(startIdx, endIdx);
// Stubs som gir handlerne et minimums-miljø uten document/window.
const stubs = `
let __store_state = null;
const store = {
get state() { return __store_state; },
save: function () {}
};
function setStoreState(s) { __store_state = s; }
function findProject(id) {
const list = (store.state && store.state.projects) || [];
for (let i = 0; i < list.length; i++) if (list[i].id === id) return list[i];
return null;
}
let __renderCount = 0;
function scheduleRender() { __renderCount++; }
let __confirmAnswer = true;
function confirm() { return __confirmAnswer; }
function setConfirmAnswer(b) { __confirmAnswer = b; }
function renderCount() { return __renderCount; }
function resetRenderCount() { __renderCount = 0; }
const ACTIONS = {};
`;
const wrapped = stubs + block + "\nreturn { ACTIONS, setStoreState, setConfirmAnswer, renderCount, resetRenderCount, projectViewUiState };";
let api;
try {
api = (new Function(wrapped))();
} catch (e) {
console.error("EVAL_FAILED: " + e.message);
process.exit(3);
}
function emit(ok, desc) { console.log((ok ? "PASS" : "FAIL") + "\t" + desc); }
function freshState(opts) {
const o = opts || {};
return {
schemaVersion: 1,
dataVersion: 3,
activeProjectId: "p1",
projects: [{
id: "p1",
name: "Demo",
artifacts: {
classify: { commandId: "classify", raw_markdown: "RAW_CLASSIFY", parsed: { risk_level: "minimal" }, verdict: "go", keyStats: [], importedAt: "x", updatedAt: "x" },
ros: { commandId: "ros", raw_markdown: "RAW_ROS", parsed: { threats: [] }, verdict: "approved", keyStats: [], importedAt: "x", updatedAt: "x" }
},
reports: {
classify: { raw_markdown: "RAW_CLASSIFY", parsed: { risk_level: "minimal" } },
ros: { raw_markdown: "RAW_ROS", parsed: { threats: [] } }
}
}],
ui: o.ui || {}
};
}
// ---- API ----
emit(typeof api.ACTIONS === "object" && api.ACTIONS !== null, "ACTIONS-objekt eksponert");
emit(typeof api.ACTIONS["project-select-artifact"] === "function", "project-select-artifact handler finnes");
emit(typeof api.ACTIONS["project-show-overview"] === "function", "project-show-overview handler finnes");
emit(typeof api.ACTIONS["import-open"] === "function", "import-open handler finnes");
emit(typeof api.ACTIONS["import-close"] === "function", "import-close handler finnes");
emit(typeof api.ACTIONS["artifact-reimport"] === "function", "artifact-reimport handler finnes");
emit(typeof api.ACTIONS["artifact-delete"] === "function", "artifact-delete handler finnes");
// ---- project-select-artifact ----
(function () {
const state = freshState();
api.setStoreState(state);
api.resetRenderCount();
api.ACTIONS["project-select-artifact"]({}, { dataset: { artifactId: "classify" } });
const ok = state.ui.projectView && state.ui.projectView.selectedArtifactId === "classify"
&& api.renderCount() === 1;
emit(ok, "project-select-artifact setter selectedArtifactId og trigger scheduleRender (got=" + (state.ui.projectView && state.ui.projectView.selectedArtifactId) + ")");
})();
// ---- project-select-artifact uten artifactId — no-op ----
(function () {
const state = freshState({ ui: { projectView: { selectedArtifactId: "ros", searchQuery: "" }, importModal: { open: false, prefillCommandId: "", prefillMarkdown: "" } } });
api.setStoreState(state);
api.resetRenderCount();
api.ACTIONS["project-select-artifact"]({}, { dataset: {} });
const ok = state.ui.projectView.selectedArtifactId === "ros" && api.renderCount() === 0;
emit(ok, "project-select-artifact uten dataset.artifactId → no-op (selectedArtifactId beholdt, ingen render)");
})();
// ---- project-show-overview ----
(function () {
const state = freshState({ ui: { projectView: { selectedArtifactId: "classify", searchQuery: "" }, importModal: { open: false, prefillCommandId: "", prefillMarkdown: "" } } });
api.setStoreState(state);
api.resetRenderCount();
api.ACTIONS["project-show-overview"]({}, {});
const ok = state.ui.projectView.selectedArtifactId === null && api.renderCount() === 1;
emit(ok, "project-show-overview tømmer selectedArtifactId");
})();
// ---- import-open med prefill-command (eksisterende artifact gir prefillMarkdown) ----
(function () {
const state = freshState();
api.setStoreState(state);
api.resetRenderCount();
api.ACTIONS["import-open"]({}, { dataset: { prefillCommand: "ros" } });
const m = state.ui.importModal;
const ok = m && m.open === true && m.prefillCommandId === "ros"
&& m.prefillMarkdown === "RAW_ROS" && api.renderCount() === 1;
emit(ok, "import-open med prefillCommand=\"ros\" åpner modal + prefyller raw_markdown");
})();
// ---- import-open uten prefill-command ----
(function () {
const state = freshState();
api.setStoreState(state);
api.resetRenderCount();
api.ACTIONS["import-open"]({}, { dataset: {} });
const m = state.ui.importModal;
const ok = m && m.open === true && m.prefillCommandId === "" && m.prefillMarkdown === "";
emit(ok, "import-open uten prefill → modal åpnet, ingen prefyll");
})();
// ---- import-close ----
(function () {
const state = freshState({ ui: { projectView: { selectedArtifactId: null, searchQuery: "" }, importModal: { open: true, prefillCommandId: "ros", prefillMarkdown: "RAW_ROS" } } });
api.setStoreState(state);
api.resetRenderCount();
api.ACTIONS["import-close"]({}, {});
const m = state.ui.importModal;
const ok = m.open === false && m.prefillCommandId === "" && m.prefillMarkdown === "" && api.renderCount() === 1;
emit(ok, "import-close tilbakestiller modal-state");
})();
// ---- artifact-reimport prefyller modal med eksisterende markdown ----
(function () {
const state = freshState();
api.setStoreState(state);
api.resetRenderCount();
api.ACTIONS["artifact-reimport"]({}, { dataset: { command: "classify" } });
const m = state.ui.importModal;
const ok = m.open === true && m.prefillCommandId === "classify" && m.prefillMarkdown === "RAW_CLASSIFY";
emit(ok, "artifact-reimport åpner modal med prefill fra eksisterende artifact");
})();
// ---- artifact-delete med confirm=false → no-op ----
(function () {
const state = freshState();
api.setStoreState(state);
api.setConfirmAnswer(false);
api.resetRenderCount();
api.ACTIONS["artifact-delete"]({}, { dataset: { command: "classify" } });
const ok = state.projects[0].artifacts.classify !== undefined && api.renderCount() === 0;
emit(ok, "artifact-delete med confirm=false → artifact bevart, ingen render");
})();
// ---- artifact-delete med confirm=true → sletter + clearer selectedArtifactId ----
(function () {
const state = freshState({ ui: { projectView: { selectedArtifactId: "classify", searchQuery: "" }, importModal: { open: false, prefillCommandId: "", prefillMarkdown: "" } } });
api.setStoreState(state);
api.setConfirmAnswer(true);
api.resetRenderCount();
api.ACTIONS["artifact-delete"]({}, { dataset: { command: "classify" } });
const p = state.projects[0];
const ok = p.artifacts.classify === undefined && p.reports.classify === undefined
&& state.ui.projectView.selectedArtifactId === null && api.renderCount() === 1;
emit(ok, "artifact-delete med confirm=true sletter artifact + reports + clearer selectedArtifactId");
})();
// ---- artifact-delete bevarer andre artifacts ----
(function () {
const state = freshState();
api.setStoreState(state);
api.setConfirmAnswer(true);
api.ACTIONS["artifact-delete"]({}, { dataset: { command: "ros" } });
const p = state.projects[0];
const ok = p.artifacts.classify !== undefined && p.artifacts.ros === undefined;
emit(ok, "artifact-delete sletter bare den ene — andre artifacts bevart");
})();
// ---- projectViewUiState initialiserer state-grener idempotent ----
(function () {
const state = { projects: [], ui: {} };
api.setStoreState(state);
const ui1 = api.projectViewUiState();
const ui2 = api.projectViewUiState();
const ok = ui1 === ui2
&& ui1.projectView && ui1.projectView.selectedArtifactId === null && ui1.projectView.searchQuery === ""
&& ui1.importModal && ui1.importModal.open === false;
emit(ok, "projectViewUiState initialiserer projectView + importModal idempotent");
})();
' "$HTML_FILE" 2>&1) || NODE_RC=$?
if [ "${NODE_RC:-0}" -ne 0 ]; then
fail "node-eval feilet (rc=${NODE_RC:-0}): $NODE_OUT"
print_summary; exit 1
fi
while IFS=$'\t' read -r status desc; do
case "$status" in
PASS) pass "$desc" ;;
FAIL) fail "$desc" ;;
WARN) warn "$desc" ;;
esac
done <<< "$NODE_OUT"
print_summary

View file

@ -0,0 +1,218 @@
#!/bin/bash
# test-playground-fingerprints.sh — Playground v3 inferCommandIdFromMarkdown
#
# Verifiserer:
# 1. PROJECT_VIEW_V2_BEGIN/END-markørene finnes
# 2. inferCommandIdFromMarkdown matcher hver av 17 test-fixtures mot
# egen commandId med confidence >= 0.6 (true-positive matrise)
# 3. False-positive immunity:
# - tom streng → null
# - 100 tegn lorem ipsum → null
# - kun "# Hello"-header → null
# - mixed content med kun classify-header → matcher classify
# 4. Sanity-asserts på COMMAND_FINGERPRINTS-shape og fingerprintScore-API
#
# Bash 3.2-kompatibel. Bruker node til JS-eval; ingen npm-deps.
set -euo pipefail
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HTML_FILE="$PLUGIN_ROOT/playground/ms-ai-architect-playground.html"
FIXTURE_DIR="$PLUGIN_ROOT/playground/test-fixtures"
# shellcheck disable=SC1091
source "$PLUGIN_ROOT/tests/lib/e2e-helpers.sh"
init_suite "Playground v3 — inferCommandIdFromMarkdown fingerprints"
# ---- 1. Filer eksisterer ----
if [ ! -f "$HTML_FILE" ]; then
fail "HTML-fila finnes ikke: $HTML_FILE"
print_summary; exit 1
fi
pass "HTML-fil finnes: $(basename "$HTML_FILE")"
if [ ! -d "$FIXTURE_DIR" ]; then
fail "Fixture-mappe mangler: $FIXTURE_DIR"
print_summary; exit 1
fi
pass "Fixture-mappe finnes"
# ---- 2. PROJECT_VIEW_V2-markører eksisterer ----
if grep -q "PROJECT_VIEW_V2_BEGIN" "$HTML_FILE" && grep -q "PROJECT_VIEW_V2_END" "$HTML_FILE"; then
pass "PROJECT_VIEW_V2_BEGIN/END markører finnes"
else
fail "Mangler PROJECT_VIEW_V2-markører i HTML"
print_summary; exit 1
fi
# ---- 3. Kjør hele test-matrisen i én node-prosess (effektivt) ----
# Node-skriptet:
# - ekstraherer PROJECT_VIEW_V2-blokken
# - stubber dependencies
# - leser alle 17 fixture-filer
# - kjører true-positive + anti-match + sanity-tester
# - skriver én linje per assert: "PASS|FAIL <description>"
NODE_OUT=$(node -e '
const fs = require("fs");
const path = require("path");
const htmlPath = process.argv[1];
const fixtureDir = process.argv[2];
const html = fs.readFileSync(htmlPath, "utf8");
const beginMarker = "// === PROJECT_VIEW_V2_BEGIN ===";
const endMarker = "// === PROJECT_VIEW_V2_END ===";
const beginIdx = html.indexOf(beginMarker);
const endIdx = html.indexOf(endMarker);
if (beginIdx < 0 || endIdx < 0) {
console.error("MARKER_MISSING");
process.exit(2);
}
const block = html.substring(beginIdx, endIdx + endMarker.length);
// 17 produces_report-renderers per PROJECT_VIEW_CONFIG.renderers.
const RENDERER_IDS = ["renderAiActPyramid","renderRequirements","renderTransparency","renderFria","renderConformity","renderDpia","renderSecurity","renderRos","renderReview","renderCost","renderLicense","renderMigrate","renderAdr","renderSummary","renderPoc","renderUtredning","renderCompare"];
const rendererStubs = RENDERER_IDS.map(function (n) { return n + ": function () {}"; }).join(", ");
// CATALOG.commands: 17 produces_report=true entries med id, label, category, renderer.
const COMMANDS = [
{ id: "classify", category: "regulatory", label: "EU AI Act — Klassifisering", renderer: "renderAiActPyramid", produces_report: true, report_archetype: "aiact" },
{ id: "requirements", category: "regulatory", label: "EU AI Act — Krav per risiko", renderer: "renderRequirements", produces_report: true, report_archetype: "requirements-list" },
{ id: "transparency", category: "regulatory", label: "Transparensnotis (Art. 13/50)", renderer: "renderTransparency", produces_report: true, report_archetype: "text-document" },
{ id: "frimpact", category: "regulatory", label: "FRIA (Art. 27)", renderer: "renderFria", produces_report: true, report_archetype: "fria" },
{ id: "conformity", category: "regulatory", label: "Samsvarsvurdering (Art. 43)", renderer: "renderConformity", produces_report: true, report_archetype: "conformity-checklist" },
{ id: "dpia", category: "regulatory", label: "DPIA / PVK", renderer: "renderDpia", produces_report: true, report_archetype: "matrix-risk" },
{ id: "security", category: "security", label: "Sikkerhetsvurdering (6×5)", renderer: "renderSecurity", produces_report: true, report_archetype: "matrix-risk-6x5" },
{ id: "ros", category: "security", label: "ROS-analyse", renderer: "renderRos", produces_report: true, report_archetype: "matrix-risk" },
{ id: "review", category: "security", label: "Arkitekturgjennomgang", renderer: "renderReview", produces_report: true, report_archetype: "findings" },
{ id: "cost", category: "economy", label: "Kostnadsestimat", renderer: "renderCost", produces_report: true, report_archetype: "cost-distribution" },
{ id: "license", category: "economy", label: "Lisenskartlegging", renderer: "renderLicense", produces_report: true, report_archetype: "scenario-comparison" },
{ id: "migrate", category: "economy", label: "Migrasjonsplan", renderer: "renderMigrate", produces_report: true, report_archetype: "phase-plan" },
{ id: "adr", category: "documentation", label: "ADR", renderer: "renderAdr", produces_report: true, report_archetype: "adr" },
{ id: "summary", category: "documentation", label: "Beslutningsnotat", renderer: "renderSummary", produces_report: true, report_archetype: "verdict" },
{ id: "poc", category: "documentation", label: "POC-plan", renderer: "renderPoc", produces_report: true, report_archetype: "phase-plan" },
{ id: "utredning", category: "documentation", label: "Utredning", renderer: "renderUtredning", produces_report: true, report_archetype: "utredning" },
{ id: "compare", category: "documentation", label: "Plattformsammenligning", renderer: "renderCompare", produces_report: true, report_archetype: "scenario-comparison" }
];
const stubs = `
const window = {};
function escapeHtml(s) { return String(s == null ? "" : s); }
function escapeAttr(s) { return escapeHtml(s); }
function renderPageShell(opts, body) { return "<header>" + (opts && opts.title || "") + "</header>" + (body || ""); }
function renderVerdictPill(v) { return "<span class=\\"verdict-pill\\" data-verdict=\\"" + v + "\\">" + v + "</span>"; }
function renderKeyStatsGrid(s) { return "<div class=\\"key-stats\\">" + (s && s.length || 0) + "</div>"; }
function inferVerdict() { return "n-a"; }
function inferKeyStats() { return []; }
const PARSERS = {};
const RENDERERS = { ` + rendererStubs + ` };
const CATALOG = { commands: ` + JSON.stringify(COMMANDS) + ` };
const ACTIONS = {};
const store = { state: { ui: {}, projects: [], activeProjectId: null }, save: function () {} };
function findProject() { return null; }
function scheduleRender() {}
`;
const wrapped = stubs + block + "\nreturn { inferCommandIdFromMarkdown, fingerprintScore, COMMAND_FINGERPRINTS };";
let api;
try {
api = (new Function(wrapped))();
} catch (e) {
console.error("EVAL_FAILED: " + e.message);
process.exit(3);
}
function emit(ok, desc) {
console.log((ok ? "PASS" : "FAIL") + "\t" + desc);
}
// ---- Sanity-asserts (5) ----
emit(typeof api.inferCommandIdFromMarkdown === "function", "inferCommandIdFromMarkdown er funksjon");
emit(typeof api.fingerprintScore === "function", "fingerprintScore er funksjon");
emit(typeof api.COMMAND_FINGERPRINTS === "object" && api.COMMAND_FINGERPRINTS !== null, "COMMAND_FINGERPRINTS er objekt");
const cfKeys = Object.keys(api.COMMAND_FINGERPRINTS);
emit(cfKeys.length === 17, "COMMAND_FINGERPRINTS har 17 entries (got " + cfKeys.length + ")");
let allShapeOk = true;
for (const k of cfKeys) {
const v = api.COMMAND_FINGERPRINTS[k];
if (!v || !Array.isArray(v.headers) || !Array.isArray(v.keywords)) {
allShapeOk = false; break;
}
}
emit(allShapeOk, "Hver fingerprint har headers[] og keywords[]");
// ---- True-positive matrise (17 fixtures) ----
//
// Kjente fingerprint/fixture-gap (oppdaget av denne testen, fix i sesjon 5):
// - requirements.md har header "# EU AI Act — Krav for høyrisiko" som ikke
// matcher /^\s*#\s*(AI\s*Act-?krav|Krav per|Requirements)/i. Annex IV-
// omtale i tabellen gjør at conformity vinner med 0.76.
// - license.md har header "# Lisens-kapabilitetsmatrise" som ikke matcher
// /^\s*#\s*(Lisens(kart)?legging|License\s*Mapping)/i.
// v1.15.0 (sesjon 5): begge gap-er lukket — requirements.headers og
// license.headers utvidet i COMMAND_FINGERPRINTS. KNOWN_GAP_FIXTURES tømt.
const KNOWN_GAP_FIXTURES = {};
const expectedIds = ["classify","requirements","transparency","frimpact","conformity","dpia","security","ros","review","cost","license","migrate","adr","summary","poc","utredning","compare"];
for (const cid of expectedIds) {
const fxPath = path.join(fixtureDir, cid + ".md");
let text = "";
try { text = fs.readFileSync(fxPath, "utf8"); } catch (e) {
emit(false, "fixture " + cid + ".md kunne ikke leses: " + e.message);
continue;
}
const r = api.inferCommandIdFromMarkdown(text, {});
const matchedCid = r && r.commandId;
const conf = r && r.confidence;
const ok = r && r.commandId === cid && r.confidence >= 0.6;
if (ok) {
emit(true, "fixture " + cid + ".md → " + matchedCid + " conf=" + conf.toFixed(2));
} else if (KNOWN_GAP_FIXTURES[cid]) {
console.log("WARN\tfixture " + cid + ".md → " + (matchedCid || "null") +
" (KNOWN_GAP — fingerprint dekker ikke fixture-header; fix i sesjon 5)");
} else {
emit(false, "fixture " + cid + ".md → " + (matchedCid || "null") +
(conf != null ? " conf=" + conf.toFixed(2) : ""));
}
}
// ---- Anti-match (4) ----
emit(api.inferCommandIdFromMarkdown("", {}) === null,
"tom streng → null");
emit(api.inferCommandIdFromMarkdown(null, {}) === null,
"null input → null");
const lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim";
emit(api.inferCommandIdFromMarkdown(lorem, {}) === null,
"100 tegn lorem ipsum → null (got " + JSON.stringify(api.inferCommandIdFromMarkdown(lorem, {})) + ")");
emit(api.inferCommandIdFromMarkdown("# Hello\n\nIngen relevant tekst.", {}) === null,
"kun \"# Hello\"-header → null");
// ---- Mixed content: dominant header vinner ----
const mixed = "# EU AI Act — Klassifisering\n\nDette inneholder også owasp og prompt injection-referanser men headeren er klassifisering.";
const mixedR = api.inferCommandIdFromMarkdown(mixed, {});
emit(mixedR !== null && mixedR.commandId === "classify",
"mixed content med classify-header → " + (mixedR && mixedR.commandId));
// ---- fingerprintScore-API direkte ----
emit(api.fingerprintScore("any text", null) === 0,
"fingerprintScore(null spec) === 0");
emit(api.fingerprintScore("", api.COMMAND_FINGERPRINTS.classify) === 0,
"fingerprintScore(tom tekst) === 0");
' "$HTML_FILE" "$FIXTURE_DIR" 2>&1) || NODE_RC=$?
if [ "${NODE_RC:-0}" -ne 0 ]; then
fail "node-eval feilet (rc=${NODE_RC:-0}): $NODE_OUT"
print_summary; exit 1
fi
# Parse PASS/FAIL/WARN-linjer fra node-output
while IFS=$'\t' read -r status desc; do
case "$status" in
PASS) pass "$desc" ;;
FAIL) fail "$desc" ;;
WARN) warn "$desc" ;;
esac
done <<< "$NODE_OUT"
print_summary

View file

@ -122,8 +122,8 @@ api.migrateDataVersion(stateB, api.defaultArchetypeFor);
const a = JSON.stringify(stateA);
const b = JSON.stringify(stateB);
if (stateA.dataVersion !== 2) {
console.error("DATA_VERSION_NOT_BUMPED");
if (stateA.dataVersion !== 3) {
console.error("DATA_VERSION_NOT_BUMPED got=" + stateA.dataVersion);
process.exit(4);
}
@ -141,8 +141,22 @@ for (const p of (stateA.projects || [])) {
if (verdictsAdded === 0) { console.error("NO_VERDICTS_ADDED"); process.exit(5); }
if (statsAdded === 0) { console.error("NO_KEYSTATS_ADDED"); process.exit(6); }
// Sesjon 3: v2→v3-migrasjon må produsere project.artifacts med v3-shape.
let artifactsBuilt = 0;
for (const p of (stateA.projects || [])) {
if (!p.artifacts) continue;
for (const id of Object.keys(p.artifacts)) {
const a = p.artifacts[id];
if (a && a.commandId === id && typeof a.raw_markdown === "string"
&& a.importedAt && a.updatedAt) {
artifactsBuilt++;
}
}
}
if (artifactsBuilt === 0) { console.error("NO_ARTIFACTS_BUILT"); process.exit(8); }
if (a === b) {
console.log("IDEMPOTENT verdicts=" + verdictsAdded + " stats=" + statsAdded);
console.log("IDEMPOTENT verdicts=" + verdictsAdded + " stats=" + statsAdded + " artifacts=" + artifactsBuilt);
} else {
console.error("NOT_IDEMPOTENT");
process.exit(7);
@ -155,7 +169,7 @@ else
fail "Idempotency-test feilet: $IDEMPOTENCY_RESULT"
fi
# ---- 6. dataVersion bumpes til 2 ved første kjøring ----
# ---- 6. dataVersion bumpes til 3 ved første kjøring (v?→v3 kjede) ----
DV_RESULT=$(node -e '
const fs = require("fs");
const html = fs.readFileSync(process.argv[1], "utf8");
@ -176,10 +190,256 @@ api.migrateDataVersion(state, api.defaultArchetypeFor);
console.log(state.dataVersion);
' "$HTML_FILE" 2>&1) || true
if [ "$DV_RESULT" = "2" ]; then
pass "dataVersion bumpes til 2"
if [ "$DV_RESULT" = "3" ]; then
pass "dataVersion bumpes til 3 (kjede v?→v3)"
else
fail "dataVersion ble ikke bumpet til 2 (got '$DV_RESULT')"
fail "dataVersion ble ikke bumpet til 3 (got '$DV_RESULT')"
fi
# ---- 7. v2→v3 produserer artifacts uten å miste reports ----
V3_SHAPE=$(node -e '
const fs = require("fs");
const html = fs.readFileSync(process.argv[1], "utf8");
const begin = html.indexOf("// === V2_FOUNDATION_BEGIN ===");
const end = html.indexOf("// === V2_FOUNDATION_END ===");
const block = html.substring(begin, end + 32);
const stubs = `
const window = {};
function escapeHtml(s) { return String(s == null ? "" : s); }
function escapeAttr(s) { return escapeHtml(s); }
const CATALOG = { commands: [
{ id: "classify", report_archetype: "aiact" }
]};
`;
const api = (new Function(stubs + block + "\nreturn { migrateDataVersion, defaultArchetypeFor };"))();
// Bygg en v2-state med ett prosjekt og én report som har parsed-data
const state = {
schemaVersion: 1,
dataVersion: 2,
projects: [{
id: "p1",
name: "Test",
createdAt: "2026-05-15T00:00:00.000Z",
reports: {
classify: { raw_markdown: "# klassifisering", parsed: { risk_level: "minimal", verdict: "go", keyStats: [] } }
}
}]
};
api.migrateDataVersion(state, api.defaultArchetypeFor);
const p = state.projects[0];
const hasReports = !!(p.reports && p.reports.classify);
const hasArtifacts = !!(p.artifacts && p.artifacts.classify);
const a = p.artifacts && p.artifacts.classify;
const shapeOk = a && a.commandId === "classify" && a.raw_markdown === "# klassifisering"
&& a.parsed && a.parsed.risk_level === "minimal" && a.verdict === "go"
&& Array.isArray(a.keyStats) && typeof a.importedAt === "string"
&& typeof a.updatedAt === "string";
console.log(JSON.stringify({ dataVersion: state.dataVersion, hasReports, hasArtifacts, shapeOk }));
' "$HTML_FILE" 2>&1) || true
if echo "$V3_SHAPE" | grep -q '"dataVersion":3'; then
pass "v2→v3 setter dataVersion=3"
else
fail "v2→v3 setter ikke dataVersion=3 ($V3_SHAPE)"
fi
if echo "$V3_SHAPE" | grep -q '"hasReports":true'; then
pass "v2→v3 bevarer project.reports (bakover-kompat v1.15.0)"
else
fail "v2→v3 mistet project.reports ($V3_SHAPE)"
fi
if echo "$V3_SHAPE" | grep -q '"hasArtifacts":true'; then
pass "v2→v3 bygger project.artifacts"
else
fail "v2→v3 bygde ikke project.artifacts ($V3_SHAPE)"
fi
if echo "$V3_SHAPE" | grep -q '"shapeOk":true'; then
pass "v2→v3 artifact-shape ({commandId, raw_markdown, parsed, verdict, keyStats, importedAt, updatedAt})"
else
fail "v2→v3 artifact-shape mismatch ($V3_SHAPE)"
fi
# ---- 8. v2→v3 kant-case-tester (sesjon 4) ----
# Tomt prosjekt, manglende reports, blandet state, idempotens-mutasjon.
EDGE_RESULT=$(node -e '
const fs = require("fs");
const html = fs.readFileSync(process.argv[1], "utf8");
const begin = html.indexOf("// === V2_FOUNDATION_BEGIN ===");
const end = html.indexOf("// === V2_FOUNDATION_END ===");
const block = html.substring(begin, end + 32);
const stubs = `
const window = {};
function escapeHtml(s) { return String(s == null ? "" : s); }
function escapeAttr(s) { return escapeHtml(s); }
const CATALOG = { commands: [
{ id: "classify", report_archetype: "aiact" },
{ id: "ros", report_archetype: "matrix-risk" },
{ id: "cost", report_archetype: "cost-distribution" },
{ id: "summary", report_archetype: "verdict" }
]};
`;
const api = (new Function(stubs + block + "\nreturn { migrateDataVersion, defaultArchetypeFor };"))();
function emit(key, ok, info) {
console.log("EDGE\t" + key + "\t" + (ok ? "PASS" : "FAIL") + "\t" + (info || ""));
}
// Case A: tomt prosjekt (reports={}).
(function () {
const state = {
schemaVersion: 1,
dataVersion: 2,
projects: [{ id: "pA", name: "Empty", createdAt: "2026-05-15T00:00:00.000Z", reports: {} }]
};
api.migrateDataVersion(state, api.defaultArchetypeFor);
const p = state.projects[0];
const ok = p.artifacts !== undefined && typeof p.artifacts === "object"
&& Object.keys(p.artifacts).length === 0
&& state.dataVersion === 3;
emit("empty-project", ok, "artifacts=" + JSON.stringify(p.artifacts) + " dv=" + state.dataVersion);
})();
// Case B: prosjekt uten reports-felt i det hele tatt.
(function () {
const state = {
schemaVersion: 1,
dataVersion: 2,
projects: [{ id: "pB", name: "NoReports", createdAt: "2026-05-15T00:00:00.000Z" }]
};
let threw = false;
try {
api.migrateDataVersion(state, api.defaultArchetypeFor);
} catch (e) { threw = true; }
const p = state.projects[0];
const ok = !threw && p.artifacts !== undefined && typeof p.artifacts === "object"
&& Object.keys(p.artifacts).length === 0;
emit("no-reports-field", ok, "threw=" + threw + " artifacts=" + JSON.stringify(p.artifacts));
})();
// Case C: blandet state — artifacts har 2 entries fra før, reports har 5.
// Migrering skal legge til de 3 manglende uten å overskrive eksisterende.
(function () {
const preNow = "2026-04-01T00:00:00.000Z";
const state = {
schemaVersion: 1,
dataVersion: 2,
projects: [{
id: "pC",
name: "Mixed",
createdAt: preNow,
artifacts: {
classify: {
commandId: "classify",
raw_markdown: "EXISTING_CLASSIFY",
parsed: { risk_level: "minimal" },
verdict: "go",
keyStats: [],
importedAt: preNow,
updatedAt: preNow,
_preExisting: true
},
ros: {
commandId: "ros",
raw_markdown: "EXISTING_ROS",
parsed: { threats: [] },
verdict: "approved",
keyStats: [],
importedAt: preNow,
updatedAt: preNow,
_preExisting: true
}
},
reports: {
classify: { raw_markdown: "NEW_CLASSIFY", parsed: { risk_level: "high" } },
ros: { raw_markdown: "NEW_ROS", parsed: { threats: [{ id: "T1" }] } },
cost: { raw_markdown: "NEW_COST", parsed: { p50: 1000 } },
summary: { raw_markdown: "NEW_SUMMARY", parsed: { verdict: "go" } }
}
}]
};
api.migrateDataVersion(state, api.defaultArchetypeFor);
const p = state.projects[0];
const cls = p.artifacts.classify;
const ros = p.artifacts.ros;
const cost = p.artifacts.cost;
const summary = p.artifacts.summary;
// Eksisterende artifacts bevart med _preExisting-flag og opprinnelig raw_markdown.
const classifyPreserved = cls && cls._preExisting === true && cls.raw_markdown === "EXISTING_CLASSIFY";
const rosPreserved = ros && ros._preExisting === true && ros.raw_markdown === "EXISTING_ROS";
// Nye artifacts bygget fra reports.
const costAdded = cost && cost.commandId === "cost" && cost.raw_markdown === "NEW_COST";
const summaryAdded = summary && summary.commandId === "summary" && summary.raw_markdown === "NEW_SUMMARY";
const ok = classifyPreserved && rosPreserved && costAdded && summaryAdded;
emit("mixed-state-merge", ok,
"cls_preserved=" + !!classifyPreserved + " ros_preserved=" + !!rosPreserved +
" cost_added=" + !!costAdded + " summary_added=" + !!summaryAdded);
})();
// Case D: idempotens etter mutasjon — sett _touched=true på en artifact, kjør
// migrasjon på nytt, sjekk at flagget er bevart (ikke overskrevet).
(function () {
const state = {
schemaVersion: 1,
dataVersion: 2,
projects: [{
id: "pD",
name: "Idempotent",
createdAt: "2026-04-01T00:00:00.000Z",
reports: {
classify: { raw_markdown: "# klassifisering", parsed: { risk_level: "minimal" } }
}
}]
};
// Første migrasjon bygger artifact.
api.migrateDataVersion(state, api.defaultArchetypeFor);
const p = state.projects[0];
p.artifacts.classify._touched = true;
p.artifacts.classify.raw_markdown = "MUTATED_AFTER_MIGRATION";
// Re-migrer — idempotent skal ikke overskrive.
api.migrateDataVersion(state, api.defaultArchetypeFor);
const a = state.projects[0].artifacts.classify;
const ok = a._touched === true && a.raw_markdown === "MUTATED_AFTER_MIGRATION";
emit("idempotent-after-mutation", ok,
"_touched=" + a._touched + " raw=" + JSON.stringify(a.raw_markdown));
})();
// Case E: dataVersion=3 fra før — migrasjon er no-op.
(function () {
const state = {
schemaVersion: 1,
dataVersion: 3,
projects: [{
id: "pE",
name: "AlreadyV3",
createdAt: "2026-04-01T00:00:00.000Z",
artifacts: {
cost: { commandId: "cost", raw_markdown: "EXISTING", parsed: { p50: 1 },
verdict: "go", keyStats: [], importedAt: "x", updatedAt: "x" }
}
}]
};
const beforeJson = JSON.stringify(state);
api.migrateDataVersion(state, api.defaultArchetypeFor);
const afterJson = JSON.stringify(state);
emit("already-v3-noop", beforeJson === afterJson, "diff=" + (beforeJson === afterJson ? "none" : "changed"));
})();
' "$HTML_FILE" 2>&1) || true
# Parse EDGE-resultater
while IFS=$'\t' read -r tag key status info; do
[ "$tag" = "EDGE" ] || continue
case "$key" in
empty-project) desc="v3 kant-case: tomt prosjekt (reports={}) → artifacts={} uten å feile" ;;
no-reports-field) desc="v3 kant-case: prosjekt uten reports-felt → artifacts={} opprettet" ;;
mixed-state-merge) desc="v3 kant-case: blandet state — eksisterende artifacts bevart, nye lagt til" ;;
idempotent-after-mutation) desc="v3 kant-case: idempotens etter mutasjon — _touched-flag bevart" ;;
already-v3-noop) desc="v3 kant-case: state allerede v3 → migrasjon er no-op" ;;
*) desc="v3 kant-case: $key" ;;
esac
if [ "$status" = "PASS" ]; then
pass "$desc"
else
fail "$desc ($info)"
fi
done <<< "$EDGE_RESULT"
print_summary

View file

@ -0,0 +1,323 @@
#!/bin/bash
# test-playground-projectview.sh — Playground v3 renderProjectView integration
#
# Verifiserer at renderProjectView + sub-renderers produserer korrekt HTML
# (som strenger — ingen DOM-deps) mot en inline demo-state-snippet.
#
# Dekker 4 view-tilstander:
# - overview (selectedArtifactId null)
# - artifact (filled) (selectedArtifactId = 'classify')
# - empty (sidebar-treff) (selectedArtifactId = 'frimpact' — mangler)
# - import-modal-open (top-state, orthogonal)
#
# Pluss:
# - renderArtifactNav-søk filtrerer
# - renderProjectOverview har 4 verdict-tiles, top-risks, next-actions, missing
# - renderImportModal har 17 dropdown-options + prefill
#
# Bash 3.2-kompatibel. Bruker node til JS-eval. Ingen npm-deps.
set -euo pipefail
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HTML_FILE="$PLUGIN_ROOT/playground/ms-ai-architect-playground.html"
# shellcheck disable=SC1091
source "$PLUGIN_ROOT/tests/lib/e2e-helpers.sh"
init_suite "Playground v3 — renderProjectView integration"
if [ ! -f "$HTML_FILE" ]; then
fail "HTML-fila finnes ikke: $HTML_FILE"
print_summary; exit 1
fi
pass "HTML-fil finnes: $(basename "$HTML_FILE")"
if grep -q "PROJECT_VIEW_V2_BEGIN" "$HTML_FILE" && grep -q "PROJECT_VIEW_V2_END" "$HTML_FILE"; then
pass "PROJECT_VIEW_V2_BEGIN/END markører finnes"
else
fail "Mangler PROJECT_VIEW_V2-markører i HTML"
print_summary; exit 1
fi
NODE_OUT=$(node -e '
const fs = require("fs");
const htmlPath = process.argv[1];
const htmlSrc = fs.readFileSync(htmlPath, "utf8");
const beginMarker = "// === PROJECT_VIEW_V2_BEGIN ===";
const endMarker = "// === PROJECT_VIEW_V2_END ===";
const beginIdx = htmlSrc.indexOf(beginMarker);
const endIdx = htmlSrc.indexOf(endMarker);
if (beginIdx < 0 || endIdx < 0) {
console.error("MARKER_MISSING"); process.exit(2);
}
const block = htmlSrc.substring(beginIdx, endIdx + endMarker.length);
// 17 produces_report-renderers per PROJECT_VIEW_CONFIG.renderers.
// Hver stub returnerer en deterministisk HTML-streng vi kan asserte mot.
const RENDERER_IDS = ["renderAiActPyramid","renderRequirements","renderTransparency","renderFria","renderConformity","renderDpia","renderSecurity","renderRos","renderReview","renderCost","renderLicense","renderMigrate","renderAdr","renderSummary","renderPoc","renderUtredning","renderCompare"];
const rendererStubs = RENDERER_IDS.map(function (n) {
return n + ": function (data, slot) { if (slot) slot.innerHTML = \"<div class=\\\"stub-\" + " + JSON.stringify(n) + " + \"\\\">\" + (data && data.title || \"stub\") + \"</div>\"; }";
}).join(", ");
const COMMANDS = [
{ id: "classify", category: "regulatory", label: "EU AI Act — Klassifisering", description: "EU AI Act klass.", renderer: "renderAiActPyramid", produces_report: true, report_archetype: "aiact" },
{ id: "requirements", category: "regulatory", label: "EU AI Act — Krav", description: "Krav per risiko", renderer: "renderRequirements", produces_report: true, report_archetype: "requirements-list" },
{ id: "transparency", category: "regulatory", label: "Transparensnotis (Art. 13/50)", description: "Art. 13/50", renderer: "renderTransparency", produces_report: true, report_archetype: "text-document" },
{ id: "frimpact", category: "regulatory", label: "FRIA (Art. 27)", description: "FRIA", renderer: "renderFria", produces_report: true, report_archetype: "fria" },
{ id: "conformity", category: "regulatory", label: "Samsvarsvurdering (Art. 43)", description: "Annex IV", renderer: "renderConformity", produces_report: true, report_archetype: "conformity-checklist" },
{ id: "dpia", category: "regulatory", label: "DPIA / PVK", description: "PVK", renderer: "renderDpia", produces_report: true, report_archetype: "matrix-risk" },
{ id: "security", category: "security", label: "Sikkerhetsvurdering (6×5)", description: "6×5 scoring", renderer: "renderSecurity", produces_report: true, report_archetype: "matrix-risk-6x5" },
{ id: "ros", category: "security", label: "ROS-analyse", description: "NS 5814", renderer: "renderRos", produces_report: true, report_archetype: "matrix-risk" },
{ id: "review", category: "security", label: "Arkitekturgjennomgang", description: "Digdir/NSM", renderer: "renderReview", produces_report: true, report_archetype: "findings" },
{ id: "cost", category: "economy", label: "Kostnadsestimat", description: "P10/P50/P90 NOK", renderer: "renderCost", produces_report: true, report_archetype: "cost-distribution" },
{ id: "license", category: "economy", label: "Lisenskartlegging", description: "M365-lisenser", renderer: "renderLicense", produces_report: true, report_archetype: "scenario-comparison" },
{ id: "migrate", category: "economy", label: "Migrasjonsplan", description: "Fase-plan", renderer: "renderMigrate", produces_report: true, report_archetype: "phase-plan" },
{ id: "adr", category: "documentation", label: "ADR", description: "MADR v3.0", renderer: "renderAdr", produces_report: true, report_archetype: "adr" },
{ id: "summary", category: "documentation", label: "Beslutningsnotat", description: "Sammendrag", renderer: "renderSummary", produces_report: true, report_archetype: "verdict" },
{ id: "poc", category: "documentation", label: "POC-plan", description: "Suksesskriterier", renderer: "renderPoc", produces_report: true, report_archetype: "phase-plan" },
{ id: "utredning", category: "documentation", label: "Utredning", description: "Utredningsinstr.", renderer: "renderUtredning", produces_report: true, report_archetype: "utredning" },
{ id: "compare", category: "documentation", label: "Plattformsammenligning", description: "Plattformer", renderer: "renderCompare", produces_report: true, report_archetype: "scenario-comparison" }
];
// Demo state med 5 fylte artifacts + ui i forskjellige tilstander.
function buildDemoState(opts) {
const o = opts || {};
const now = "2026-05-15T10:00:00.000Z";
return {
dataVersion: 3,
schemaVersion: 1,
activeProjectId: "p1",
projects: [{
id: "p1",
name: "Acme: Kunde-chatbot",
description: "AI-system for objekt-deteksjon i sensordata.",
createdAt: "2026-04-01T08:00:00.000Z",
artifacts: {
classify: {
commandId: "classify",
raw_markdown: "# EU AI Act — Klassifisering",
parsed: { title: "Klassifisering", risk_level: "Høy", role: "Provider og Deployer" },
verdict: "warning",
keyStats: [{ label: "Risikonivå", value: "Høy" }],
importedAt: now, updatedAt: now
},
ros: {
commandId: "ros",
raw_markdown: "# ROS-analyse",
parsed: { title: "ROS", threats: [
{ id: "T1", description: "Modell-bias", severity: "Høy" },
{ id: "T2", description: "Privacy leak", severity: "Kritisk" },
{ id: "T3", description: "Hallusinerte fakta", severity: "Medium" }
]},
verdict: "warning",
keyStats: [],
importedAt: now, updatedAt: now
},
security: {
commandId: "security",
raw_markdown: "# Sikkerhetsvurdering",
parsed: { title: "Security", findings: [
{ id: "S1", finding: "Manglende DLP", severity: "Kritisk" },
{ id: "S2", finding: "Audit ufullstendig", severity: "Høy" }
]},
verdict: "block",
keyStats: [],
importedAt: now, updatedAt: now
},
dpia: {
commandId: "dpia",
raw_markdown: "# DPIA",
parsed: { title: "DPIA", threats: [] },
verdict: "go-with-conditions",
keyStats: [],
importedAt: now, updatedAt: now
},
cost: {
commandId: "cost",
raw_markdown: "# Kostnadsestimat",
parsed: { title: "Kostnad", p10: 78000, p50: 142000, p90: 285000 },
verdict: "approved",
keyStats: [{ label: "P50/mnd", value: "142 000 NOK" }],
importedAt: now, updatedAt: now
}
},
reports: {}
}],
ui: {
projectView: {
selectedArtifactId: o.selectedArtifactId === undefined ? null : o.selectedArtifactId,
searchQuery: o.searchQuery || ""
},
importModal: {
open: !!o.importOpen,
prefillCommandId: o.prefillCommandId || "",
prefillMarkdown: o.prefillMarkdown || ""
}
}
};
}
const stubs = `
const window = {};
function escapeHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function escapeAttr(s) { return escapeHtml(s); }
function renderPageShell(opts, body) {
const t = (opts && opts.title) || "";
const e = (opts && opts.eyebrow) || "";
const v = (opts && opts.verdict) || "";
return "<header class=\\"page__header\\" data-eyebrow=\\"" + escapeAttr(e) + "\\" data-verdict=\\"" + escapeAttr(v) + "\\"><h1>" + escapeHtml(t) + "</h1></header>" + (body || "");
}
function renderVerdictPill(v) {
return "<span class=\\"verdict-pill\\" data-verdict=\\"" + escapeAttr(String(v || "").toLowerCase()) + "\\">" + escapeHtml(String(v || "").toUpperCase()) + "</span>";
}
function renderKeyStatsGrid(s) { return "<div class=\\"key-stats\\">" + (Array.isArray(s) ? s.length : 0) + "</div>"; }
function inferVerdict() { return "n-a"; }
function inferKeyStats() { return []; }
const PARSERS = {};
const RENDERERS = { ` + rendererStubs + ` };
const CATALOG = { commands: ` + JSON.stringify(COMMANDS) + ` };
const ACTIONS = {};
let __STORE_STATE = null;
const store = {
get state() { return __STORE_STATE; },
save: function () {}
};
function setStoreState(s) { __STORE_STATE = s; }
function findProject(id) {
const list = (store.state && store.state.projects) || [];
for (let i = 0; i < list.length; i++) if (list[i].id === id) return list[i];
return null;
}
function scheduleRender() {}
`;
const wrapped = stubs + block + "\nreturn { renderProjectView, renderProjectHeader, renderArtifactNav, renderArtifactNavItem, renderProjectMain, renderProjectOverview, renderProjectArtifact, renderEmptyArtifactPrompt, renderImportModal, PROJECT_VIEW_CONFIG, setStoreState };";
let api;
try {
api = (new Function(wrapped))();
} catch (e) {
console.error("EVAL_FAILED: " + e.message);
process.exit(3);
}
function emit(ok, desc) { console.log((ok ? "PASS" : "FAIL") + "\t" + desc); }
// ---- API-eksponering ----
emit(typeof api.renderProjectView === "function", "renderProjectView er funksjon");
emit(typeof api.renderProjectOverview === "function", "renderProjectOverview er funksjon");
emit(typeof api.renderImportModal === "function", "renderImportModal er funksjon");
emit(typeof api.PROJECT_VIEW_CONFIG === "object" && api.PROJECT_VIEW_CONFIG !== null, "PROJECT_VIEW_CONFIG eksponert");
// ---- View 1: overview (selectedArtifactId null) ----
let demo = buildDemoState({});
api.setStoreState(demo);
let project = demo.projects[0];
let html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG);
emit(html.indexOf("class=\"project-view\"") !== -1 && html.indexOf("data-view=\"overview\"") !== -1,
"overview: .project-view rot med data-view=\"overview\"");
emit(html.indexOf("Acme: Kunde-chatbot") !== -1,
"overview: prosjekt-navn rendres i header");
emit(html.indexOf("data-action=\"import-open\"") !== -1,
"overview: Importer-rapport-knapp finnes");
// Alle 4 kategori-grupper i sidebaren
emit(html.indexOf("Regulatorisk") !== -1 && html.indexOf("Risiko &amp; sikkerhet") !== -1 && html.indexOf("Økonomi") !== -1 && html.indexOf("Dokumentasjon") !== -1,
"overview: alle 4 kategori-labels rendres i nav");
// Overview-tiles (verdict-grid)
emit(html.indexOf("project-overview__verdict-tile") !== -1,
"overview: project-overview__verdict-tile finnes");
const tileCount = (html.match(/project-overview__verdict-tile/g) || []).length;
emit(tileCount >= 4, "overview: minst 4 verdict-tiles (got " + tileCount + ")");
// Top-risks (3 fra ros + 2 fra security = 5)
emit(html.indexOf("top-risks") !== -1 && html.indexOf("Privacy leak") !== -1,
"overview: top-risks-listen inkluderer ROS-trussel");
// Next-actions / missing reports
emit(html.indexOf("project-overview__next-actions") !== -1 || html.indexOf("empty-hint") !== -1,
"overview: next-actions-seksjon rendres");
emit(html.indexOf("project-overview__missing-reports") !== -1 || html.indexOf("Alle må-ha-rapporter er importert") !== -1,
"overview: missing-reports-seksjon rendres");
// ---- View 2: artifact (filled) ----
demo = buildDemoState({ selectedArtifactId: "classify" });
api.setStoreState(demo);
project = demo.projects[0];
html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG);
emit(html.indexOf("data-view=\"artifact\"") !== -1,
"artifact: data-view=\"artifact\"");
emit(html.indexOf("project-view__artifact") !== -1 && html.indexOf("data-artifact=\"classify\"") !== -1,
"artifact: .project-view__artifact med data-artifact=\"classify\"");
emit(html.indexOf("project-view__artifact-title") !== -1 && html.indexOf("EU AI Act — Klassifisering") !== -1,
"artifact: command-label vises i artifact-header");
emit(html.indexOf("data-action=\"artifact-reimport\"") !== -1 && html.indexOf("data-action=\"artifact-delete\"") !== -1,
"artifact: reimport + delete-action-knapper finnes");
// ---- View 3: empty (sidebar-treff uten artifact) ----
demo = buildDemoState({ selectedArtifactId: "frimpact" });
api.setStoreState(demo);
project = demo.projects[0];
html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG);
emit(html.indexOf("data-view=\"empty\"") !== -1,
"empty: data-view=\"empty\"");
emit(html.indexOf("empty-artifact-prompt") !== -1 && html.indexOf("data-command=\"frimpact\"") !== -1,
"empty: empty-artifact-prompt for frimpact");
emit(html.indexOf("data-prefill-command=\"frimpact\"") !== -1,
"empty: Importer-knapp har data-prefill-command=\"frimpact\"");
// ---- View 4: import-modal-open ----
demo = buildDemoState({ importOpen: true, prefillCommandId: "ros", prefillMarkdown: "# ROS-analyse\nDemo" });
api.setStoreState(demo);
project = demo.projects[0];
html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG);
emit(html.indexOf("data-import-modal") !== -1,
"import-modal: [data-import-modal] rendres når open=true");
// Dropdown har 17 options + tom default = 18 <option>-tags
const optionMatches = html.match(/<option /g) || [];
emit(optionMatches.length >= 17,
"import-modal: dropdown har 17+ options (got " + optionMatches.length + ")");
emit(html.indexOf("value=\"ros\" selected") !== -1 || html.indexOf("value=\"ros\" selected") !== -1,
"import-modal: prefillCommandId=\"ros\" gir selected option");
emit(html.indexOf("# ROS-analyse") !== -1 || html.indexOf("ROS-analyse") !== -1,
"import-modal: prefillMarkdown rendres i textarea");
// ---- Modal IKKE rendret når open=false ----
demo = buildDemoState({});
api.setStoreState(demo);
project = demo.projects[0];
html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG);
emit(html.indexOf("data-import-modal") === -1,
"import-modal: ikke rendret når importModal.open=false");
// ---- renderArtifactNav-søk filtrerer ----
demo = buildDemoState({ searchQuery: "ros" });
api.setStoreState(demo);
project = demo.projects[0];
const navHtml = api.renderArtifactNav(project, api.PROJECT_VIEW_CONFIG, null, "ros");
emit(navHtml.indexOf("data-artifact-id=\"ros\"") !== -1,
"nav-filter: ROS-element finnes ved søk=\"ros\"");
emit(navHtml.indexOf("data-artifact-id=\"cost\"") === -1,
"nav-filter: cost-element filtrert ut ved søk=\"ros\"");
// ---- renderProjectView med null project ----
emit(api.renderProjectView(null, api.PROJECT_VIEW_CONFIG).indexOf("Ingen prosjekt valgt") !== -1,
"guard: null project → empty-hint");
' "$HTML_FILE" 2>&1) || NODE_RC=$?
if [ "${NODE_RC:-0}" -ne 0 ]; then
fail "node-eval feilet (rc=${NODE_RC:-0}): $NODE_OUT"
print_summary; exit 1
fi
while IFS=$'\t' read -r status desc; do
case "$status" in
PASS) pass "$desc" ;;
FAIL) fail "$desc" ;;
WARN) warn "$desc" ;;
esac
done <<< "$NODE_OUT"
print_summary

View file

@ -223,20 +223,23 @@ else
fi
# -------------------------------------------------------
# 13. data-report-slot per rapport-produserende command (17 stk)
# 13. v1.15.0: artifact-slot rendres dynamisk via renderProjectArtifact
# -------------------------------------------------------
# v2 hadde per-command data-report-slot="..." på alle 17 cards. v3 har én
# .project-main-zone der renderProjectArtifact mounter den valgte artefakten.
# Sjekk i stedet at RENDERERS routing-objektet er wired for alle 17.
REPORT_CMDS="classify requirements transparency frimpact conformity dpia security ros review cost license migrate adr summary poc utredning compare"
slot_hits=0
renderer_hits=0
for c in $REPORT_CMDS; do
if grep -qE "data-report-slot=[\"']${c}[\"']" "$HTML_FILE"; then
pass "data-report-slot=\"${c}\" markup til stede"
slot_hits=$((slot_hits + 1))
# PROJECT_VIEW_CONFIG.renderers[<cid>] = RENDERERS.renderXxx
if grep -qE "^[[:space:]]+${c}:[[:space:]]+RENDERERS\." "$HTML_FILE"; then
pass "PROJECT_VIEW_CONFIG.renderers.${c} wired"
renderer_hits=$((renderer_hits + 1))
else
warn "data-report-slot=\"${c}\" finnes ikke i statisk markup (kan rendres dynamisk)"
fail "PROJECT_VIEW_CONFIG.renderers.${c} mangler"
fi
done
# Slot rendrer dynamisk via render-funksjoner — warn kun, ingen fail
pass "Report-slot-stikkprøve fullført ($slot_hits/17 statiske; resterende rendres dynamisk)"
pass "v3 renderer-routing wired ($renderer_hits/17)"
# -------------------------------------------------------
# 14. report_archetype-routing-felt i CATALOG-data

View file

@ -1,5 +1,53 @@
# playground-design-system — CHANGELOG
## 0.6.0 — 2026-05-15
### Added — Project-view archetype (Tier 4)
Generic "project as artifact-collection" archetype for plugins where a project owns 0-N read-only report artifacts grouped by category. Default view is an aggregated dashboard; clicking a sidebar item swaps the main panel to the per-artifact render. Edit-mode is paste-import only (no inline editor).
- **New file `components-tier4-project-view.css`** — 11 sections covering:
- `.project-view` + `.project-view__layout` (grid: nav 280px + main 1fr, responsive collapse at 1280 / 960px)
- `.project-view__header` (CSS Grid with eyebrow/title/lede/verdict/key-stats/actions areas)
- `.verdict-pill` (small pill variant — companion to existing `.verdict-pill-lg` in tier2)
- `.project-view__nav` + `.project-view__nav-search` (sticky sidebar with search)
- `.artifact-list` + `__group` / `__group-label` / `__group-count` / `__group-items` / `__item` / `__item-marker` / `__item-body` / `__item-name` / `__item-meta` (grouped, severity-coded sidebar)
- `.artifact-status[data-severity]` (mini-pill: positive | medium | critical)
- `.project-view__main` (main column container)
- `.project-overview` + `__intro` / `__verdict-grid` / `__verdict-tile[data-severity]` / `__section` / `__top-risks` / `__next-actions` / `__missing-reports` (aggregated dashboard)
- `.project-view__artifact` + `__artifact-header` / `__artifact-title` / `__artifact-meta` / `__artifact-actions` / `__artifact-body` (single-rapport viewer wrapper)
- `.empty-artifact-prompt` + `__icon` / `__title` / `__text` / `__actions` (empty-state)
- `.import-modal` + `__backdrop` / `__panel` / `__head` / `__title` / `__close` / `__form` / `__detect` / `__preview` / `__preview-label` / `__footer` (overlay modal for paste-import)
- **6 new tokens in `tokens.css`:**
- `--project-view-nav-width: 280px` — sidebar width at full layout
- `--project-view-collapse-bp: 960px` — doc-only token referenced by responsive breakpoints
- `--artifact-list-item-pad-y: var(--space-2)` — sidebar row vertical padding
- `--artifact-list-item-pad-x: var(--space-3)` — sidebar row horizontal padding
- `--artifact-marker-size: 14px` — sidebar status marker diameter
- `--artifact-marker-border: 1.5px` — sidebar status marker border thickness
### Påvirkning
Endringen er **additiv**: ny komponent-fil + 6 nye tokens, ingen eksisterende selectors eller verdier endres. Plugin-konsumenter (`ms-ai-architect`, `llm-security`, `okr`, `config-audit`, `voyage`) får silent drift mot ny source-commit, men kan re-sync på eget tempo. Bare `ms-ai-architect` og `llm-security` re-syncer i samme commit som denne DS-bumpen (forberedelse til koordinert v1.15.0 / v7.7.0-release etter ~8 sesjoner med JS-implementasjon).
Førsteadoptere: `ms-ai-architect` v1.15.0 (17 artefakter, 5 kategorier) + `llm-security` v7.7.0 (≥18 artefakter, 6 kategorier). State-driven visibility håndteres i plugin-JS, ikke i denne CSS-en — kun aktiv state rendres per pass.
### Plugins som må laste den nye filen
Etter `<link>` til `components-tier3-supplement.css`, legg til:
```html
<link rel="stylesheet" href="vendor/playground-design-system/components-tier4-project-view.css">
```
### For å adoptere v0.6.0
```bash
node scripts/sync-design-system.mjs <plugin-name>
# --force hvis drift detected
```
## 0.5.0 — 2026-05-10
### Added

View file

@ -0,0 +1,665 @@
/* =============================================================================
Playground Design System components-tier4-project-view.css
v0.6.0 Tier 4 project-view archetype
============================================================================
Generic "project as artifact-collection" archetype. Default-view is an
aggregated overview dashboard; clicking a sidebar item swaps main to a
per-artifact render. Tracks 0-N read-only artifacts; edit-mode is paste-
import only (markdown from terminal parser store).
First adopters: ms-ai-architect v1.15.0 (17 artifacts, 5 categories) +
llm-security v7.7.0 (18 artifacts, 6 categories). Each plugin injects a
PROJECT_VIEW_CONFIG object that maps commands renderers, categories,
verdict-aggregators, missing-report heuristics.
The CSS in this file is plugin-agnostic. Plugin-specific shape (category
names, artifact ordering, custom severity-mappings) lives in JS config.
State-driven visibility is NOT handled here production playgrounds emit
only the active state (overview | artifact | empty | import) per render
pass. The mockup uses body[data-state="..."] for prototyping; production
renders one branch at a time.
============================================================================= */
/* === 1. Project-view top-level layout ===================================== */
.project-view {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.project-view__layout {
display: grid;
grid-template-columns: var(--project-view-nav-width) 1fr;
gap: var(--space-6);
align-items: start;
}
@media (max-width: 1279px) {
.project-view__layout { grid-template-columns: 240px 1fr; }
}
@media (max-width: 959px) {
.project-view__layout { grid-template-columns: 1fr; }
}
/* === 2. Project-view header =============================================== */
.project-view__header {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-5) var(--space-6);
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas:
"title verdict"
"title keystats"
"actions actions";
gap: var(--space-4) var(--space-6);
align-items: start;
}
.project-view__title-block { grid-area: title; }
.project-view__verdict { grid-area: verdict; justify-self: end; }
.project-view__key-stats { grid-area: keystats; justify-self: end; }
.project-view__actions { grid-area: actions; display: flex; gap: var(--space-2); justify-content: flex-end; }
.project-view__eyebrow {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-text-tertiary);
font-weight: var(--font-weight-semibold);
margin: 0 0 var(--space-2) 0;
}
.project-view__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
margin: 0 0 var(--space-2) 0;
}
.project-view__lede {
color: var(--color-text-secondary);
margin: 0;
max-width: 60ch;
}
.project-view__key-stats {
display: flex;
gap: var(--space-5);
}
.project-view__key-stat-label {
font-size: 10px;
text-transform: uppercase;
color: var(--color-text-tertiary);
letter-spacing: 0.06em;
}
.project-view__key-stat-value {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
font-variant-numeric: tabular-nums;
}
/* === 3. Verdict-pill (small) ==============================================
Companion to .verdict-pill-lg (Tier 2). Inline-flex pill used in project
header + sidebar status badges. The larger -lg variant lives in
components-tier2.css; both share the same severity-band semantics. */
.verdict-pill {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: 4px 12px;
border-radius: 999px;
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-sm);
}
.verdict-pill--positive { background: var(--color-state-success); color: #fff; }
.verdict-pill--medium { background: var(--color-severity-medium); color: var(--color-severity-medium-on); }
.verdict-pill--critical { background: var(--color-severity-critical); color: #fff; }
.verdict-pill--in-progress {
background: var(--color-bg-soft);
color: var(--color-text-secondary);
border: 1px dashed var(--color-border-moderate);
}
/* === 4. Sidebar nav ======================================================= */
.project-view__nav {
position: sticky;
top: var(--space-6);
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.project-view__nav-search input {
width: 100%;
box-sizing: border-box;
padding: 6px 10px;
font-size: var(--font-size-sm);
background: var(--color-bg);
color: var(--color-text-primary);
border: 1px solid var(--color-border-moderate);
border-radius: var(--radius-sm);
}
/* === 5. Artifact-list ===================================================== */
.artifact-list {
display: flex;
flex-direction: column;
gap: var(--space-4);
margin: 0;
padding: 0;
list-style: none;
}
.artifact-list__group {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.artifact-list__group-label {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-tertiary);
font-weight: var(--font-weight-semibold);
padding: 0 var(--space-2);
}
.artifact-list__group-count {
background: var(--color-bg-soft);
color: var(--color-text-tertiary);
font-family: var(--font-family-mono);
font-size: 10px;
padding: 1px 6px;
border-radius: 999px;
}
.artifact-list__group-items {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.artifact-list__item {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: var(--space-2);
padding: var(--artifact-list-item-pad-y) var(--artifact-list-item-pad-x);
border-radius: var(--radius-sm);
cursor: pointer;
background: transparent;
border: 1px solid transparent;
transition: background 120ms ease, border-color 120ms ease;
}
.artifact-list__item:hover { background: var(--color-bg-soft); }
.artifact-list__item[data-state="active"] {
background: var(--color-bg-soft);
border-color: var(--color-primary-500);
box-shadow: inset 3px 0 0 var(--color-primary-500);
padding-left: calc(var(--artifact-list-item-pad-x) - 3px);
}
.artifact-list__item-marker {
width: var(--artifact-marker-size);
height: var(--artifact-marker-size);
border-radius: 50%;
border: var(--artifact-marker-border) solid var(--color-border-moderate);
background: transparent;
flex-shrink: 0;
}
.artifact-list__item[data-state="filled"][data-severity="positive"] .artifact-list__item-marker {
background: var(--color-state-success);
border-color: var(--color-state-success);
}
.artifact-list__item[data-state="filled"][data-severity="medium"] .artifact-list__item-marker {
background: var(--color-severity-medium);
border-color: var(--color-severity-medium);
}
.artifact-list__item[data-state="filled"][data-severity="critical"] .artifact-list__item-marker {
background: var(--color-severity-critical);
border-color: var(--color-severity-critical);
}
.artifact-list__item-body { min-width: 0; }
.artifact-list__item-name {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.artifact-list__item[data-state="empty"] .artifact-list__item-name {
color: var(--color-text-tertiary);
font-weight: var(--font-weight-regular);
}
.artifact-list__item-meta {
font-size: 11px;
color: var(--color-text-tertiary);
}
/* === 6. Artifact-status (mini pill in sidebar) =========================== */
.artifact-status {
font-family: var(--font-family-mono);
font-size: 10px;
font-weight: var(--font-weight-semibold);
padding: 1px 5px;
border-radius: var(--radius-sm);
letter-spacing: 0.04em;
}
.artifact-status[data-severity="positive"] { background: var(--color-severity-low-soft); color: var(--color-severity-low-on); }
.artifact-status[data-severity="medium"] { background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on); }
.artifact-status[data-severity="critical"] { background: var(--color-severity-critical-soft); color: var(--color-severity-critical-on); }
/* === 7. Project-view main panel ========================================== */
.project-view__main {
min-width: 0;
display: flex;
flex-direction: column;
gap: var(--space-5);
}
/* === 8. Project-overview (default dashboard) ============================= */
.project-overview {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.project-overview__intro {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-5);
}
.project-overview__intro h2 {
font-size: var(--font-size-lg);
margin: 0 0 var(--space-2) 0;
}
.project-overview__intro p {
color: var(--color-text-secondary);
margin: 0;
}
.project-overview__verdict-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--space-3);
}
.project-overview__verdict-tile {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-left: 4px solid var(--color-border-moderate);
border-radius: var(--radius-md);
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.project-overview__verdict-tile[data-severity="positive"] { border-left-color: var(--color-state-success); }
.project-overview__verdict-tile[data-severity="medium"] { border-left-color: var(--color-severity-medium); }
.project-overview__verdict-tile[data-severity="critical"] { border-left-color: var(--color-severity-critical); }
.project-overview__verdict-tile[data-severity="empty"] { border-left-style: dashed; }
.project-overview__verdict-tile-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-tertiary);
font-weight: var(--font-weight-semibold);
}
.project-overview__verdict-tile-value {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
}
.project-overview__verdict-tile-meta {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
}
.project-overview__section h3 {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-text-tertiary);
font-weight: var(--font-weight-semibold);
margin: 0 0 var(--space-3) 0;
}
.project-overview__top-risks,
.project-overview__next-actions {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-5);
}
.project-overview__top-risks ol,
.project-overview__next-actions ol {
list-style: none;
counter-reset: rank;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.project-overview__top-risks li,
.project-overview__next-actions li {
counter-increment: rank;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
background: var(--color-bg-soft);
}
.project-overview__top-risks li::before,
.project-overview__next-actions li::before {
content: counter(rank);
font-family: var(--font-family-mono);
font-weight: var(--font-weight-bold);
color: var(--color-text-tertiary);
font-size: var(--font-size-sm);
min-width: 20px;
}
.project-overview__missing-reports {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-5);
}
.project-overview__missing-reports ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.project-overview__missing-reports li {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) var(--space-3);
background: var(--color-bg-soft);
border-radius: var(--radius-sm);
border-left: 3px dashed var(--color-border-moderate);
}
/* === 9. Artifact-view (one report rendered) ============================== */
.project-view__artifact {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.project-view__artifact-header {
display: flex;
justify-content: space-between;
align-items: start;
gap: var(--space-4);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--color-border-subtle);
}
.project-view__artifact-title {
font-size: var(--font-size-xl);
margin: 0 0 var(--space-1) 0;
}
.project-view__artifact-meta {
font-size: var(--font-size-sm);
color: var(--color-text-tertiary);
margin: 0;
}
.project-view__artifact-actions {
display: flex;
gap: var(--space-2);
flex-shrink: 0;
}
.project-view__artifact-body {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
/* === 10. Empty-artifact-prompt (no report imported yet) ================== */
.empty-artifact-prompt {
background: var(--color-surface);
border: 2px dashed var(--color-border-moderate);
border-radius: var(--radius-md);
padding: var(--space-8);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
text-align: center;
}
.empty-artifact-prompt__icon {
font-size: 48px;
opacity: 0.5;
}
.empty-artifact-prompt__title {
font-size: var(--font-size-lg);
margin: 0;
}
.empty-artifact-prompt__text {
color: var(--color-text-secondary);
margin: 0;
max-width: 50ch;
}
.empty-artifact-prompt__actions {
display: flex;
gap: var(--space-2);
margin-top: var(--space-2);
}
/* === 11. Import-modal (overlay) ========================================== */
.import-modal {
position: fixed;
inset: 0;
z-index: 200;
display: none;
}
.import-modal[data-open="true"] {
display: flex;
align-items: center;
justify-content: center;
}
.import-modal__backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
}
.import-modal__panel {
position: relative;
width: min(720px, 92vw);
max-height: 90vh;
overflow: auto;
background: var(--color-surface);
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
display: flex;
flex-direction: column;
}
.import-modal__head {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-3);
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--color-border-subtle);
}
.import-modal__title {
margin: 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
}
.import-modal__close {
background: transparent;
border: none;
cursor: pointer;
padding: 4px 10px;
color: var(--color-text-tertiary);
font-size: 20px;
line-height: 1;
border-radius: var(--radius-sm);
}
.import-modal__close:hover {
background: var(--color-bg-soft);
color: var(--color-text-primary);
}
.import-modal__form {
padding: var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.import-modal__form .field {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.import-modal__form label {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
}
.import-modal__form select,
.import-modal__form textarea {
width: 100%;
box-sizing: border-box;
padding: var(--space-2) var(--space-3);
background: var(--color-bg);
color: var(--color-text-primary);
border: 1px solid var(--color-border-moderate);
border-radius: var(--radius-sm);
font-family: var(--font-family-mono);
font-size: var(--font-size-sm);
}
.import-modal__form textarea {
resize: vertical;
min-height: 180px;
}
.import-modal__detect {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
background: var(--color-severity-low-soft);
color: var(--color-severity-low-on);
font-size: var(--font-size-sm);
}
.import-modal__preview {
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
padding: var(--space-3);
background: var(--color-bg);
max-height: 200px;
overflow: auto;
}
.import-modal__preview-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-tertiary);
margin-bottom: var(--space-2);
}
.import-modal__footer {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
padding: var(--space-3) var(--space-5);
border-top: 1px solid var(--color-border-subtle);
background: var(--color-bg-soft);
}

View file

@ -142,6 +142,14 @@
--container-default: 1080px;
--container-wide: 1280px;
--sidebar-width: 280px;
/* ---------- Project-view (Tier 4 — v0.6.0) --------------------------- */
--project-view-nav-width: 280px;
--project-view-collapse-bp: 960px; /* doc-only — referenced by media queries */
--artifact-list-item-pad-y: var(--space-2);
--artifact-list-item-pad-x: var(--space-3);
--artifact-marker-size: 14px;
--artifact-marker-border: 1.5px;
}
:root { color-scheme: light; }