Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions services/graph-comparator-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Basis-Image: offizielles, schlankes Python-Image.
FROM python:3.12-slim

# Arbeitsverzeichnis im Container. Alle folgenden Befehle laufen hier. /app ist Standard.
WORKDIR /app

# Abhaengigkeiten installieren
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Code kopieren
COPY src/ ./src/

EXPOSE 8000

CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
21 changes: 21 additions & 0 deletions services/graph-comparator-service/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
services:
graph-comparator:
# Dockerfile im aktuellen Ordner benutzen:
build: .
ports:
- "8000:8000"

# Volume-Mount für Live-Reload während der Entwicklung:
# Änderungen in ./app werden sofort im Container sichtbar.
volumes:
- ./src:/app/src
# ^^^^^ ^^^^^^^^
# Host Container


# Befehl der beim Start ausgefuehrt wird (ueberschreibt CMD aus dem Dockerfile).
# Uvicorn, starte einen Server auf Port 8000,
# der bei jedem Request das Objekt namens app aus der Datei app/main.py befragt:

command: uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload
restart: unless-stopped
20 changes: 20 additions & 0 deletions services/graph-comparator-service/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# TODO: Versionen pinnen, wenn die Implementierung steht
# TODO: requirements-dev.txt anlegen
# TODO: requirements-docker.txt anlegen

# DEV
pytest


# DOCKER
fastapi

# Uvicorn ist der empfohlene ASGI-Server für FastAPI.
# "standard" installiert zusätzlich einige nützliche Pakete wie "watchgod" für automatisches Neuladen bei Codeänderungen.
uvicorn[standard]

# Pydantic ist die Datenvalidierungsbibliothek, die FastAPI verwendet.
pydantic


# CODE
1 change: 1 addition & 0 deletions services/graph-comparator-service/src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Empty file.
16 changes: 16 additions & 0 deletions services/graph-comparator-service/src/comparison/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Interface für alle Vergleichs-Implementierungen."""
from __future__ import annotations

from abc import ABC, abstractmethod

from src.comparison.results import ComparisonResult
from src.diagrams.erd.models import Graph


class GraphComparator(ABC):

@abstractmethod
def compare(
self, reference: Graph, candidate: Graph
) -> ComparisonResult:
...
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

from src.comparison.base import GraphComparator
from src.comparison.results import ComparisonResult
from src.diagrams.erd.models import Graph
from src.diagrams.graph_queries import count_edges_by_kind, count_nodes_by_type


class DefaultComparator(GraphComparator):
"""Einfacher Comparator: vergleicht nur Zählwerte."""

def compare(self, reference: Graph, candidate: Graph) -> ComparisonResult:
reference_stats = {
"nodes_by_type": count_nodes_by_type(reference),
"edges_by_kind": count_edges_by_kind(reference),
}
candidate_stats = {
"nodes_by_type": count_nodes_by_type(candidate),
"edges_by_kind": count_edges_by_kind(candidate),
}

match = reference_stats == candidate_stats

return ComparisonResult(
match=match,
differences=[],
stats={
"reference": reference_stats,
"candidate": candidate_stats,
},
confidence=1.0,
warnings=[
"Aktuell werden nur die Anzahlen von Knotentypen und Kantenarten verglichen."
],
pipeline_version="counts.v1",
)
80 changes: 80 additions & 0 deletions services/graph-comparator-service/src/comparison/results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
Ergebnistypen für den Vergleich.
Pydantic-Modelle, damit FastAPI automatisch zu JSON serialisieren kann.
"""
from __future__ import annotations

from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field

DiffOpType = Literal[
"node_missing", # in reference aber nicht in candidate
"node_extra", # in candidate aber nicht in reference
"node_modified", # beide vorhanden, Eigenschaften abweichend
"edge_missing",
"edge_extra",
"edge_modified",
]

Severity = Literal["error", "warning", "info"]



class DiffOperation(BaseModel):
"""
Eine einzelne Abweichung zwischen reference und candidate (nach erd-diff.v1-Schema).
Wird in ComparisonResult.differences gesammelt.
"""
model_config = ConfigDict(extra="forbid")

op: DiffOpType
target: str
severity: Severity = "error"
description: str = ""
details: dict[str, Any] = Field(default_factory=dict)
# TODO: ggf. category für kumulierte Fehler (z.B. "Übermodellierung")



# Pydantic-Modell-Klassen für die Vergleichsergebnisse
class ComparisonResult(BaseModel):
"""Vollständiges Vergleichsergebnis."""

# akzeptiere nur definierte Felder:
model_config = ConfigDict(extra="forbid")

#
# Pflichtfelder
#

# Stimmt mit Musterlösung überein oder nicht?
match: bool


#
# Automatisch generierte Felder aus dem Vergleichsprozess
#

# Alle Abweichungen, die gefunden wurden (leere Liste = keine Abweichungen)
differences: list[DiffOperation] = Field(default_factory=list)

# Feedback-Texte für den Nutzer
feedback: list[str] = Field(default_factory=list)

# Statistiken
stats: dict[str, Any] = Field(default_factory=dict)


#
# Optionale Felder für erweiterte Informationen
# (optional = Defaultwert angegeben)
#

