🟦 Modul-Vertrag

Wie ein Screen an die CORE-Shell andockt — eine Steckdose, kein iframe · verankert aus PLAT#09

Der Grundsatz: Ein Fach-Screen ist ein Modul, das die Shell in ein leeres Outlet mountet. Die Shell besitzt Scope, Periode, Sprache, Session und Auth — das Modul liest sie (eingefroren), setzt sie nie. Ein Modul erfindet keine eigene Navigation, keine eigene i18n, keinen eigenen Auth-Weg. NULL iframe (JC 23.06.: „auch nicht als Brücke") — Alt-Cockpits treten nativ ein.

Der Vertrag

TGV_MODULES.define("buchhalter", {
  mount(outlet, ctx) { /* PFLICHT — Modul rendert in outlet */ },
  unmount()          { /* optional — Shell bricht ctx.signal ohnehin ab */ },
  onScopeChange(s)   { /* optional — fehlt er, re-mountet die Shell */ },
});
MethodePflichtSemantik
mount(outlet, ctx)✓Modul rendert in outlet (leeres <div>, gehört dem Modul). ctx ist eingefroren.
unmount()–Aufräumen. Die Shell ruft vorher ctx.signal.abort(). Fehlt die Methode, reicht der Abort.
onScopeChange(scope)–Benachrichtigung. Fehlt sie, re-mountet die Shell (= heutiges Verhalten).

Was ein Modul über ctx bekommt (lesen, nie setzen)

FeldZweck
ctx.scope · ctx.periodAktiver Scope / Periode — frozen. Lesen. Nie setzen.
ctx.localeAktive Sprache (Shell besitzt i18n global — Module bringen keine eigene).
ctx.session · ctx.paramsSupabase-Session {email, account} · Route-Parameter, beide frozen.
ctx.signalAbortSignal — jeder fetch reicht ihn durch. Wird beim Unmount abgebrochen.
ctx.restGet(q)PostgREST mit User-JWT → RLS greift. Der bevorzugte Datenpfad.
ctx.skillFetch(n, b)/api/skill/<n>. ⚠️ serverseitig service_role → RLS umgangen (s.u.).
ctx.postToChat(p)Modul → Chat-Zone über den Shell-Event-Bus.
ctx.requestScope(cand)Modul erfragt einen Scope-Wechsel. Die Shell entscheidet.

🔒 Die Sicherheits-Invariante (nicht verhandelbar)

Ein Modul ERFRAGT den Scope. Es SETZT ihn nie. Der Server entscheidet.
Zwei Datenpfade, nicht gleich sicher: restGet läuft über RLS (Scope = harmloser View-Filter) · skillFetch läuft über service_role (RLS umgangen — die App-Schicht ist die einzige Grenze). Ein Modul, das den nutzer-editierbaren localStorage-Scope an einen service_role-Endpoint reicht, wäre das Leck.

Strukturell verriegelt: ctx ist Object.freezed ohne Setter · requestScope prüft gegen authorizedScopes() und delegiert an die Shell · assertContract() weist ein Modul ab, das setScope/setStore exponiert (fail-rot).

⚠️ Offene Naht (Owner MANDANT#01): authorizedScopes() ist heute ein UX-Guard, keine echte Autorisierung — mandant ist serverseitig hart "jcbinetti". Vor dem ersten externen Mandanten blockierend: jeder /api/skill/*-Endpoint MUSS den Client-Scope serverseitig gegen die Memberships schneiden (Intersection, fail-closed).

Legacy-Eintritt — nativ, nicht iframe

TGV_MODULES.adaptLegacy("dokumente", secDokumente);   // kein Rewrite

adaptLegacy(id, renderFn) hüllt eine bestehende render(outlet, params)-Funktion in den Vertrag — ohne Rewrite, ohne eigenes onScopeChange (die Shell re-mountet bei Scope-Wechsel wie heute). Der Weg eines Cockpits ist dreistufig, jede Stufe rückrollbar: Deep-Link-Karte (Vollbild) → adaptLegacy (Render-Fn tritt ein) → define (natives Modul mit onScopeChange + ctx.signal).

Der Selbst-Guard assertContract

Nach dem TGV-Prinzip „regressions-anfällige Regeln → aktive Guards, nicht nur Doku" wirft define() fail-rot, wenn: mount fehlt/keine Funktion · unmount/onScopeChange existieren, aber keine Funktionen sind · das Modul setScope/setStore exponiert (Scope-Setter-Verbot). Zusätzlich prüft die Shell nach jedem mount, dass ctx.scope unverändert ist.

Verankerung, kein Neubau (KANON#01 P2.3): Der Vertrag ist gebaut. SSOT = 01_ITERATIONS/PLAT_09_CORE_FRONTEND_APPSHELL/MODULE_CONTRACT.md · Implementierung = TGV_MODULES/assertContract in 06_DASHBOARD/core_shell.html. Diese Seite macht ihn im Kompendium auffindbar. Im Bau gebunden über den Seitenbau-Kanon (Composer-Skill, Gate D): ein Fach-Screen, der als Modul lebt, MUSS diesen Vertrag erfüllen — sonst weist assertContract ihn ab.

Quellen: PLAT#09 MODULE_CONTRACT.md · core_shell.html (TGV_MODULES, adaptLegacy, assertContract) · IDENTITY_RLS_SSOT_DECISION.md §4 (service_role RLS-bypassed) · MANDANT#01 (offene Auth-Naht).