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 |
|
||||
|----------|------------|------------------|-----------|--------|-------|
|
||||
| A1 Reifegrad | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2c + AP2.0e |
|
||||
| A1 Reifegrad | ✓ AP2.2a | ✓ | ✓ | ◐ | AP2.2c ✓ |
|
||||
| A2 Linear | ✓ AP2.2a | ✓ | ✓ | ◐ | AP2.2b ✓ |
|
||||
| B2b Product | ◐ AP2.2a | ◐ | ◐ | ◐ | 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 ✓ |
|
||||
| Steering Kernel Proposals | ✓ | sprint_commit · gate_next_actions · intake_triage (Kernel v0.4) |
|
||||
| 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.1 | MVP-Abnahfe ✗→✓ |
|
||||
| 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 2 AP1.9d Methoden-Default-Ansichten (Anti-Todo-Wand) ◐ (Hints + Recurring)
|
||||
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.2e B2a Programm (optional vor AP2.1)
|
||||
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,
|
||||
actions: ops.actions,
|
||||
roadmapItems: ops.roadmapItems,
|
||||
recurringItems: ops.recurringItems,
|
||||
steeringSnapshotLoading: ops.steeringSnapshotLoading,
|
||||
steeringSnapshotError: ops.steeringSnapshotError,
|
||||
steeringMethods: ops.steeringMethods,
|
||||
|
|
@ -36,6 +38,8 @@ export function InitiativeCompositionSurface({
|
|||
[
|
||||
ops.initiativeId,
|
||||
ops.actions,
|
||||
ops.roadmapItems,
|
||||
ops.recurringItems,
|
||||
ops.steeringSnapshotLoading,
|
||||
ops.steeringSnapshotError,
|
||||
ops.steeringMethods,
|
||||
|
|
|
|||
|
|
@ -57,6 +57,26 @@ export const COMPOSITION_PROVIDERS = [
|
|||
scopeTypes: ['initiative'],
|
||||
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',
|
||||
kind: 'steering_element',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { Link } from 'react-router-dom'
|
|||
import { scopedPath } from '../utils/routes.js'
|
||||
import { SteeringSnapshotPanel } from '../components/SteeringSnapshotPanel.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 { SteeringProposalsPanel } from '../components/SteeringProposalsPanel.jsx'
|
||||
import { NextActionWidget } from '../widgets/NextActionWidget.jsx'
|
||||
|
|
@ -34,6 +36,8 @@ export function WidgetHost({ widget }) {
|
|||
export const PROVIDER_COMPONENTS = {
|
||||
SteeringSnapshotPanel,
|
||||
CriticalPathPanel,
|
||||
MaturityStagePanel,
|
||||
RecurringRhythmPanel,
|
||||
NextActionWidget,
|
||||
AgentSlotsPanel,
|
||||
SteeringProposalsPanel,
|
||||
|
|
|
|||
|
|
@ -77,7 +77,14 @@ function resolveAgentSlots(input) {
|
|||
* @param {object | null | undefined} 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)
|
||||
for (const key of priority) {
|
||||
if (key === 'work_cycle_scope' && !hasActiveSprint) continue
|
||||
|
|
@ -165,6 +172,16 @@ export function buildProviderProps(provider, input) {
|
|||
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': {
|
||||
const nextActionUi = resolveNextActionUi(elements, input.steeringSnapshot)
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -215,7 +215,33 @@ describe('resolveSteeringComposition', () => {
|
|||
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 withGate = buildProviderProps(provider, {
|
||||
steeringElements: ['gate_fulfillment', 'next_action_primary'],
|
||||
|
|
|
|||
|
|
@ -14,6 +14,15 @@ export const STEERING_ELEMENT_UI = {
|
|||
nextActionSubtitle:
|
||||
'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: {
|
||||
nextActionTitle: 'Nächster sinnvoller Schritt',
|
||||
nextActionSubtitle:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user