# Wie sicher ist sich das System beim Vergleich? (Standard 1.0 = volle Sicherheit, 0.0 = keine Aussage möglich)
confidence: float = 1.0

warnings: list[str] = Field(default_factory=list)

# ggf. Version der Vergleichspipeline, damit Feedback auf bekannte Fehler oder Einschränkungen bezogen werden kann
pipeline_version: str | None = None
Empty file.
26 changes: 26 additions & 0 deletions services/graph-comparator-service/src/diagrams/erd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# diagrams/erd/

Entity-Relationship-Diagramm nach `erd.v1`-Schema.

## Knotentypen

- `entity` - Entitätstyp (z.B. "Student")
- `relationship` - Beziehungstyp (z.B. "besucht")
- `attribute` - Attribut (z.B. "Name"), gehört über hasAttribute-Edge zu Owner
- `isa` - ISA-Knoten für Generalisierung/Spezialisierung

## Kantentypen

- `participates` - Entity nimmt an Relationship teil (mit Kardinalität)
- `hasAttribute` - Owner (entity/relationship) hat Attribut
- `isaSuper` - ISA-Knoten zeigt auf Supertyp
- `isaSub` - ISA-Knoten zeigt auf Subtyp

## Dateien

- `models.py` - Pydantic-Modelle `Node`, `Edge`, `Graph`.
- `ops.py` - ERD-spezifische Graph-Operationen (Owner-Auflösung, ISA-Hierarchie-Extraktion).

## Schema-Referenz

Siehe `erd_v1_schema.json`
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "erd-diff.v1.schema.json",
"title": "ERD Diff (erd-diff.v1)",
"description": "Format für Korrekturen/Unterschiede zwischen zwei ER-Diagrammen im erd.v1-Format. Beschreibt Operationen, die auf eine Studierenden-Lösung angewendet werden müssen, um die Musterlösung zu erhalten.",
"type": "object",
"additionalProperties": false,
"required": ["operations"],
"properties": {
"schema": {
"type": "string",
"const": "erd-diff.v1"
},
"source": {
"type": "string",
"description": "Referenz/ID der Studierenden-Lösung (Quell-Diagramm)."
},
"target": {
"type": "string",
"description": "Referenz/ID der Musterlösung (Ziel-Diagramm)."
},
"operations": {
"type": "array",
"items": { "$ref": "#/$defs/operation" },
"description": "Geordnete Liste der Operationen. Reihenfolge: erst remove, dann modify, dann add."
}
},

"$defs": {

"operation": {
"type": "object",
"required": ["op", "target"],
"properties": {
"op": {
"type": "string",
"enum": ["add_node", "remove_node", "modify_node", "add_edge", "remove_edge", "modify_edge"],
"description": "Art der Operation."
},
"target": {
"type": "string",
"description": "ID des betroffenen Knotens oder Kanten-Bezeichner (z.B. 'student' oder 'student->besucht')."
},
"description": {
"type": "string",
"description": "Menschenlesbare Beschreibung der Korrektur."
},
"severity": {
"type": "string",
"enum": ["error", "warning", "info"],
"default": "error",
"description": "Schwere des Fehlers: error = strukturell falsch, warning = suboptimal, info = stilistisch."
},
"node_data": {
"$ref": "#/$defs/node_data",
"description": "Vollständige Knoten-Daten (bei add_node)."
},
"edge_data": {
"$ref": "#/$defs/edge_data",
"description": "Vollständige Kanten-Daten (bei add_edge)."
},
"changes": {
"type": "array",
"items": { "$ref": "#/$defs/property_change" },
"description": "Liste der Eigenschaftsänderungen (bei modify_node / modify_edge)."
}
},
"allOf": [
{
"if": { "properties": { "op": { "const": "add_node" } } },
"then": { "required": ["op", "target", "node_data"] }
},
{
"if": { "properties": { "op": { "const": "add_edge" } } },
"then": { "required": ["op", "target", "edge_data"] }
},
{
"if": { "properties": { "op": { "enum": ["modify_node", "modify_edge"] } } },
"then": { "required": ["op", "target", "changes"] }
}
]
},

"property_change": {
"type": "object",
"required": ["property", "from", "to"],
"additionalProperties": false,
"properties": {
"property": {
"type": "string",
"description": "Name der geänderten Eigenschaft (z.B. 'type', 'label', 'key', 'kind', 'cardinality')."
},
"from": {
"description": "Alter Wert (im Studierenden-Diagramm). null wenn vorher nicht vorhanden."
},
"to": {
"description": "Neuer Wert (in der Musterlösung). null wenn entfernt."
}
}
},

"node_data": {
"type": "object",
"required": ["id", "type"],
"properties": {
"id": { "type": "string" },
"type": { "type": "string", "enum": ["entity", "relationship", "attribute", "isa"] },
"label": { "type": "string" },
"flags": { "type": "array", "items": { "type": "string" } },
"key": { "type": "string", "enum": ["NONE", "PK", "PARTIAL"] }
}
},

"edge_data": {
"type": "object",
"required": ["from", "to"],
"properties": {
"from": { "type": "string" },
"to": { "type": "string" },
"kind": { "type": "string", "enum": ["participates", "hasAttribute", "isaSuper", "isaSub"] },
"cardinality": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "type": "string" } }
}
}
}
}
Loading