wiki/d-guides/coding-conventions.md
2026-05-16 22:54:27 +02:00

19 KiB

Coding-Konventionen

Naming

  • Alle internen Funktionen beginnen mit _ Prefix (nicht exportierbar)
  • camelCase fuer Variablen und Funktionsnamen (kein snake_case)
  • PascalCase fuer Klassen und Pydantic-Models
  • Dateien: camelCase fuer Module (z.B. mainServiceAi.py, routeBilling.py)

i18n Grundprinzip: Uebersetzen an der Quelle

Jeder Text wird an der Quelle uebersetzt. Backend-Strings werden im Backend uebersetzt, Frontend-Strings im Frontend. Der Empfaenger rendert direkt — keine doppelte Uebersetzung, keine Checks.

Backend liefert IMMER in der korrekten Sprache. Das Frontend muss nichts pruefen oder nochmals uebersetzen.

Text-Quelle Wer uebersetzt Empfaenger
Frontend-Komponente (Button, Label, h1) Frontend: t('Speichern') rendert direkt
Backend statische Struktur (Navigation, Katalog) Backend: t() registriert Key, resolveText() liefert zur Request-Zeit Frontend: item.uiLabel direkt rendern
Backend API-Response (Fehlermeldung, Erfolg) Backend: t("Zugriff verweigert") Frontend: error.detail direkt rendern
Backend DB-Wert (TextMultilingual) Backend: resolveText(role.description) Frontend: role.description direkt rendern

Kernregel: Strings vom Backend sind immer bereits uebersetzt. Das Frontend darf t() nie auf Backend-Werte anwenden — weder t(item.label) noch t(variable). Keine typeof === 'object' Checks, keine Fallback-Ketten. Nur eigene Frontend-Literale wie t('Speichern').

Redundanz vermeiden: Derselbe Text darf nicht an zwei Stellen mit t() getaggt werden.

Frontend (React/TypeScript)

  • Keine Browser-Dialoge (alert, confirm, prompt) -- stattdessen useConfirm() / usePrompt() Hooks
  • CSS Modules fuer Styling
  • Hooks-Pattern fuer State und API-Zugriffe (useApiRequest, useBilling, etc.)
  • Fehler propagieren -- keine stillen Fallbacks bei kritischen Pfaden

i18n-Pflicht: t() fuer alle UI-Texte

Jeder sichtbare Text im UI (Labels, Buttons, Placeholders, Tooltips, Fehlermeldungen) muss mit t() getaggt werden. Hardcodierte deutsche Strings im JSX sind nicht erlaubt.

import { useLanguage } from '../../providers/language/LanguageContext';

const { t } = useLanguage();

// Einfacher Text
t('Speichern')

// Mit Variablen-Interpolation
t('{count} Eintraege gefunden', { count: String(total) })

// Gleicher Text, anderer Kontext → Klammer als Kontext-Hinweis
t('Offen (Status)')   // vs.  t('Offen (Zustand)')
  • Key = deutscher Klartext (kein Dot-Notation-Schema)
  • t() NUR mit String-Literalent(variable) ist verboten. Backend-Werte (Navigation-Labels, Feature-Labels, Role-Descriptions etc.) sind bereits uebersetzt und werden direkt gerendert:
// FALSCH - Backend-Wert nochmal durch t()
label: t(item.uiLabel)        // Backend hat schon uebersetzt!
<h1>{t(plan.title)}</h1>      // Backend hat schon uebersetzt!

// RICHTIG - Backend-Wert direkt rendern
label: item.uiLabel
<h1>{plan.title}</h1>
  • Fuer statische Frontend-Maps (Wochentage, Monatsnamen, Status-Labels) wird t() per switch-Funktion mit Literalen aufgerufen, nicht ueber Map-Lookup:
// FALSCH - t() mit Variable
const labels: Record<string, string> = { budget: 'Budget-Vergleich', kpi: 'KPI-Dashboard' };
{t(labels[tabId])}

// RICHTIG - t() mit String-Literal per switch
function _tabLabel(tabId: string, t: (k: string) => string): string {
  switch (tabId) {
    case 'budget': return t('Budget-Vergleich');
    case 'kpi': return t('KPI-Dashboard');
    default: return tabId;
  }
}
  • Kein Plural-Framework -- separate Keys verwenden: t('1 Eintrag') vs. t('{count} Eintraege', { count })
  • Fehlende Keys erscheinen als [Text] (eckige Klammern = unuebersetzt)
  • Admin synchronisiert Sprachsets ueber Administration → System → UI-Sprachen → "Alle aktualisieren"

