Some checks failed
Deploy Development / deploy (push) Failing after 45s
Test Suite / pytest-backend (push) Failing after 1s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Review-Auswertung, aktualisierte Roadmap/Gap-Analyse und idempotenter Seed fuer Kairo-Jinkendo als Referenz-Vorhaben mit Gates, Actions und Gitea-Evidence. Co-authored-by: Cursor <cursoragent@cursor.com>
412 lines
13 KiB
Python
412 lines
13 KiB
Python
"""Dogfooding R1 — Referenz-Vorhaben „Jinkendo Kairo“ (idempotent, Nicht-Prod).
|
||
|
||
Spiegelt Plan aus docs/product/ gemäß Kairo_Dogfooding_Mirror_Steering_v0.1.md.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from typing import Any, Optional
|
||
|
||
GITEA_BASE = "http://192.168.2.144:3000/Lars/Kairo-Jinkendo"
|
||
INITIATIVE_TITLE = "Jinkendo Kairo"
|
||
TENANT_SLUG = "stommer"
|
||
|
||
|
||
def _gitea_commit(sha: str, label: str = "") -> str:
|
||
line = f"Commit: [{sha[:7]}]({GITEA_BASE}/commit/{sha})"
|
||
if label:
|
||
return f"{label}\n{line}\nBranch: develop"
|
||
return f"{line}\nBranch: develop"
|
||
|
||
|
||
def _resolve_context() -> tuple[str, str]:
|
||
from db import get_connection
|
||
from psycopg2.extras import RealDictCursor
|
||
|
||
conn = get_connection()
|
||
try:
|
||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||
cur.execute("SELECT id FROM tenants WHERE slug = %s", (TENANT_SLUG,))
|
||
row = cur.fetchone()
|
||
if not row:
|
||
raise RuntimeError(
|
||
f"Tenant '{TENANT_SLUG}' nicht gefunden — zuerst Portal-Admin/seed_003"
|
||
)
|
||
tenant_id = str(row["id"])
|
||
cur.execute(
|
||
"""
|
||
SELECT a.id
|
||
FROM actors a
|
||
WHERE a.tenant_id = %s AND a.actor_type = 'human' AND a.is_active = TRUE
|
||
ORDER BY a.created_at
|
||
LIMIT 1
|
||
""",
|
||
(tenant_id,),
|
||
)
|
||
actor_row = cur.fetchone()
|
||
if not actor_row:
|
||
raise RuntimeError("Kein Human-Actor im Tenant — seed_003 zuerst")
|
||
return tenant_id, str(actor_row["id"])
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def _find_initiative(tenant_id: str) -> Optional[dict[str, Any]]:
|
||
from services.initiatives import list_initiatives
|
||
|
||
for item in list_initiatives(tenant_id=tenant_id):
|
||
if item["title"] == INITIATIVE_TITLE:
|
||
return item
|
||
return None
|
||
|
||
|
||
def _find_by_title(items: list[dict[str, Any]], title: str) -> Optional[dict[str, Any]]:
|
||
for item in items:
|
||
if item["title"] == title:
|
||
return item
|
||
return None
|
||
|
||
|
||
def _close_gate_and_verify(*, tenant_id: str, item_id: str) -> None:
|
||
from services.roadmap import verify_reached
|
||
from services.roadmap_criteria import list_criteria_for_item, waive_criterion
|
||
|
||
for criterion in list_criteria_for_item(tenant_id=tenant_id, item_id=item_id):
|
||
if criterion["status"] not in {"satisfied", "waived", "deferred"}:
|
||
waive_criterion(
|
||
tenant_id=tenant_id,
|
||
criterion_id=criterion["id"],
|
||
decision_title="Dogfooding R1 — historisches Gate",
|
||
decision_description="Rückwirkende Abnahme per Seed.",
|
||
)
|
||
verify_reached(tenant_id=tenant_id, item_id=item_id)
|
||
|
||
|
||
def _ensure_gate_chain(
|
||
*,
|
||
tenant_id: str,
|
||
initiative_id: str,
|
||
gates: list[tuple[str, str, str]],
|
||
) -> dict[str, str]:
|
||
"""gates: (title, status, goal) -> id map."""
|
||
from services.roadmap import add_dependency, create_roadmap_item, list_roadmap_items
|
||
|
||
existing = list_roadmap_items(tenant_id=tenant_id, initiative_id=initiative_id)
|
||
ids: dict[str, str] = {}
|
||
ordered: list[str] = []
|
||
|
||
for index, (title, status, goal) in enumerate(gates):
|
||
found = _find_by_title(existing, title)
|
||
if found:
|
||
gate_id = found["id"]
|
||
else:
|
||
created = create_roadmap_item(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
title=title,
|
||
item_type="milestone",
|
||
goal_description=goal,
|
||
status="planned" if status == "reached" else status,
|
||
sort_order=index * 10,
|
||
initial_criterion_title=f"{title} — Abnahme",
|
||
)
|
||
gate_id = created["id"]
|
||
existing.append(created)
|
||
|
||
ids[title] = gate_id
|
||
ordered.append(gate_id)
|
||
|
||
if status == "reached":
|
||
_close_gate_and_verify(tenant_id=tenant_id, item_id=gate_id)
|
||
|
||
for i in range(1, len(ordered)):
|
||
try:
|
||
add_dependency(
|
||
tenant_id=tenant_id,
|
||
from_item_id=ordered[i],
|
||
to_item_id=ordered[i - 1],
|
||
dependency_type="requires",
|
||
)
|
||
except ValueError as exc:
|
||
if "existiert bereits" not in str(exc):
|
||
raise
|
||
|
||
return ids
|
||
|
||
|
||
def _ensure_projects(
|
||
*, tenant_id: str, initiative_id: str, titles: list[str]
|
||
) -> dict[str, str]:
|
||
from services.projects import create_project, list_projects_for_initiative
|
||
|
||
existing = list_projects_for_initiative(tenant_id=tenant_id, initiative_id=initiative_id)
|
||
ids: dict[str, str] = {}
|
||
for index, title in enumerate(titles):
|
||
found = _find_by_title(existing, title)
|
||
if found:
|
||
ids[title] = found["id"]
|
||
continue
|
||
created = create_project(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
title=title,
|
||
description=f"Ist-Struktur — {title}",
|
||
sort_order=index,
|
||
)
|
||
ids[title] = created["id"]
|
||
existing.append(created)
|
||
return ids
|
||
|
||
|
||
def _ensure_action(
|
||
*,
|
||
tenant_id: str,
|
||
initiative_id: str,
|
||
title: str,
|
||
status: str,
|
||
roadmap_item_id: Optional[str] = None,
|
||
project_id: Optional[str] = None,
|
||
description: str = "",
|
||
) -> dict[str, Any]:
|
||
from services.actions import create_action, list_actions_for_initiative
|
||
|
||
existing = list_actions_for_initiative(tenant_id=tenant_id, initiative_id=initiative_id)
|
||
found = _find_by_title(existing, title)
|
||
if found:
|
||
return found
|
||
return create_action(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
title=title,
|
||
description=description,
|
||
status=status,
|
||
roadmap_item_id=roadmap_item_id,
|
||
project_id=project_id,
|
||
)
|
||
|
||
|
||
def _ensure_evidence(
|
||
*,
|
||
tenant_id: str,
|
||
initiative_id: str,
|
||
title: str,
|
||
description: str,
|
||
roadmap_item_id: Optional[str] = None,
|
||
action_id: Optional[str] = None,
|
||
actor_id: Optional[str] = None,
|
||
) -> None:
|
||
from services.evidence import create_evidence, list_evidence_for_initiative
|
||
|
||
for item in list_evidence_for_initiative(tenant_id=tenant_id, initiative_id=initiative_id):
|
||
if item["title"] == title:
|
||
return
|
||
create_evidence(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
title=title,
|
||
description=description,
|
||
status="accepted",
|
||
roadmap_item_id=roadmap_item_id,
|
||
action_id=action_id,
|
||
submitted_by_actor_id=actor_id,
|
||
)
|
||
|
||
|
||
def _ensure_decision(
|
||
*,
|
||
tenant_id: str,
|
||
initiative_id: str,
|
||
title: str,
|
||
outcome: str,
|
||
) -> None:
|
||
from db import get_connection
|
||
from psycopg2.extras import RealDictCursor
|
||
from services.decisions import create_decision
|
||
|
||
conn = get_connection()
|
||
try:
|
||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||
cur.execute(
|
||
"""
|
||
SELECT 1 FROM decisions
|
||
WHERE tenant_id = %s AND initiative_id = %s AND title = %s
|
||
""",
|
||
(tenant_id, initiative_id, title),
|
||
)
|
||
if cur.fetchone():
|
||
return
|
||
finally:
|
||
conn.close()
|
||
|
||
create_decision(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
title=title,
|
||
description=outcome,
|
||
status="decided",
|
||
outcome=outcome,
|
||
)
|
||
|
||
|
||
def _ensure_portfolio_first(*, tenant_id: str, initiative_id: str) -> None:
|
||
from services.initiatives import list_initiatives, reorder_portfolio
|
||
|
||
all_items = list_initiatives(tenant_id=tenant_id)
|
||
ids = [initiative_id] + [i["id"] for i in all_items if i["id"] != initiative_id]
|
||
reorder_portfolio(tenant_id=tenant_id, initiative_ids=ids)
|
||
|
||
|
||
def run() -> None:
|
||
if os.getenv("ENVIRONMENT", "development").strip().lower() == "production":
|
||
print("[dogfooding] Übersprungen in Production")
|
||
return
|
||
|
||
tenant_id, owner_actor_id = _resolve_context()
|
||
|
||
initiative = _find_initiative(tenant_id)
|
||
if not initiative:
|
||
from services.initiatives import create_initiative
|
||
|
||
initiative = create_initiative(
|
||
tenant_id=tenant_id,
|
||
title=INITIATIVE_TITLE,
|
||
owner_actor_id=owner_actor_id,
|
||
archetype_key="initiative.product",
|
||
method_profile_key="product.kairo_dev",
|
||
goal=(
|
||
"Operativer Program Director — MVP v0.3 Stufe A (B2b). "
|
||
f"Repository: {GITEA_BASE} (Branch develop)"
|
||
),
|
||
vision="Kairo steuert Kairo — Dogfooding Referenz-Vorhaben.",
|
||
target_state_summary="Leitfrage in ≤2 Min; Gates + Ist gespiegelt aus Git/Docs.",
|
||
priority="high",
|
||
)
|
||
print(f"[dogfooding] Initiative angelegt: {initiative['id']}")
|
||
else:
|
||
print(f"[dogfooding] Initiative vorhanden: {initiative['id']}")
|
||
|
||
initiative_id = initiative["id"]
|
||
|
||
gate_defs = [
|
||
("G1 — Foundation AP0", "reached", "Tenant, Actor, Auth, OM, Migrationen"),
|
||
("G2 — IA & Steering Skeleton", "reached", "AP1.2c, AP1.0, Snapshot, Signals"),
|
||
("G3 — Gates & Checkliste", "reached", "AP1.4/4b/4c, Verify, Kriterien"),
|
||
("G4 — Ist-Hierarchie & Plan/Ist", "reached", "AP1.5/5c, AP1.6, AP1.14, Journey"),
|
||
("G5 — PM Work Modes & Plan-Outline", "active", "AP1.9a ✓; AP1.12 ◐; 9b–e offen"),
|
||
("G6 — Archetyp-Steuerung MVP", "active", "AP2.0 ◐; AP2.0d offen"),
|
||
("G7 — MVP-Abnahfe B2b", "planned", "Validation, Leitfrage, Cockpit-Signale"),
|
||
("G8 — Agent & Integration", "planned", "AP1.7, Gitea-Webhook, MCP"),
|
||
]
|
||
gate_ids = _ensure_gate_chain(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
gates=gate_defs,
|
||
)
|
||
|
||
project_ids = _ensure_projects(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
titles=["Backend", "Frontend", "Docs & ADPs", "Deploy / CI"],
|
||
)
|
||
|
||
g5 = gate_ids["G5 — PM Work Modes & Plan-Outline"]
|
||
g6 = gate_ids["G6 — Archetyp-Steuerung MVP"]
|
||
g4 = gate_ids["G4 — Ist-Hierarchie & Plan/Ist"]
|
||
|
||
done_actions = [
|
||
(
|
||
"AP1.6b — Journey Gate-Fortschritt",
|
||
"done",
|
||
g4,
|
||
project_ids["Frontend"],
|
||
"b85a6ec",
|
||
),
|
||
(
|
||
"AP1.8a — Portfolio-Rang Cockpit",
|
||
"done",
|
||
g5,
|
||
project_ids["Frontend"],
|
||
"a19c6ab",
|
||
),
|
||
(
|
||
"AP1.14 — Plan/Ist-Snapshots",
|
||
"done",
|
||
g4,
|
||
project_ids["Backend"],
|
||
"064779e",
|
||
),
|
||
(
|
||
"AP2.0 — Archetyp-Slice",
|
||
"done",
|
||
g6,
|
||
project_ids["Backend"],
|
||
"3bd9832",
|
||
),
|
||
(
|
||
"DOC-Sync — Status-Review & Roadmap",
|
||
"done",
|
||
g6,
|
||
project_ids["Docs & ADPs"],
|
||
None,
|
||
),
|
||
]
|
||
|
||
for title, status, gate_id, project_id, sha in done_actions:
|
||
action = _ensure_action(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
title=title,
|
||
status=status,
|
||
roadmap_item_id=gate_id,
|
||
project_id=project_id,
|
||
)
|
||
if sha:
|
||
_ensure_evidence(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
title=f"Merge {title}",
|
||
description=_gitea_commit(sha, title),
|
||
roadmap_item_id=gate_id,
|
||
action_id=action["id"],
|
||
actor_id=owner_actor_id,
|
||
)
|
||
|
||
open_actions = [
|
||
("AP1.9c — Cockpit-Signale auf Portfolio-Kacheln", g5, project_ids["Frontend"]),
|
||
("AP2.0d — Next-Action-Strategien pro Archetyp", g6, project_ids["Backend"]),
|
||
("Dogfooding R2 — Ist laufend pflegen", g6, project_ids["Docs & ADPs"]),
|
||
]
|
||
for title, gate_id, project_id in open_actions:
|
||
_ensure_action(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
title=title,
|
||
status="open",
|
||
roadmap_item_id=gate_id,
|
||
project_id=project_id,
|
||
)
|
||
|
||
_ensure_decision(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
title="Join/Branch Graph — deferred",
|
||
outcome="OR-Alternativpfade und volle Join-Logik erst bei Use Case (AP1.15d). "
|
||
"Siehe ADP_Roadmap_Graph_and_Gate_Checklist_v0.1.md",
|
||
)
|
||
_ensure_decision(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
title="MCP nach Operational API",
|
||
outcome="MCP-Adapter erst nach AP1.7 (/api/operational). Schicht 4 eingefroren bis B2b-Validation.",
|
||
)
|
||
_ensure_decision(
|
||
tenant_id=tenant_id,
|
||
initiative_id=initiative_id,
|
||
title="MVP v0.3 PO-Freigabe",
|
||
outcome="Abnahfe Stufe A: A1, A2, B2b + B3 minimal. Dokument: Kairo_MVP_Definition_v0.3.md",
|
||
)
|
||
|
||
_ensure_portfolio_first(tenant_id=tenant_id, initiative_id=initiative_id)
|
||
print("[dogfooding] R1 abgeschlossen — Kairo-Jinkendo als Vorhaben gespiegelt")
|