ontology: Ontologie als Quelle im Repo, Abgleich mit n8n/KNOW in der Service-Routine
ci / test (push) Successful in 27s

- ontology/know.yaml: 49 Klassen, 66 Beziehungen, 12 Vokabulare (72 Konzepte);
  erzeugt: know.ttl (OWL + SKOS), Ansicht aus viewer.html.tmpl
- knaster.ontology: Modell + Prüfung, Export, Momentaufnahme der n8n-Workflows
  (nur GET; KNOW-Spezifikation aus dem Workflow-Code, Aufrufe inkl. Laufzeit-
  Operationen, direktes SQL), Abgleich, Übernahme von Entscheidungen
- Service-Routine Schritt 8: Verstöße als Befunde, Ontologie-Änderungen als
  Vorschläge ontology_change (immer Freigabe); „ordne P-x <konzept> zu“;
  Entscheidungen in ontology_decisions, `knaster ontology apply` schreibt sie
  in die YAML (Version +1)
- CLI: knaster ontology validate|export|snapshot|check|apply; knaster validate
  prüft Modell und Aktualität der Turtle-Datei
- Gegen echten n8n-Export geprüft: neue Befunde B31–B33 (verified/confirmed,
  KAN ruft unbekannte Operation, MPA ordnet Projektereignisse nie ein)
- SPEC-ontology, BETRIEB §6, Router-Image enthält die Ontologie

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 02:54:49 +02:00
co-authored by Claude Opus 5.5
parent 525b29aa6d
commit 9442cc49d1
29 changed files with 3675 additions and 96 deletions
+39 -2
View File
@@ -35,6 +35,7 @@ class MaintenanceConfig(BaseModel):
adapter_min_cases: int = 300
adapter_target_rate: float = 0.90
lookback_days: float = 7.0
ontology_max_findings: int = 15
@classmethod
def from_file(cls, path: str | Path) -> MaintenanceConfig:
@@ -83,7 +84,8 @@ HELP = """Befehle:
- status · hilfe · routine · vorschläge
- genehmige P-3 [P-5 …] · genehmige alle mit risiko niedrig · lehne P-3 ab: <Grund>
- wechsle <slot> auf <basismodell> · trainiere adapter für <cluster> · rollback <slot>
- stufe <skill> zurück · markiere <slot> als ok|degraded|unavailable"""
- stufe <skill> zurück · markiere <slot> als ok|degraded|unavailable
- ordne P-7 <konzept> zu (Vokabel-Vorschlag einem vorhandenen Konzept zuordnen)"""
class LLMIntentMapper:
@@ -120,6 +122,7 @@ COMMANDS: list[tuple[str, re.Pattern[str]]] = [
("rollback", r"^rollback\s+(?P<slot>[\w.-]+)$"),
("demote", r"^(stufe\s+(?P<skill>[\w.-]+)\s+zurück|degradiere\s+(?P<skill2>[\w.-]+))$"),
("mark", r"^markiere\s+(?P<slot>[\w.-]+)\s+(als\s+)?(?P<status>ok|degraded|unavailable)$"),
("map_concept", rf"^ordne\s+(?P<id>{_ID})\s+(dem\s+konzept\s+)?(?P<concept>[\w.-]+)\s+zu$"),
("confirm", r"^(ja|yes|j|ok)$"),
("cancel", r"^(nein|no|n|abbrechen)$"),
]
@@ -143,6 +146,7 @@ class MaintenanceAgent:
def __init__(self, *, store: Store, registry: Registry, library: SkillLibrary, index: SkillIndex,
policy: Policy, actions: ActionExecutor, lifecycle: Lifecycle,
config: MaintenanceConfig | None = None, intent_mapper: IntentMapper | None = None,
ontology_check: Callable[[], list[Any]] | None = None,
clock: Callable[[], float] = now, wallclock: Callable[[], datetime] = datetime.now):
self.store = store
self.registry = registry
@@ -153,6 +157,7 @@ class MaintenanceAgent:
self.lifecycle = lifecycle
self.config = config or MaintenanceConfig()
self.intent_mapper = intent_mapper
self.ontology_check = ontology_check
self.clock = clock
self.wallclock = wallclock
self._pending: dict[str, str] = {} # Sitzung → zu bestätigender Befehl
@@ -197,8 +202,9 @@ class MaintenanceAgent:
def _execute(self, seq: int) -> Proposal:
p = self._load(seq)
params = {**p.params, "decided_by": p.decided_by} if p.kind == "ontology_change" else p.params
try:
result = self.actions.execute(p.kind, p.params)
result = self.actions.execute(p.kind, params)
self.store.update_proposal(seq, state="executed", result=result)
except ActionError as exc:
self.store.update_proposal(seq, state="failed", result={"ok": False, "message": str(exc)})
@@ -212,6 +218,9 @@ class MaintenanceAgent:
result={"message": reason or "abgelehnt"})
if p.kind == "promote_candidate":
self.lifecycle.reject(p.params["candidate"], f"abgelehnt: {reason}")
if p.kind == "ontology_change":
params = {k: v for k, v in p.params.items() if k != "key"}
self.store.set_ontology_decision(p.params["key"], "reject", params, by)
return self._load(seq)
def run_deferred(self) -> list[Proposal]:
@@ -318,6 +327,24 @@ class MaintenanceAgent:
f"{cand['kind']} für {cand['slot']} übernehmen ({cand['target']})",
reason="alle Prüfstufen bestanden", source="lifecycle"))
# 8. Ontologie ↔ Datenhaltung (SPEC-ontology §3)
if self.ontology_check is not None:
try:
ontology_findings = self.ontology_check()
except Exception as exc: # noqa: BLE001 – externe Quelle (n8n-API) darf die Routine nicht abbrechen
report.findings.append(f"Ontologie-Abgleich nicht möglich: {type(exc).__name__}: {exc}")
ontology_findings = []
violations = [f for f in ontology_findings if not f.change]
for f in violations[:c.ontology_max_findings]:
report.findings.append(f"Ontologie [{f.severity}]: {f.summary}")
if len(violations) > c.ontology_max_findings:
report.findings.append(f"Ontologie: {len(violations) - c.ontology_max_findings} weitere Befunde "
"(knaster ontology check)")
for f in ontology_findings:
if f.change:
add(self.propose("ontology_change", {"key": f.key, **f.change}, f.summary, reason=f.detail,
dedup_key=f"ontology:{f.key}"))
for p in self.run_deferred():
report.auto_executed.append(f"{p.id} im Wartungsfenster ausgeführt: {p.summary}")
@@ -446,3 +473,13 @@ class MaintenanceAgent:
return self._new("mark_slot", {"slot": args["slot"], "status": args["status"].lower(),
"reason": f"Auftrag von {user}", "set_by": user},
f"Slot {args['slot']} als {args['status'].lower()} markieren", user)
def _cmd_map_concept(self, args: dict[str, str], user: str) -> CommandReply:
p = self._load(_seq(args["id"]))
if p.kind != "ontology_change" or p.params.get("type") != "vocab_value":
raise ValueError(f"{p.id} ist kein Vokabel-Vorschlag")
if p.state != "open":
raise ValueError(f"{p.id} ist {p.state}")
self.store.update_proposal(p.seq, params={**p.params, "decision": "map", "concept": args["concept"]})
done = self.approve(p.seq, by=user)
return CommandReply(text=self._decided_text(done), proposals=[done])