Platzhalter-Namen sind Code, nicht Text

Die Token in {...} (z. B. {konten}, {count}) sind interne Variablen-Namen und werden zur Laufzeit durch das Param-Objekt ersetzt. Sie duerfen nie uebersetzt werden — auch wenn sie wie deutsche oder englische Woerter aussehen.

// Quelle (immer Deutsch, weil Source-Sprache):
t('{konten} Konten, {buchungen} Buchungen', { konten: '215', buchungen: '72' })

// Korrekte EN-Uebersetzung in der DB:
'{konten} accounts, {buchungen} entries'
//   ^^^^^^^                ^^^^^^^^^^^   <-- Token bleiben Deutsch!

// FALSCH (wuerde zu rohem '{accounts}' im UI fuehren):
'{accounts} accounts, {entries} entries'

Der AI-Uebersetzer verletzt diese Regel ab und zu, deshalb laeuft in routeI18n._enforcePlaceholdersOnBatch und i18nRegistry._loadCache ein deterministischer Guard, der bei gleicher Token-Anzahl die Source-Namen positionsweise wiederherstellt. Bei abweichender Anzahl bleibt der Wert unangetastet — das muss der Reviewer manuell pruefen.

Backend (FastAPI/Python)

  • Pydantic-Models als einzige Quelle fuer UI-Feld-Definitionen
  • PowerOnModel als Basis mit System-Audit-Feldern (sysCreatedAt, sysCreatedBy, sysModifiedAt, sysModifiedBy)
  • Fehler propagieren -- Exceptions explizit werfen, nicht schlucken
  • Config ueber APP_CONFIG (aus modules/shared/configuration.py)

Async-Routen / Background-Jobs: niemals den Event-Loop blockieren

Alle FastAPI-async def-Routen und alle Background-Job-Handler (registerJobHandler) laufen auf dem gemeinsamen Event-Loop des Workers. Synchroner, blockierender Code (psycopg2, requests, time.sleep, ...) friert damit den ganzen Worker ein -- inklusive Health-Checks. Eine grosse Schleife mit db.recordCreate(...) reicht, um die Instanz fuer Minuten unerreichbar zu machen.

Regeln:

  • HTTP / IO mit nativem await aufrufen (httpx, aiohttp, asyncpg, ...).
  • Sync-Code (psycopg2, lange CPU-Schleifen) immer in await asyncio.to_thread(_syncFn, ...) verpacken.
  • Bei grossen Datenmengen (>100 Zeilen) niemals recordCreate / recordDelete in der Schleife aufrufen. Stattdessen die Bulk-Methoden des Connectors verwenden:
    • db.recordCreateBulk(model, rows) -> ein execute_values + ein COMMIT
    • db.recordDeleteWhere(model, recordFilter) -> ein DELETE WHERE ...
  • Bei laengeren Phasen Heartbeat-Logs alle 500-1000 Items, damit Hangs im Log sichtbar sind.

Referenz-Implementation: modules/features/trustee/accounting/accountingDataSync.py (jede Phase: await connector.fetch(...) -> await asyncio.to_thread(self._persistXxx, ...)).

Datum/Zeit: UTC fuer Storage, Request-TZ fuer User-sichtbare Werte

Backend-Code, der gespeicherte Zeitstempel produziert (DB-Felder, Audit-Logs, Token-Expiry), nutzt UTC via getUtcTimestamp(), getUtcNow() oder getIsoTimestamp() aus modules/shared/timeUtils.py.

Backend-Code, der user-sichtbare "jetzt"-Werte produziert (AI-Agent-System-Prompt, gerenderte Display-Strings, formatierte Logs an den Endnutzer), nutzt getRequestNow() / getRequestTimezone(). Diese lesen die Browser-Zeitzone, die das Frontend per X-User-Timezone-Header schickt (Axios-Interceptor in frontend_nyla/src/api.ts) und die _requestContextMiddleware (app.py) in eine ContextVar schreibt — analog zum _setLanguage-Pattern.

from modules.shared.timeUtils import getUtcTimestamp, getRequestNow

createdAt = getUtcTimestamp()              # DB-Speicherung -> UTC float
nowForUser = getRequestNow()               # Anzeige/Prompt -> tz-aware datetime in User-TZ

