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) 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 linkedin-thought-leadership/ v1.2.0 — LinkedIn content pipeline + analytics
llm-security/ v6.0.0 — Security scanning, auditing, threat modeling 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 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. 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/ 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/ 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 3. Les REMEMBER.md og TODO.md for sesjonsstatus
4. Jobb innenfor scope 4. Jobb innenfor scope
5. Oppdater REMEMBER.md ved avslutning 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. 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 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. - **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). - **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`. - **Foundation helpers**`renderPageShell`, `renderVerdictPill`, `renderKeyStatsGrid`, `inferVerdict`, `inferKeyStats`, `KEY_STATS_CONFIG`.

View file

@ -1,6 +1,6 @@
{ {
"name": "ms-ai-architect", "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", "description": "Microsoft AI Solution Architect - structured architecture guidance for the full Microsoft AI stack",
"author": { "author": {
"name": "Kjell Tore Guttormsen" "name": "Kjell Tore Guttormsen"

View file

@ -1,4 +1,6 @@
*.local.md *.local.md
*.local.html
*.local.json
.mcp.json .mcp.json
.DS_Store .DS_Store
.claude/ .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/), 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). 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 ## [1.14.0] - 2026-05-08
### Changed — playground root-cause refaktor (6 sesjoner) ### Changed — playground root-cause refaktor (6 sesjoner)

View file

@ -187,12 +187,14 @@ claude --plugin ./plugins/ms-ai-architect
/architect:help /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) - **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`. - **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. - **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`. - **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. - **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. - **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 | | 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 | | 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 | | Migrasjon | `bash tests/test-playground-migrations.sh` | 16 PASS — v1→v2 + v2→v3 idempotent migrasjon |
| Kombinert (E2E) | `bash tests/run-e2e.sh --playground` | 272 PASS — statisk + parser-suiter | | 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 | | 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 | | 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 | | 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) ### 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)* *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) ![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Docs](https://img.shields.io/badge/reference_docs-387-green) ![Docs](https://img.shields.io/badge/reference_docs-387-green)
![Agents](https://img.shields.io/badge/agents-12-orange) ![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 | | 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.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.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. | | **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 # 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 ## 0.4.0 — 2026-05-08
### Bug fixes ### Bug fixes

View file

@ -2,16 +2,17 @@
"generated_by": "scripts/sync-design-system.mjs", "generated_by": "scripts/sync-design-system.mjs",
"do_not_edit": true, "do_not_edit": true,
"source": "shared/playground-design-system/", "source": "shared/playground-design-system/",
"source_commit": "9f806469f37742be65f778059bf364308c9d2811", "source_commit": "c1b7bad3899c5cfe9ff90663003609b018aa79a0",
"sync_date": "2026-05-08T17:57:53.412Z", "sync_date": "2026-05-15T14:11:07.444Z",
"file_count": 26, "file_count": 27,
"files": { "files": {
"CHANGELOG.md": "dfbd75552c94848acba3e2503bfad56c1c4bc8cfdcbd638d9409149010913d28", "CHANGELOG.md": "b5018b46cd0830334109e915d23b5554c060412c2b7e132f97f2933e5dd5d79c",
"README.md": "83de0e29b207c979b7b2a3327b7a4ec0c2e1b4d3705ee2677f26c28c3a3ee643", "README.md": "83de0e29b207c979b7b2a3327b7a4ec0c2e1b4d3705ee2677f26c28c3a3ee643",
"base.css": "604fe6839e2ed304bc0ba112a4e045f208b4b3f084f449a1abdb94ce0a1e5263", "base.css": "df0db874473412eb771b7355b589f7478042987756898f0921584286bd5ba70a",
"components-tier2.css": "c2cb7e9d76d6af28d50db654030413777feb2f2f2b93213e598de8b686b14523", "components-tier2.css": "c2cb7e9d76d6af28d50db654030413777feb2f2f2b93213e598de8b686b14523",
"components-tier3-supplement.css": "51fab10377d80029d6552613069d46fd14ce66af77fe6705b1c6bdf5c9e6481e", "components-tier3-supplement.css": "51fab10377d80029d6552613069d46fd14ce66af77fe6705b1c6bdf5c9e6481e",
"components-tier3.css": "c391ea387298ce864bc35078e7e044b2cdd4187e3130456347d91876599ff4b1", "components-tier3.css": "c391ea387298ce864bc35078e7e044b2cdd4187e3130456347d91876599ff4b1",
"components-tier4-project-view.css": "f8f784df70044ecc9bdc862a327b1ee58b201d056581316808a9b60632c5a993",
"components.css": "56fa7392b8b20b567a46f72a8fe9e0205d78ce475eae6b22fc3f50b39b235545", "components.css": "56fa7392b8b20b567a46f72a8fe9e0205d78ce475eae6b22fc3f50b39b235545",
"fonts.css": "e3c3df581c6e4d66e25c555f125c745f6512a33038401089d2519a94ea63ee3d", "fonts.css": "e3c3df581c6e4d66e25c555f125c745f6512a33038401089d2519a94ea63ee3d",
"fonts/Inter-Bold.woff2": "220976705fbec109f43c5cfdceca639e99ace7e51f3eb67292b105d3575eb39b", "fonts/Inter-Bold.woff2": "220976705fbec109f43c5cfdceca639e99ace7e51f3eb67292b105d3575eb39b",
@ -31,6 +32,6 @@
"schemas/finding.schema.json": "0b24797373650582bac232d31a4dd9260593375a0d17259e18f1141a20de8d0c", "schemas/finding.schema.json": "0b24797373650582bac232d31a4dd9260593375a0d17259e18f1141a20de8d0c",
"schemas/okr-set.schema.json": "aa27347fb232a956ec9dcee1775115710e2715a665c8d729ac50b90c6884de26", "schemas/okr-set.schema.json": "aa27347fb232a956ec9dcee1775115710e2715a665c8d729ac50b90c6884de26",
"schemas/ros-threat.schema.json": "e16497c1a6b79d6e78149d6cf1c28ac9df1e93234627a0c546814fb24d6c96d9", "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-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-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-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 ---------- */ /* ---------- Cards / surfaces ---------- */
.card { .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-security: #A40E26; /* llm-security — crimson */
--color-scope-ultraplan: #4338CA; /* ultraplan-local — indigo */ --color-scope-ultraplan: #4338CA; /* ultraplan-local — indigo */
--color-scope-config: #3F5963; /* config-audit — slate */ --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 -------------------------------------------------- */ /* ---------- Spacing -------------------------------------------------- */
--space-1: 4px; --space-1: 4px;
@ -140,6 +143,14 @@
--container-default: 1080px; --container-default: 1080px;
--container-wide: 1280px; --container-wide: 1280px;
--sidebar-width: 280px; --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; } :root { color-scheme: light; }

View file

@ -76,6 +76,10 @@ fi
if $RUN_PLAYGROUND; then if $RUN_PLAYGROUND; then
bash "$SCRIPT_DIR/test-playground-v3.sh" || FAILURES=$((FAILURES + 1)) 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-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 fi
if $RUN_KB_UPDATE; then if $RUN_KB_UPDATE; then

View file

@ -1,14 +1,12 @@
#!/usr/bin/env node #!/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: // v1.15.0: v2 project-surface (renderProjectSurface med screen-tabs +
// - Initial onboarding (empty state) // category-tabs) erstattet av renderProjectView (sidebar med 17 artifacts +
// - "Last inn demo-data" → project surface with all 17 reports rehydrated // main-area med per-artifact view + import-modal). Skjermbilder oppdatert
// - All 4 project screen-tabs (oversikt / rapporter / kontekst / eksport) // til å fange v3-surfaces.
// - Each rapport-tab category (regulatory / security / economy / docs / tool)
// - Both themes (dark + light)
// //
// Output: playground/screenshots/v1.14.0/<surface>-<theme>.png // Output: playground/screenshots/v1.15.0/<surface>-<theme>.png
// //
// Usage: // Usage:
// cd tests/screenshot // cd tests/screenshot
@ -25,7 +23,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const PLUGIN_ROOT = resolve(__dirname, '..', '..'); const PLUGIN_ROOT = resolve(__dirname, '..', '..');
const HTML_PATH = join(PLUGIN_ROOT, 'playground', 'ms-ai-architect-playground.html'); 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 HTML_URL = 'file://' + HTML_PATH;
const VIEWPORT = { width: 1440, height: 900 }; const VIEWPORT = { width: 1440, height: 900 };
@ -49,7 +47,6 @@ async function clearState(page) {
await page.evaluate(() => { await page.evaluate(() => {
try { localStorage.clear(); } catch (e) {} try { localStorage.clear(); } catch (e) {}
try { try {
// Best-effort: clear IndexedDB databases.
const dbs = ['ms-ai-architect-state-v1', 'ms-ai-architect-playground']; const dbs = ['ms-ai-architect-state-v1', 'ms-ai-architect-playground'];
dbs.forEach((n) => indexedDB.deleteDatabase(n)); dbs.forEach((n) => indexedDB.deleteDatabase(n));
} catch (e) {} } catch (e) {}
@ -61,9 +58,9 @@ async function loadDemo(page) {
const action = document.querySelector('[data-action="load-demo"]'); const action = document.querySelector('[data-action="load-demo"]');
if (action) action.click(); 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.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) { async function clickAction(page, action) {
@ -71,28 +68,46 @@ async function clickAction(page, action) {
const el = document.querySelector('[data-action="' + a + '"]'); const el = document.querySelector('[data-action="' + a + '"]');
if (el) el.click(); if (el) el.click();
}, action); }, 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); await page.waitForTimeout(400);
} }
async function clickProjectScreen(page, screenId) { async function selectArtifact(page, artifactId) {
await page.evaluate((s) => { await page.evaluate((id) => {
const el = document.querySelector('[data-action="project-screen"][data-screen="' + s + '"]'); const el = document.querySelector('[data-action="project-select-artifact"][data-artifact-id="' + id + '"]');
if (el) el.click(); 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); 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'); 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'); console.log(' → ' + name + '.png');
} }
@ -106,52 +121,58 @@ async function captureAllSurfaces(page, theme) {
await setTheme(page, theme); await setTheme(page, theme);
await shoot(page, '01-onboarding-empty-' + 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 loadDemo(page);
await setTheme(page, theme); 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) // 3-7. 5 sample artifacts som dekker arketype-bredden
const TABS = [ const SAMPLE_ARTIFACTS = [
{ id: 'security', label: 'security' }, { id: 'classify', label: 'classify' }, // AI Act-pyramide
{ id: 'economy', label: 'economy' }, { id: 'security', label: 'security' }, // 6×5 sikkerhets-matrise
{ id: 'documentation', label: 'documentation' }, { id: 'ros', label: 'ros' }, // ROS matrise + radar
{ id: 'tool', label: 'tool' } { id: 'cost', label: 'cost' }, // P10/P50/P90 distribusjon
{ id: 'summary', label: 'summary' } // Beslutningsnotat
]; ];
for (const tab of TABS) { for (let i = 0; i < SAMPLE_ARTIFACTS.length; i++) {
await clickProjectTab(page, tab.id); const a = SAMPLE_ARTIFACTS[i];
await page.waitForTimeout(500); await selectArtifact(page, a.id);
await shoot(page, '03-project-rapporter-' + tab.label + '-' + theme); const num = String(3 + i).padStart(2, '0');
await shoot(page, num + '-project-artifact-' + a.label + '-' + theme);
} }
// 4. Project screen-tabs (oversikt / kontekst / eksport) // 8. Import-modal åpen (med prefill fra eksisterende ros-artifact)
await clickProjectScreen(page, 'oversikt'); // Viewport-only (ikke fullPage) — modal er position:fixed; fullPage
await shoot(page, '04-project-oversikt-' + theme); // skroller forbi overlay-en og kaster bort kontekst.
await clickProjectScreen(page, 'kontekst'); await openImportModal(page, 'ros');
await shoot(page, '05-project-kontekst-' + theme); await page.evaluate(() => window.scrollTo(0, 0));
await clickProjectScreen(page, 'eksport'); await page.waitForTimeout(200);
await shoot(page, '06-project-eksport-' + theme); 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 // 9. Sidebar-søk aktivt (filtrer på "ros")
await clickProjectScreen(page, 'rapporter'); 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 clickAction(page, 'goto-home');
await page.waitForSelector('[data-surface="home"]:not([hidden])'); await page.waitForSelector('[data-surface="home"]:not([hidden])');
await page.waitForTimeout(300); 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 clickAction(page, 'goto-catalog');
await page.waitForSelector('[data-surface="catalog"]:not([hidden])'); await page.waitForSelector('[data-surface="catalog"]:not([hidden])');
await page.waitForTimeout(300); 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 clickAction(page, 'goto-onboarding');
await page.waitForSelector('[data-surface="onboarding"]:not([hidden])'); await page.waitForSelector('[data-surface="onboarding"]:not([hidden])');
await page.waitForTimeout(300); await page.waitForTimeout(300);
await shoot(page, '09-onboarding-prefilled-' + theme); await shoot(page, '12-onboarding-prefilled-' + theme);
} }
async function main() { async function main() {
@ -160,7 +181,7 @@ async function main() {
const browser = await chromium.launch(); const browser = await chromium.launch();
const context = await browser.newContext({ const context = await browser.newContext({
viewport: VIEWPORT, viewport: VIEWPORT,
deviceScaleFactor: 2 // crisper screenshots for retina deviceScaleFactor: 2
}); });
const page = await context.newPage(); const page = await context.newPage();
page.on('console', (msg) => { 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 a = JSON.stringify(stateA);
const b = JSON.stringify(stateB); const b = JSON.stringify(stateB);
if (stateA.dataVersion !== 2) { if (stateA.dataVersion !== 3) {
console.error("DATA_VERSION_NOT_BUMPED"); console.error("DATA_VERSION_NOT_BUMPED got=" + stateA.dataVersion);
process.exit(4); 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 (verdictsAdded === 0) { console.error("NO_VERDICTS_ADDED"); process.exit(5); }
if (statsAdded === 0) { console.error("NO_KEYSTATS_ADDED"); process.exit(6); } 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) { if (a === b) {
console.log("IDEMPOTENT verdicts=" + verdictsAdded + " stats=" + statsAdded); console.log("IDEMPOTENT verdicts=" + verdictsAdded + " stats=" + statsAdded + " artifacts=" + artifactsBuilt);
} else { } else {
console.error("NOT_IDEMPOTENT"); console.error("NOT_IDEMPOTENT");
process.exit(7); process.exit(7);
@ -155,7 +169,7 @@ else
fail "Idempotency-test feilet: $IDEMPOTENCY_RESULT" fail "Idempotency-test feilet: $IDEMPOTENCY_RESULT"
fi 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 ' DV_RESULT=$(node -e '
const fs = require("fs"); const fs = require("fs");
const html = fs.readFileSync(process.argv[1], "utf8"); const html = fs.readFileSync(process.argv[1], "utf8");
@ -176,10 +190,256 @@ api.migrateDataVersion(state, api.defaultArchetypeFor);
console.log(state.dataVersion); console.log(state.dataVersion);
' "$HTML_FILE" 2>&1) || true ' "$HTML_FILE" 2>&1) || true
if [ "$DV_RESULT" = "2" ]; then if [ "$DV_RESULT" = "3" ]; then
pass "dataVersion bumpes til 2" pass "dataVersion bumpes til 3 (kjede v?→v3)"
else else
fail "dataVersion ble ikke bumpet til 2 (got '$DV_RESULT')" fail "dataVersion ble ikke bumpet til 3 (got '$DV_RESULT')"
fi 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 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 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" 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 for c in $REPORT_CMDS; do
if grep -qE "data-report-slot=[\"']${c}[\"']" "$HTML_FILE"; then # PROJECT_VIEW_CONFIG.renderers[<cid>] = RENDERERS.renderXxx
pass "data-report-slot=\"${c}\" markup til stede" if grep -qE "^[[:space:]]+${c}:[[:space:]]+RENDERERS\." "$HTML_FILE"; then
slot_hits=$((slot_hits + 1)) pass "PROJECT_VIEW_CONFIG.renderers.${c} wired"
renderer_hits=$((renderer_hits + 1))
else else
warn "data-report-slot=\"${c}\" finnes ikke i statisk markup (kan rendres dynamisk)" fail "PROJECT_VIEW_CONFIG.renderers.${c} mangler"
fi fi
done done
# Slot rendrer dynamisk via render-funksjoner — warn kun, ingen fail pass "v3 renderer-routing wired ($renderer_hits/17)"
pass "Report-slot-stikkprøve fullført ($slot_hits/17 statiske; resterende rendres dynamisk)"
# ------------------------------------------------------- # -------------------------------------------------------
# 14. report_archetype-routing-felt i CATALOG-data # 14. report_archetype-routing-felt i CATALOG-data

View file

@ -1,5 +1,53 @@
# playground-design-system — CHANGELOG # 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 ## 0.5.0 — 2026-05-10
### Added ### 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-default: 1080px;
--container-wide: 1280px; --container-wide: 1280px;
--sidebar-width: 280px; --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; } :root { color-scheme: light; }