feat(AP2.2c): A1 Reifegrad E2E, Stufen-/Rhythmus-Panels und Composition-Kernel
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
43d33d0edb
commit
02b6469c2d
179
backend/tests/test_ap22c_maturity_e2e.py
Normal file
179
backend/tests/test_ap22c_maturity_e2e.py
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
"""AP2.2c — A1 maturity journey End-to-End (Spagat Happy Path)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from tests.factories import provision_user_in_tenant
|
||||||
|
from tests.test_initiatives_actions import _auth, _create_initiative, _login
|
||||||
|
|
||||||
|
_STAGE1 = "Stufe 1 — Basis"
|
||||||
|
_STARTER_RECURRING = "Tägliche Übung"
|
||||||
|
|
||||||
|
|
||||||
|
def _create_spagat(client, token):
|
||||||
|
created = _create_initiative(
|
||||||
|
client,
|
||||||
|
token,
|
||||||
|
title="Spagat können",
|
||||||
|
archetype_key="initiative.maturity_journey",
|
||||||
|
)
|
||||||
|
assert created.status_code == 201
|
||||||
|
body = created.json()
|
||||||
|
assert body["starter_kit"]["applied"] is True
|
||||||
|
return body["id"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_a1_starter_kit_spagat_structure(client):
|
||||||
|
"""Starter-Kit: Stufen, Training-Project, tägliche Übung, Operating Context."""
|
||||||
|
user = provision_user_in_tenant(tenant_role="admin")
|
||||||
|
token = _login(client, user)
|
||||||
|
initiative_id = _create_spagat(client, token)
|
||||||
|
|
||||||
|
ctx = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/operating-context",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert ctx.status_code == 200
|
||||||
|
body = ctx.json()
|
||||||
|
assert body["method_key"] == "maturity_progression"
|
||||||
|
assert "maturity_stage" in body["steering_elements"]
|
||||||
|
assert "recurring_rhythm" in body["steering_elements"]
|
||||||
|
assert body["ui_profile"]["controlDefaultRoute"] == "/control/status"
|
||||||
|
|
||||||
|
roadmap = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/roadmap/items",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert roadmap.status_code == 200
|
||||||
|
stages = [i for i in roadmap.json() if i.get("item_type") == "maturity_stage"]
|
||||||
|
assert len(stages) == 3
|
||||||
|
assert sum(1 for s in stages if s["status"] == "active") == 1
|
||||||
|
assert any(s["title"] == _STAGE1 and s["status"] == "active" for s in stages)
|
||||||
|
|
||||||
|
recurring = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/recurring",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert recurring.status_code == 200
|
||||||
|
assert any(r["title"] == _STARTER_RECURRING and r["status"] == "active" for r in recurring.json())
|
||||||
|
|
||||||
|
projects = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/projects",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert projects.status_code == 200
|
||||||
|
assert any(p["title"] == "Training" for p in projects.json())
|
||||||
|
|
||||||
|
snap = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/steering-snapshot",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert snap.status_code == 200
|
||||||
|
snap_body = snap.json()
|
||||||
|
assert snap_body["steering_kernel"]["primary_method_key"] == "maturity_progression"
|
||||||
|
assert snap_body.get("steering_guidance")
|
||||||
|
next_actions = snap_body.get("next_actions") or []
|
||||||
|
assert next_actions
|
||||||
|
top = next_actions[0]
|
||||||
|
assert top.get("kind") in ("recurring_due", "action", "review_milestone")
|
||||||
|
if top.get("kind") == "recurring_due":
|
||||||
|
assert top.get("reason_code") == "recurring_due"
|
||||||
|
|
||||||
|
|
||||||
|
def test_a1_stage_verify_rotates_recurring_and_next_action(client):
|
||||||
|
"""AP2.0e: Stufe 1 reached → alte Übung pausiert, Stufe 2 aktiv, neue Routine."""
|
||||||
|
user = provision_user_in_tenant(tenant_role="admin")
|
||||||
|
token = _login(client, user)
|
||||||
|
initiative_id = _create_spagat(client, token)
|
||||||
|
|
||||||
|
items = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/roadmap/items",
|
||||||
|
headers=_auth(token),
|
||||||
|
).json()
|
||||||
|
stage1 = next(i for i in items if i["title"] == _STAGE1)
|
||||||
|
|
||||||
|
snap_before = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/steering-snapshot",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert snap_before.status_code == 200
|
||||||
|
|
||||||
|
client.post(
|
||||||
|
f"/api/initiatives/{initiative_id}/evidence",
|
||||||
|
json={
|
||||||
|
"title": "Stufe 1 geschafft",
|
||||||
|
"roadmap_item_id": stage1["id"],
|
||||||
|
"status": "accepted",
|
||||||
|
},
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
|
||||||
|
verify = client.post(
|
||||||
|
f"/api/roadmap-items/{stage1['id']}/verify-reached",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert verify.status_code == 200
|
||||||
|
body = verify.json()
|
||||||
|
assert body["status"] == "reached"
|
||||||
|
transition = body.get("maturity_transition") or {}
|
||||||
|
assert transition.get("new_recurring_id")
|
||||||
|
|
||||||
|
recurring = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/recurring",
|
||||||
|
headers=_auth(token),
|
||||||
|
).json()
|
||||||
|
titles = {r["title"]: r["status"] for r in recurring}
|
||||||
|
assert titles.get(_STARTER_RECURRING) == "paused"
|
||||||
|
assert any(t.startswith("Übung — Stufe 2") and titles[t] == "active" for t in titles)
|
||||||
|
|
||||||
|
items_after = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/roadmap/items",
|
||||||
|
headers=_auth(token),
|
||||||
|
).json()
|
||||||
|
stage2 = next(i for i in items_after if i["title"] == "Stufe 2 — Aufbau")
|
||||||
|
assert stage2["status"] == "active"
|
||||||
|
|
||||||
|
snap_after = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/steering-snapshot",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert snap_after.status_code == 200
|
||||||
|
next_after = snap_after.json().get("next_actions") or []
|
||||||
|
assert next_after
|
||||||
|
assert next_after[0].get("kind") == "recurring_due" or next_after[0].get("title", "").startswith(
|
||||||
|
"Übung —"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_a1_journey_lists_stage_reached_event(client):
|
||||||
|
"""Journey enthält nach Verify mindestens ein Ereignis."""
|
||||||
|
user = provision_user_in_tenant(tenant_role="admin")
|
||||||
|
token = _login(client, user)
|
||||||
|
initiative_id = _create_spagat(client, token)
|
||||||
|
|
||||||
|
items = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/roadmap/items",
|
||||||
|
headers=_auth(token),
|
||||||
|
).json()
|
||||||
|
stage1 = next(i for i in items if i["title"] == _STAGE1)
|
||||||
|
|
||||||
|
client.post(
|
||||||
|
f"/api/initiatives/{initiative_id}/evidence",
|
||||||
|
json={
|
||||||
|
"title": "Nachweis Stufe 1",
|
||||||
|
"roadmap_item_id": stage1["id"],
|
||||||
|
"status": "accepted",
|
||||||
|
},
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
client.post(
|
||||||
|
f"/api/roadmap-items/{stage1['id']}/verify-reached",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
|
||||||
|
journey = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/journey",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert journey.status_code == 200
|
||||||
|
events = journey.json().get("events") or []
|
||||||
|
assert len(events) >= 1
|
||||||
|
|
@ -156,7 +156,7 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden.
|
||||||
|
|
||||||
| Archetyp | Anlage+Kit | Struktur pflegen | Kontrolle | Op-API | Paket |
|
| Archetyp | Anlage+Kit | Struktur pflegen | Kontrolle | Op-API | Paket |
|
||||||
|----------|------------|------------------|-----------|--------|-------|
|
|----------|------------|------------------|-----------|--------|-------|
|
||||||
| A1 Reifegrad | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2c + AP2.0e |
|
| A1 Reifegrad | ✓ AP2.2a | ✓ | ✓ | ◐ | AP2.2c ✓ |
|
||||||
| A2 Linear | ✓ AP2.2a | ✓ | ✓ | ◐ | AP2.2b ✓ |
|
| A2 Linear | ✓ AP2.2a | ✓ | ✓ | ◐ | AP2.2b ✓ |
|
||||||
| B2b Product | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2d ✓ |
|
| B2b Product | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2d ✓ |
|
||||||
| B3 Sprint-Profil | ◐ | ◐ | ◐ | ◐ | AP2.2d ✓ |
|
| B3 Sprint-Profil | ◐ | ◐ | ◐ | ◐ | AP2.2d ✓ |
|
||||||
|
|
@ -231,6 +231,7 @@ Details: `Kairo_Status_Review_and_Next_Steps_v0.1.md` §2.1
|
||||||
| ADP Backlog/Epic | P1–P8 ✓ · K-Ext-1/2/3/4/5 ✓ |
|
| ADP Backlog/Epic | P1–P8 ✓ · K-Ext-1/2/3/4/5 ✓ |
|
||||||
| Steering Kernel Proposals | ✓ | sprint_commit · gate_next_actions · intake_triage (Kernel v0.4) |
|
| Steering Kernel Proposals | ✓ | sprint_commit · gate_next_actions · intake_triage (Kernel v0.4) |
|
||||||
| AP2.2b | A2 Linear E2E ✓ (2026-07-27) |
|
| AP2.2b | A2 Linear E2E ✓ (2026-07-27) |
|
||||||
|
| AP2.2c | A1 Reifegrad E2E ✓ (2026-07-27) |
|
||||||
| AP2.2c–e | Referenz-Archetypen End-to-End ◐→✓ |
|
| AP2.2c–e | Referenz-Archetypen End-to-End ◐→✓ |
|
||||||
| AP2.1 | MVP-Abnahfe ✗→✓ |
|
| AP2.1 | MVP-Abnahfe ✗→✓ |
|
||||||
| AP1.7b | Op-API Parität |
|
| AP1.7b | Op-API Parität |
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ Phase 1 AP2.2a Archetyp-geführte Anlage + Starter-Kits ✓
|
||||||
Phase 1b Specs Welle 1 finalisieren (A1,A2,B2a,B2b,B3) ◐
|
Phase 1b Specs Welle 1 finalisieren (A1,A2,B2a,B2b,B3) ◐
|
||||||
Phase 2 AP1.9d Methoden-Default-Ansichten (Anti-Todo-Wand) ◐ (Hints + Recurring)
|
Phase 2 AP1.9d Methoden-Default-Ansichten (Anti-Todo-Wand) ◐ (Hints + Recurring)
|
||||||
Phase 3 AP2.2b A2 Linear End-to-End ✓
|
Phase 3 AP2.2b A2 Linear End-to-End ✓
|
||||||
AP2.2c A1 Reifegrad + AP2.0e ◐ Code
|
AP2.2c A1 Reifegrad + AP2.0e ✓
|
||||||
AP2.2d B2b Product + B3 Sprint ✓
|
AP2.2d B2b Product + B3 Sprint ✓
|
||||||
AP2.2e B2a Programm (optional vor AP2.1)
|
AP2.2e B2a Programm (optional vor AP2.1)
|
||||||
Phase 4 AP1.7b Operational API — Pflege-Parität
|
Phase 4 AP1.7b Operational API — Pflege-Parität
|
||||||
|
|
|
||||||
72
frontend/src/components/MaturityStagePanel.jsx
Normal file
72
frontend/src/components/MaturityStagePanel.jsx
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { MILESTONE_STATUS_LABELS } from '../constants/status.js'
|
||||||
|
import { EmptyState } from './EmptyState.jsx'
|
||||||
|
import { scopedPath } from '../utils/routes.js'
|
||||||
|
|
||||||
|
export function MaturityStagePanel({
|
||||||
|
initiativeId,
|
||||||
|
roadmapItems = [],
|
||||||
|
embedded = true,
|
||||||
|
}) {
|
||||||
|
const stages = [...roadmapItems]
|
||||||
|
.filter((item) => item.item_type === 'maturity_stage')
|
||||||
|
.sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0))
|
||||||
|
|
||||||
|
const activeStage = stages.find((s) => s.status === 'active')
|
||||||
|
|
||||||
|
const body =
|
||||||
|
stages.length === 0 ? (
|
||||||
|
<EmptyState message="Noch keine Reifegrad-Stufen — unter Plan anlegen oder Starter-Kit nutzen." />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{activeStage && (
|
||||||
|
<p className="maturity-stage-active muted">
|
||||||
|
Aktive Stufe: <strong>{activeStage.title}</strong>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<ol className="item-list maturity-stage-list">
|
||||||
|
{stages.map((stage, index) => (
|
||||||
|
<li
|
||||||
|
key={stage.id}
|
||||||
|
className={
|
||||||
|
'list-item card-list-item maturity-stage-item' +
|
||||||
|
(stage.status === 'active' ? ' maturity-stage-item--active' : '') +
|
||||||
|
(stage.status === 'reached' ? ' maturity-stage-item--reached' : '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="maturity-stage-item__index muted">{index + 1}</span>
|
||||||
|
<div className="list-item-main">
|
||||||
|
<strong>{stage.title}</strong>
|
||||||
|
<span className="muted list-item-sub">
|
||||||
|
{MILESTONE_STATUS_LABELS[stage.status] || stage.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
<p className="muted maturity-stage-hint">
|
||||||
|
Stufen und Übungen unter{' '}
|
||||||
|
<Link to={scopedPath('/control/journey', { initiativeId })} className="link-inline">
|
||||||
|
Kontrolle → Rhythmen & Journey
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!embedded) return body
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card maturity-stage-panel">
|
||||||
|
<div className="section-header">
|
||||||
|
<div>
|
||||||
|
<h2>Reifegrad-Stufen</h2>
|
||||||
|
<p className="section-lead muted">
|
||||||
|
Aktive Stufe und Fortschritt — Steuerungshorizont für Reifegrad-Vorhaben (A1).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{body}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
84
frontend/src/components/RecurringRhythmPanel.jsx
Normal file
84
frontend/src/components/RecurringRhythmPanel.jsx
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { RECURRING_STATUS_LABELS } from '../constants/status.js'
|
||||||
|
import { EmptyState } from './EmptyState.jsx'
|
||||||
|
import { scopedPath } from '../utils/routes.js'
|
||||||
|
|
||||||
|
function formatDueAt(iso) {
|
||||||
|
if (!iso) return null
|
||||||
|
try {
|
||||||
|
return new Date(iso).toLocaleString('de-DE')
|
||||||
|
} catch {
|
||||||
|
return iso
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RecurringRhythmPanel({
|
||||||
|
initiativeId,
|
||||||
|
recurringItems = [],
|
||||||
|
embedded = true,
|
||||||
|
}) {
|
||||||
|
const sorted = [...recurringItems].sort((a, b) => {
|
||||||
|
const statusOrder = { active: 0, paused: 1, archived: 2 }
|
||||||
|
const diff = (statusOrder[a.status] ?? 9) - (statusOrder[b.status] ?? 9)
|
||||||
|
if (diff !== 0) return diff
|
||||||
|
return (a.title || '').localeCompare(b.title || '', 'de')
|
||||||
|
})
|
||||||
|
|
||||||
|
const active = sorted.filter((item) => item.status === 'active')
|
||||||
|
|
||||||
|
const body =
|
||||||
|
sorted.length === 0 ? (
|
||||||
|
<EmptyState message="Noch keine Rhythmen — Starter-Kit oder Journey legt Übungen an." />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{active.length > 0 && (
|
||||||
|
<p className="recurring-rhythm-summary muted">
|
||||||
|
{active.length} aktive Routine{active.length === 1 ? '' : 'n'} — fällige Übung steuert
|
||||||
|
Next Action.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<ul className="item-list recurring-rhythm-list">
|
||||||
|
{sorted.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
className={
|
||||||
|
'list-item card-list-item recurring-rhythm-item' +
|
||||||
|
(item.status === 'active' ? ' recurring-rhythm-item--active' : '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="list-item-main">
|
||||||
|
<strong>{item.title}</strong>
|
||||||
|
<span className="muted list-item-sub">
|
||||||
|
{RECURRING_STATUS_LABELS[item.status] || item.status}
|
||||||
|
{item.next_due_at ? ` · Fällig: ${formatDueAt(item.next_due_at)}` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<p className="muted recurring-rhythm-hint">
|
||||||
|
Rhythmen pflegen unter{' '}
|
||||||
|
<Link to={scopedPath('/control/journey', { initiativeId })} className="link-inline">
|
||||||
|
Kontrolle → Journey
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!embedded) return body
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card recurring-rhythm-panel">
|
||||||
|
<div className="section-header">
|
||||||
|
<div>
|
||||||
|
<h2>Rhythmen & Übungen</h2>
|
||||||
|
<p className="section-lead muted">
|
||||||
|
Aktive Routinen am Reifegrad-Pfad — Leading Next aus fälliger Übung (A1).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{body}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,8 @@ export function InitiativeCompositionSurface({
|
||||||
() => ({
|
() => ({
|
||||||
initiativeId: ops.initiativeId,
|
initiativeId: ops.initiativeId,
|
||||||
actions: ops.actions,
|
actions: ops.actions,
|
||||||
|
roadmapItems: ops.roadmapItems,
|
||||||
|
recurringItems: ops.recurringItems,
|
||||||
steeringSnapshotLoading: ops.steeringSnapshotLoading,
|
steeringSnapshotLoading: ops.steeringSnapshotLoading,
|
||||||
steeringSnapshotError: ops.steeringSnapshotError,
|
steeringSnapshotError: ops.steeringSnapshotError,
|
||||||
steeringMethods: ops.steeringMethods,
|
steeringMethods: ops.steeringMethods,
|
||||||
|
|
@ -36,6 +38,8 @@ export function InitiativeCompositionSurface({
|
||||||
[
|
[
|
||||||
ops.initiativeId,
|
ops.initiativeId,
|
||||||
ops.actions,
|
ops.actions,
|
||||||
|
ops.roadmapItems,
|
||||||
|
ops.recurringItems,
|
||||||
ops.steeringSnapshotLoading,
|
ops.steeringSnapshotLoading,
|
||||||
ops.steeringSnapshotError,
|
ops.steeringSnapshotError,
|
||||||
ops.steeringMethods,
|
ops.steeringMethods,
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,26 @@ export const COMPOSITION_PROVIDERS = [
|
||||||
scopeTypes: ['initiative'],
|
scopeTypes: ['initiative'],
|
||||||
order: 10,
|
order: 10,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'steering.maturity_stage',
|
||||||
|
kind: 'steering_element',
|
||||||
|
steeringElement: 'maturity_stage',
|
||||||
|
slotKeys: ['control.status.steering'],
|
||||||
|
componentKey: 'MaturityStagePanel',
|
||||||
|
requiresCapability: 'kairo.initiative.read',
|
||||||
|
scopeTypes: ['initiative'],
|
||||||
|
order: 15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'steering.recurring_rhythm',
|
||||||
|
kind: 'steering_element',
|
||||||
|
steeringElement: 'recurring_rhythm',
|
||||||
|
slotKeys: ['control.status.steering'],
|
||||||
|
componentKey: 'RecurringRhythmPanel',
|
||||||
|
requiresCapability: 'kairo.initiative.read',
|
||||||
|
scopeTypes: ['initiative'],
|
||||||
|
order: 16,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'steering.next_action',
|
key: 'steering.next_action',
|
||||||
kind: 'steering_element',
|
kind: 'steering_element',
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { Link } from 'react-router-dom'
|
||||||
import { scopedPath } from '../utils/routes.js'
|
import { scopedPath } from '../utils/routes.js'
|
||||||
import { SteeringSnapshotPanel } from '../components/SteeringSnapshotPanel.jsx'
|
import { SteeringSnapshotPanel } from '../components/SteeringSnapshotPanel.jsx'
|
||||||
import { CriticalPathPanel } from '../components/CriticalPathPanel.jsx'
|
import { CriticalPathPanel } from '../components/CriticalPathPanel.jsx'
|
||||||
|
import { MaturityStagePanel } from '../components/MaturityStagePanel.jsx'
|
||||||
|
import { RecurringRhythmPanel } from '../components/RecurringRhythmPanel.jsx'
|
||||||
import { AgentSlotsPanel } from '../components/AgentSlotsPanel.jsx'
|
import { AgentSlotsPanel } from '../components/AgentSlotsPanel.jsx'
|
||||||
import { SteeringProposalsPanel } from '../components/SteeringProposalsPanel.jsx'
|
import { SteeringProposalsPanel } from '../components/SteeringProposalsPanel.jsx'
|
||||||
import { NextActionWidget } from '../widgets/NextActionWidget.jsx'
|
import { NextActionWidget } from '../widgets/NextActionWidget.jsx'
|
||||||
|
|
@ -34,6 +36,8 @@ export function WidgetHost({ widget }) {
|
||||||
export const PROVIDER_COMPONENTS = {
|
export const PROVIDER_COMPONENTS = {
|
||||||
SteeringSnapshotPanel,
|
SteeringSnapshotPanel,
|
||||||
CriticalPathPanel,
|
CriticalPathPanel,
|
||||||
|
MaturityStagePanel,
|
||||||
|
RecurringRhythmPanel,
|
||||||
NextActionWidget,
|
NextActionWidget,
|
||||||
AgentSlotsPanel,
|
AgentSlotsPanel,
|
||||||
SteeringProposalsPanel,
|
SteeringProposalsPanel,
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,14 @@ function resolveAgentSlots(input) {
|
||||||
* @param {object | null | undefined} steeringSnapshot
|
* @param {object | null | undefined} steeringSnapshot
|
||||||
*/
|
*/
|
||||||
export function resolveNextActionUi(elements, steeringSnapshot) {
|
export function resolveNextActionUi(elements, steeringSnapshot) {
|
||||||
const priority = ['critical_path', 'work_cycle_scope', 'gate_fulfillment', 'queue_inbox']
|
const priority = [
|
||||||
|
'critical_path',
|
||||||
|
'recurring_rhythm',
|
||||||
|
'work_cycle_scope',
|
||||||
|
'maturity_stage',
|
||||||
|
'gate_fulfillment',
|
||||||
|
'queue_inbox',
|
||||||
|
]
|
||||||
const hasActiveSprint = Boolean(steeringSnapshot?.active_work_cycle)
|
const hasActiveSprint = Boolean(steeringSnapshot?.active_work_cycle)
|
||||||
for (const key of priority) {
|
for (const key of priority) {
|
||||||
if (key === 'work_cycle_scope' && !hasActiveSprint) continue
|
if (key === 'work_cycle_scope' && !hasActiveSprint) continue
|
||||||
|
|
@ -165,6 +172,16 @@ export function buildProviderProps(provider, input) {
|
||||||
scopeRoadmapItemId: kernel?.horizon?.gate_roadmap_item_id ?? null,
|
scopeRoadmapItemId: kernel?.horizon?.gate_roadmap_item_id ?? null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'MaturityStagePanel':
|
||||||
|
return {
|
||||||
|
initiativeId: ops.initiativeId,
|
||||||
|
roadmapItems: ops.roadmapItems || [],
|
||||||
|
}
|
||||||
|
case 'RecurringRhythmPanel':
|
||||||
|
return {
|
||||||
|
initiativeId: ops.initiativeId,
|
||||||
|
recurringItems: ops.recurringItems || [],
|
||||||
|
}
|
||||||
case 'NextActionWidget': {
|
case 'NextActionWidget': {
|
||||||
const nextActionUi = resolveNextActionUi(elements, input.steeringSnapshot)
|
const nextActionUi = resolveNextActionUi(elements, input.steeringSnapshot)
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,33 @@ describe('resolveSteeringComposition', () => {
|
||||||
expect(props.scopeRoadmapItemId).toBe('gate-1')
|
expect(props.scopeRoadmapItemId).toBe('gate-1')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('buildProviderProps gates milestone horizon in snapshot panel', () => {
|
it('activates maturity panels for A1 on control.status', () => {
|
||||||
|
const result = resolveSteeringComposition({
|
||||||
|
surfaceKey: 'control.status',
|
||||||
|
scope: 'initiative',
|
||||||
|
capabilities: CAPS,
|
||||||
|
steeringElements: ['next_action_primary', 'maturity_stage', 'recurring_rhythm'],
|
||||||
|
steeringSnapshot: { counts: {}, next_actions: [] },
|
||||||
|
opsContext: { initiativeId: 'init-1', roadmapItems: [], recurringItems: [] },
|
||||||
|
})
|
||||||
|
const steering = result.slots['control.status.steering'] || []
|
||||||
|
expect(steering.some((p) => p.key === 'steering.maturity_stage')).toBe(true)
|
||||||
|
expect(steering.some((p) => p.key === 'steering.recurring_rhythm')).toBe(true)
|
||||||
|
expect(steering.some((p) => p.key === 'steering.critical_path')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('buildProviderProps uses recurring rhythm next-action copy', () => {
|
||||||
|
const provider = COMPOSITION_PROVIDERS.find((p) => p.key === 'steering.next_action')
|
||||||
|
const props = buildProviderProps(provider, {
|
||||||
|
steeringElements: ['next_action_primary', 'recurring_rhythm', 'maturity_stage'],
|
||||||
|
steeringSnapshot: { next_actions: [] },
|
||||||
|
opsContext: { initiativeId: 'x', steeringSnapshotLoading: false },
|
||||||
|
capabilities: CAPS,
|
||||||
|
})
|
||||||
|
expect(props.title).toContain('Rhythmus')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('buildProviderProps toggles gate horizon on steering snapshot', () => {
|
||||||
const provider = COMPOSITION_PROVIDERS.find((p) => p.key === 'core.steering_snapshot')
|
const provider = COMPOSITION_PROVIDERS.find((p) => p.key === 'core.steering_snapshot')
|
||||||
const withGate = buildProviderProps(provider, {
|
const withGate = buildProviderProps(provider, {
|
||||||
steeringElements: ['gate_fulfillment', 'next_action_primary'],
|
steeringElements: ['gate_fulfillment', 'next_action_primary'],
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,15 @@ export const STEERING_ELEMENT_UI = {
|
||||||
nextActionSubtitle:
|
nextActionSubtitle:
|
||||||
'Empfehlung aus dem Sprint-Backlog — Continuous Next außerhalb des Sprints.',
|
'Empfehlung aus dem Sprint-Backlog — Continuous Next außerhalb des Sprints.',
|
||||||
},
|
},
|
||||||
|
recurring_rhythm: {
|
||||||
|
nextActionTitle: 'Heutige Übung / Rhythmus',
|
||||||
|
nextActionSubtitle:
|
||||||
|
'Reifegrad — aktive Routine steuert den nächsten Schritt, nicht die Gesamtliste.',
|
||||||
|
},
|
||||||
|
maturity_stage: {
|
||||||
|
nextActionTitle: 'Nächster Schritt in der Reifegrad-Entwicklung',
|
||||||
|
nextActionSubtitle: 'Aktive Stufe und fällige Übung bestimmen die Empfehlung.',
|
||||||
|
},
|
||||||
gate_fulfillment: {
|
gate_fulfillment: {
|
||||||
nextActionTitle: 'Nächster sinnvoller Schritt',
|
nextActionTitle: 'Nächster sinnvoller Schritt',
|
||||||
nextActionSubtitle:
|
nextActionSubtitle:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user