Verboten: Hardcoded-Zeitzonen wie ZoneInfo("Europe/Zurich") als Default fuer User-Anzeige. Stattdessen die Request-TZ benutzen; ohne HTTP-Kontext (z.B. Scheduler) faellt getRequestNow() automatisch auf UTC zurueck.

Frontend: User-sichtbare Zeitstempel werden mit formatUnixTimestamp() aus frontend_nyla/src/utils/time.ts formatiert; ohne expliziten timeZone-Override nimmt toLocaleString die Browser-TZ. Backend-Felder bleiben UTC-Floats (number).

Debug-File-Dumps: nur DEV, niemals INT/PROD

Code, der zu Debug-Zwecken raw payloads / Prompts / Sync-Daten auf die Disk schreibt, muss ein dediziertes Env-Flag pruefen. Pattern:

  • APP_DEBUG_<FEATURE>_ENABLED (True nur in env_dev.env)
  • APP_DEBUG_<FEATURE>_DIR (gateway-relativer Pfad ist OK; absoluter Pfad nur fuer DEV-Workstations)

Beispiele: APP_DEBUG_CHAT_WORKFLOW_*, APP_DEBUG_ACCOUNTING_SYNC_*. Hardcoded Windows-Pfade wie D:/... sind verboten (laufen auf Linux als Relativ-Pfad an und schreiben unkontrolliert ins Dateisystem).

i18n-Pflicht: t() fuer alle UI-sichtbaren Gateway-Texte

Jeder Text der im Frontend angezeigt wird (HTTPException-Details, API-Response-Messages, Erfolgs-/Fehlermeldungen) muss mit t() getaggt werden (nur String-Literale in t()).

Dynamische Werte (Feld aus DB, Katalog, verschachteltes Dict): resolveText(wert) aus modules.shared.i18nRegistry verwenden — nicht t(variable). resolveText akzeptiert optional lang="fr" fuer Kontexte ohne Request-Middleware (z.B. Scheduler mit expliziter Nutzersprache).

from modules.shared.i18nRegistry import t

# Fehlermeldung (context automatisch = "api")
raise HTTPException(status_code=403, detail=t("Zugriff verweigert", "api.routeSecurity",
    "Fehlermeldung bei fehlendem Zugriff"))

# Erfolgsmeldung
return {"message": t("Datei erfolgreich hochgeladen", "api.routeFiles",
    "Bestaetigung nach Datei-Upload")}

Nicht mit t() taggen: Log-Eintraege, AI-Prompts, interne technische Fehlermeldungen.

Statische Dicts mit i18n-Keys: t() registriert, resolveText() liefert

Statische Dicts (Navigation, Kataloge) haben zwei Schritte:

  1. Import-Zeit: t() im Dict registriert den Key (und gibt den deutschen String zurueck, der im Dict gespeichert wird)
  2. Request-Zeit: resolveText() in der Route nimmt den deutschen String und liefert die Uebersetzung fuer die aktuelle Sprache
# mainSystem.py — t() registriert Keys bei Import-Zeit
from modules.shared.i18nRegistry import t

NAVIGATION_SECTIONS = [
    {"title": t("Meine Sicht"), "items": [
        {"label": t("Uebersicht"), "objectKey": "ui.system.home", ...},
    ]},
]
# routeSystem.py — resolveText() uebersetzt zur Request-Zeit
from modules.shared.i18nRegistry import resolveText

def _formatBlockItem(item):
    return {"uiLabel": resolveText(item["label"]), ...}

Warum zwei Schritte? t() wird bei Import-Zeit ausgewertet — der Rueckgabewert ist immer deutsch (Default-Sprache). Der Wert im Dict ist daher ein fester deutscher String. resolveText() nimmt diesen deutschen String zur Request-Zeit und liefert die korrekte Uebersetzung.

Warum Modul-Ebene? t() registriert Keys nur beim ersten Aufruf. Wenn t() nur in einer Funktion steht, wird der Key erst beim ersten Request registriert — nach dem Boot-Sync. Der Key fehlt dann im xx-Set und kann nicht uebersetzt werden. Im UI erscheint er als [Schluessel] (eckige Klammern = Cache-Miss).

Pflicht-Hook fuer dynamisch geladene Quellen: Plugin-/Lazy-Module (z.B. Connector-Plugins, dynamisch entdeckte Klassen) deren t()-Aufrufe in einer Methode stecken (nicht auf Modul-Ebene), brauchen einen eigenen _register…Labels()-Hook in _syncRegistryToDb() (modules/shared/i18nRegistry.py). Beispiel: _registerAccountingConnectorLabels() importiert die Accounting-Connector-Registry und ruft getRequiredConfigFields() einmal beim Boot, damit die Field-Labels im xx-Set landen. Analog zu _registerNodeLabels, _registerRbacLabels, etc.

TextMultilingual-Felder: Backend loest auf via resolveText()

TextMultilingual-Felder (User-Content in mehreren Sprachen, z.B. Role.description) werden im Backend via resolveText() aufgeloest bevor sie ans Frontend geliefert werden. Das Frontend erhaelt immer einen String, nie ein Dict.

from modules.shared.i18nRegistry import resolveText

# In Route-Handlern:
"description": resolveText(role.description),
"label": resolveText(featureDef.get("label")) or featureCode,

Keine lokalen Resolve-Helfer: _resolveTextMultilingual(), _resolveLabel(), _storeLabelText(), _featureLabelPlain(), _pickInvocationTitleLabel() etc. sind verboten. Immer resolveText() aus i18nRegistry verwenden.

Frontend: Keine Dict-Fallbacks fuer Labels

Das Frontend darf keine label?.de || label?.en || Object.values(...) Ketten verwenden. Wenn das Backend korrekt arbeitet, sind alle Labels Strings. Defensive typeof === 'object' Checks verdecken Backend-Fehler und sind verboten.

// FALSCH - verdeckt Backend-Fehler
const name = typeof m.name === 'object' ? m.name.de || m.name.en : m.name;

// RICHTIG - Backend liefert String
const name = m.label || m.name || m.id;

Fuer Route-Module gibt es den Shorthand apiRouteContext:

from modules.shared.i18nRegistry import apiRouteContext
routeApiMsg = apiRouteContext("routeBilling")

raise HTTPException(status_code=403, detail=routeApiMsg("Zugriff verweigert"))

Pydantic-Models: @i18nModel Decorator Pflicht

Jedes Pydantic-Model das im UI angezeigt wird (Tabellen, Formulare) muss den @i18nModel Decorator haben. Feld-Labels werden in json_schema_extra["label"] definiert.

from modules.shared.i18nRegistry import i18nModel

@i18nModel("Benutzer")
class User(PowerOnModel):
    name: str = Field(
        description="Full name of the user",
        json_schema_extra={"label": "Name", "frontend_type": "text"}
    )
    email: str = Field(
        description="Email address for login and notifications",
        json_schema_extra={"label": "E-Mail-Adresse", "frontend_type": "text"}
    )
  • @i18nModel("Deutscher Modelname") -- AI-Kontext kommt automatisch aus dem Class-Docstring
  • json_schema_extra={"label": "Deutscher Feldname"} -- Pflicht fuer jedes UI-sichtbare Feld
  • Field(description=...) -- wird als AI-Kontext fuer die Uebersetzung verwendet
  • Kein registerModelLabels() mehr verwenden (entfernt)

Feature-Module: Labels als deutsche Basis-Strings

DATA_OBJECTS, RESOURCE_OBJECTS und UI_OBJECTS Labels verwenden deutsche Klartext-Strings als Basis-Key. Labels werden beim Boot automatisch ueber _registerRbacLabels() registriert und erscheinen im xx-Basisset. Bei Runtime werden sie via resolveText() aufgeloest.

DATA_OBJECTS = [
    {
        "objectKey": "data.uam.UserInDB",
        "label": "Benutzer",
        "meta": {"table": "UserInDB", "namespace": "uam"}
    },
]

Legacy {en, de, fr}-Dicts werden via _extractRegistrySourceText() auf den xx-Key reduziert. Neue Eintraege: immer String, kein Dict.

Projektstruktur Gateway

gateway/
  app.py                          # FastAPI-App, Middleware, Startup
  config.ini                      # Statische Konfiguration
  modules/
    auth/                         # JWT, OAuth, CSRF, Authentication
    datamodels/                   # Pydantic-Models (zentrale Quelle)
    features/                     # Feature-Module (workspace, automation, ...)
      <name>/
        main<Name>.py             # FEATURE_CODE, Registrierung
        routeFeature<Name>.py     # HTTP-Endpunkte
        interfaceFeature<Name>.py # DB-Interface
        datamodelFeature<Name>.py # Feature-spezifische Models
    interfaces/                   # DB-Interfaces (CRUD, Queries)
    routes/                       # Core-Routes (billing, admin, GDPR, ...)
    security/                     # RBAC-Engine
    serviceCenter/
      core/                       # serviceSecurity, serviceUtils, serviceStreaming
      services/                   # serviceAi, serviceChat, serviceAgent, ...
      registry.py                 # Service-Registrierung und Dependencies
    shared/                       # configuration.py, Utilities
    system/                       # registry.py (Feature-Discovery)
    workflows/
      methods/                    # Unified Action Library
      processing/                 # WorkflowProcessor, Modes
      automation/                 # v1 Runtime
      automation2/                # v2 Engine
    connectors/                   # Externe Systeme (DB, SharePoint, Jira, ...)
    aicore/                       # AI-Provider-Plugins, Model-Selector

Feature Data Sub-Agent: Ontologie statt freier Domain-Hints (ab 2026-05)

Wenn ein Feature dem AI-Agent strukturierten Zugriff auf seine Daten gibt (typisch ueber queryFeatureInstance -> Feature Data Sub-Agent), exportiert es eine OntologyDescriptor statt eines freien Hint-Strings.

Alt (deprecated, nur noch als Fallback):

def getAgentDomainHints() -> str:
    return "Bankkonten = accountNumber LIKE '102%'. closingBalance ist already-aggregated, niemals SUM-en. ..."

Neu (Standard ab Mai 2026):

from modules.serviceCenter.services.serviceAgent.datamodelOntology import (
    OntologyDescriptor, Entity, Constraint, ConstraintRule,
    CanonicalQueryPattern, SemanticType,
)

_ONTOLOGY = OntologyDescriptor(
    featureCode="myfeature",
    entities=[
        Entity(
            name="BankAccount",
            pythonClass="MyFeatureAccount",
            semanticType=SemanticType.ACCOUNT,
            parentEntity="Account",
            description="Account with accountNumber LIKE '102%'.",
        ),
    ],
    constraints=[
        Constraint(
            appliesTo="MyFeatureAccountBalance.closingBalance",
            rule=ConstraintRule.NEVER_AGGREGATE,
            message="closingBalance is per-period already; query with periodYear+periodMonth, never SUM/AVG it.",
        ),
    ],
    canonicalPatterns=[
        CanonicalQueryPattern(
            intent="BANK_BALANCE_AT_DATE",
            description="Saldo eines Bankkontos per Jahresende.",
            pattern={
                "tool": "queryTable",
                "tableName": "MyFeatureAccountBalance",
                "filters": [
                    {"field": "accountNumber", "op": "=", "value": "<accountNumber>"},
                    {"field": "periodYear", "op": "=", "value": "<year>"},
                    {"field": "periodMonth", "op": "=", "value": 0},
                ],
                "fields": ["closingBalance", "currency"],
            },
        ),
    ],
)

def getAgentOntology() -> OntologyDescriptor:
    return _ONTOLOGY

Warum: Die Ontologie ist die Single Source of Truth fuer Prompt und Validator. Aenderungen an einer Constraint wirken sowohl auf den AI-Steering-Block als auch auf die deterministische Pre-Execute-Validierung -- es gibt keine Drift zwischen Prompt-Text und Tool-Reject-Logik mehr. Mehr Hintergrund: b-reference/gateway/ai-agent.md Abschnitt "FeatureDataAgent: Query-Repair-Loop + Ontologie" und b-reference/gateway/features/trustee.md als Referenz-Pilot.

Backward-Compatibility: Features, die getAgentOntology() (noch) nicht haben, behalten ihren bestehenden getAgentDomainHints()-Pfad ohne Code-Change. Der Sub-Agent nutzt automatisch den passenden Block.

Anti-Patterns

  • Keine impliziten Type-Conversions in API-Responses
  • Keine DB-Queries in Route-Handlern (immer ueber Interfaces)
  • Kein direkter os.environ-Zugriff -- immer APP_CONFIG
  • Keine hartkodierten Secrets -- verschluesselt in Env-Dateien
  • Neue Features: kein getAgentDomainHints() -> str mehr. Wenn der AI-Agent auf die Feature-Daten zugreifen soll, getAgentOntology() -> OntologyDescriptor exportieren (siehe Abschnitt oben).