Zum Inhalt

Transport-agnostische API-Contracts, Engine-Architektur und Constitution as Contract — Design-Notiz

Status: Informative Design-Notiz
Geltungsbereich: JDL-Sprache, Blueprint-Typsystem, Engine-Architektur, Stdlib-Architektur, Transport-Layer, Semantische Analyse, Intrinsics-Integration, I/O-Modell
Vorstufe zu: jdl::api, jdl::http, jdl::grpc, jdl::ws, Constitution-as-Contract-Spec, VM-I/O-Spec
Bezieht sich auf: Blueprint-Spec, Ontologie (Tags & Phantoms), Sprachüberblick (Function Unions), Wire-Integration, Design Constitution, CompilerDB-Spec, Intrinsics & VM API, TypeFn-Spezifikation
Autor: Elias

Konsolidierungshinweis: Diese Notiz wurde auf das aktuelle Engine-/Descriptor- und Constitution-Modell angepasst. Engine.validate/execute wird informativ als alte Kurzform behandelt; normativ gilt describe -> Descriptor -> instantiate. Constitutionen sind keine freien CompilerDB-Query-Lambdas, sondern deklarative ConstitutionSpecs, die von der ConstitutionEngine zu ConstitutionDescriptoren materialisiert werden.


1. Ausgangspunkt und Motivation

Die Blueprint-Spec definiert in Section 7.3 einen RouteSpec-Blueprint für HTTP-Server-Routing und in Section 7.6 einen ApiContract-Blueprint für geteilte Client/Server-Verträge. Beide Konzepte adressieren Teilaspekte desselben Problems: zwei Parteien kommunizieren über einen typisierten Vertrag.

Die Parallele zu den Parser-Generatoren ist aufschlussreich. ParserSpec[T] beschreibt format-agnostisch, wie ein Eingabeformat in einen getypten JDL-Wert transformiert wird. Verschiedene Engines interpretieren denselben Blueprint für unterschiedliche Formate — JSON, YAML, Markdown, Protobuf. Der ParserSpec weiß nichts über das konkrete Format; das ist Engine-Verantwortung.

Dieselbe Trennung lässt sich auf API-Kommunikation anwenden. Ein API-Contract beschreibt was eine Schnittstelle kann — welche Operationen existieren, welche Typen fließen, welche Interaktionsmuster gelten. Wie die Kommunikation transportiert wird — HTTP, gRPC, WebSocket, IPC — ist eine separate Entscheidung, die in der Transport-Engine liegt.

Der entscheidende Unterschied zu Parsing: bei Parsing ist die Richtung klar (Bytestream rein, getypter Baum raus). Bei APIs gibt es zwei Richtungen und zwei Rollen — Client und Server — die aus derselben Beschreibung unterschiedliche Artefakte ableiten. Das erfordert eine sauberere Trennung der Ebenen.


2. Die Grundidee: Contract und Transport trennen

Die Architektur teilt sich in zwei Pakete:

jdl::api definiert den Contract-Blueprint — die transport-agnostische Beschreibung einer API. Dieses Paket weiß nichts über HTTP-Verben, gRPC-Service-Descriptoren oder WebSocket-Frames.

jdl::http, jdl::grpc, jdl::ws und weitere Pakete liefern jeweils Transport-Engines, die einen Contract konsumieren und daraus Client- bzw. Server-Artefakte ableiten.

┌───────────────────────────────────────────────────────┐
│                      jdl::api                         │
│                                                       │
│   Contract Blueprint — transport-agnostisch           │
│   Beschreibt: Operationen, Typen, Interaktionsmuster  │
│   Weiß nicht: wie Bytes über die Leitung gehen        │
└───────────────────────┬───────────────────────────────┘
            ┌───────────┼───────────┐
            │           │           │
            ▼           ▼           ▼
      ┌──────────┐ ┌──────────┐ ┌──────────┐
      │jdl::http │ │jdl::grpc │ │ jdl::ws  │
      │          │ │          │ │          │
      │ Transport│ │ Transport│ │ Transport│
      │ Engine   │ │ Engine   │ │ Engine   │
      └──────────┘ └──────────┘ └──────────┘

Die Trennung ist nicht nur kosmetisch — sie hat konkrete Auswirkungen auf das Typsystem. Der Contract kann zur Compile-Zeit geprüft werden, ohne dass der Compiler etwas über HTTP wissen muss. Die Transport-Engine kann zur Laufzeit validieren, ob sie alle Interaktionsmuster des Contracts unterstützt. Und derselbe Contract kann von mehreren Transport-Engines gleichzeitig bedient werden.

Warum transport-agnostisch?

Typisierte Kommunikation zwischen zwei Parteien findet nicht nur über Web-APIs statt. Dasselbe Muster taucht auf bei Actor-Messaging (der ActorSpec aus der Blueprint-Spec hat bereits receive: (S, M) -> (S, R) — ein Request-Response-Contract), bei IPC zwischen Prozessen, bei Plugin-Kommunikation über Ring-Grenzen, bei Node-to-Node-Kommunikation im geplanten JadeOS-Mesh, und beim LSP-Protokoll (bidirektionale Kommunikation mit Requests und Notifications).

Das Pattern ist immer dasselbe: zwei Seiten, ein geteilter Vertrag, typisierte Nachrichten, ein oder mehrere Interaktionsmuster. Der Transport ist austauschbar. Ein transport-agnostischer Contract deckt alle diese Szenarien ab, ohne dass für jedes ein eigenes Konzept erfunden werden muss.


3. Interaktionsmuster als Tag-Enum

Jede Operation in einer API folgt einem von fünf fundamentalen Kommunikationsmustern. Diese Muster sind nicht erfunden, sondern ergeben sich aus der Realität verteilter Systeme. gRPC definiert explizit vier davon (Unary, Server-Streaming, Client-Streaming, Bidirectional-Streaming). HTTP bildet primär Request-Response ab. WebSocket ist nativ bidirektional.

Die fünf Muster:

Request-Response — der klassische RPC-Aufruf. Der Client sendet eine Anfrage, der Server antwortet mit einem Ergebnis. HTTP GET/POST, gRPC Unary, synchroner Funktionsaufruf.

Server-Stream — der Client sendet eine Anfrage, der Server antwortet mit einem Datenstrom. HTTP Server-Sent Events, gRPC Server-Streaming, Datenbankabfragen mit Cursor.

Client-Stream — der Client sendet einen Datenstrom, der Server antwortet mit einem einzelnen Ergebnis. Datei-Upload, Batch-Verarbeitung, Log-Aggregation.

Bidirektional — beide Seiten senden und empfangen gleichzeitig. WebSocket-Kanäle, gRPC Bidirectional-Streaming, Chat-Protokolle.

Fire-and-Forget — der Client sendet eine Nachricht ohne Antwort zu erwarten. Event-Emission, Audit-Logging, Telemetrie.

Diese Muster sind reine Compile-Zeit-Bedeutung — es existieren davon keine Laufzeitwerte. Die Frage "ist diese Operation ein Request-Response oder ein Stream?" wird zur Compile-Zeit beantwortet, nicht zur Laufzeit. Das macht sie zu Tags, nicht zu Types — konsistent mit der Ontologie-Spec (Dokument 00).

tag InteractionMode : enum =
    | RequestResponse
    | ServerStream
    | ClientStream
    | Bidirectional
    | FireAndForget

Die Varianten von InteractionMode sind keine Runtime-Werte. Man kann keinen val mode = RequestResponse schreiben. Sie existieren ausschließlich auf der Typ- und Meta-Ebene — genau wie OwnPolicy, SharePolicy und die Arena-Marker in der bestehenden Spec.

Transport-Kompatibilität

Nicht jeder Transport kann jedes Muster abbilden. HTTP/1.1 unterstützt im Wesentlichen Request-Response und (mit Einschränkungen über Server-Sent Events) Server-Streaming. WebSocket ist nativ bidirektional. gRPC unterstützt alle vier gerichteten Muster. Ein reiner UDP-Transport könnte nur Fire-and-Forget sinnvoll abbilden.

Diese Kompatibilität wird nicht durch Konvention geregelt, sondern durch das Typsystem erzwungen. Die Transport-Engine deklariert, welche InteractionMode-Varianten sie unterstützt. Wenn ein Contract eine Bidirectional-Operation enthält und eine HTTP/1.1-Engine angebunden wird, die kein WebSocket-Upgrade kann, ist das ein Fehler in der describe-/Descriptor-Phase der TransportEngine — nicht ein Laufzeit-Crash irgendwo tief im Stack.


4. Die Op-Typen — Operationen als Phantom-annotierte Werte

Jede Operation in einem Contract wird durch einen Op-Typ beschrieben. Dieser Typ trägt vier Informationen: den Input-Typ, den Output-Typ, den Fehlertyp und das Interaktionsmuster. Das Muster ist ein Phantom-Parameter — es beeinflusst die Typidentität, erzeugt aber keine Runtime-Repräsentation (Ontologie-Spec, Regel T-7 und T-8).

type Op[In, Out, E, phantom Mode: InteractionMode] : struct {}

Op ist ein leerer Struct. Er trägt keine Daten — seine gesamte Bedeutung steckt in den Typparametern. Zur Laufzeit ist ein Op-Wert ein Nullwert. Zur Compile-Zeit ist er ein vollständiger Operationsvertrag.

Für Ergonomie definiert die Stdlib Typ-Aliasse, die den Phantom-Parameter vorbelegen:

type Rpc[In, Out, E]          = Op[In, Out, E, RequestResponse]
type ServerStream[In, Out, E] = Op[In, Out, E, ServerStream]
type ClientStream[In, Out, E] = Op[In, Out, E, ClientStream]
type Channel[In, Out, E]      = Op[In, Out, E, Bidirectional]
type Signal[In]               = Op[In, (), (), FireAndForget]

Rpc ist die häufigste Form: ein Input geht rein, ein Ergebnis kommt raus. Signal ist die einfachste: nur ein Input, kein Output, kein Fehler. Channel ist die komplexeste: beide Seiten senden und empfangen.

Die Transport-Engine liest den Phantom-Parameter zur Compile-Zeit oder über Typ-Inspektion zur Laufzeit. Für HTTP bedeutet Rpc einen POST-Endpoint, ServerStream Server-Sent Events oder Chunked Response, Channel ein WebSocket-Upgrade. Für gRPC wird Rpc zu einem Unary-RPC, ServerStream zu einem Server-Streaming-RPC, und so weiter.


5. Der Contract-Blueprint

Ein Contract ist ein Blueprint, dessen Felder Op-Typen sind. Jedes Feld repräsentiert eine Operation. Der Feldname ist der Operationsname.

type UserService : blueprint {
    getUser:      Rpc[UserId, User, ApiError]
    listUsers:    ServerStream[UserFilter, User, ApiError]
    createUser:   Rpc[CreateUserRequest, User, ApiError]
    deleteUser:   Rpc[UserId, (), ApiError]
    watchChanges: Channel[WatchConfig, UserEvent, ApiError]
    notifyLogin:  Signal[LoginEvent]
} :> Queryable()
  :> Versioned(SemVer(1, 0, 0))

Was der Compiler hier prüft:

Alle In- und Out-Typen (UserId, User, UserFilter, CreateUserRequest, etc.) müssen Serializable implementieren. Das ist keine neue Regel — die Blueprint-Spec erzwingt Serialisierbarkeit auf allen Blueprint-Feldern. Da Op ein Struct ist dessen Typparameter diese Typen enthalten, greift die bestehende Compiler-Regel transitiv.

Die InteractionMode-Tags müssen gültige Varianten der Tag-Domain sein. Ein Op[UserId, User, ApiError, InvalidTag] wäre ein Compile-Fehler.

:> Queryable() injiziert die Standard-Metadaten (id, name, class) via TypeFn — der Contract wird damit adressierbar und selektierbar durch Engines, genau wie in der Blueprint-Spec definiert.

:> Versioned(SemVer(1, 0, 0)) ist ein Domain-Refinement, das von der Engine gelesen wird, um API-Versionierung zu unterstützen.

Was der Contract bewusst nicht enthält

Keine HTTP-Verben. Kein Pfad-Routing (/users/{id}). Kein Content-Type. Keine Header-Definitionen. Keine Auth-Konfiguration. Kein Wire-Format.

All das sind Transport-Details, die in der jeweiligen Transport-Engine konfiguriert werden. Der Contract beschreibt die Semantik der API, nicht ihre Drahtrepräsentation. Das ist der Kern der transport-agnostischen Architektur.


6. Die Architektur-Schichten

Die gesamte Verdrahtung besteht aus vier Schichten, die sauber voneinander getrennt sind. Jede Schicht hat eine klar definierte Verantwortung und kommuniziert nur mit ihren direkten Nachbarn.

┌─────────────────────────────────────────────────────────────┐
│                    Bedeutungswelt (Compile-Zeit)             │
│                                                             │
│   tag InteractionMode : enum = | RequestResponse | ...      │
│                       │                                     │
│                       ▼ phantom                             │
│   Op[In, Out, E, phantom Mode: InteractionMode] : struct {} │
└─────────────────────────────┬───────────────────────────────┘
                              │ Felder verwenden Op-Typen
┌─────────────────────────────▼───────────────────────────────┐
│                    Contract-Schicht (jdl::api)               │
│                                                             │
│   UserService : blueprint {                                 │
│       getUser:    Rpc[UserId, User, ApiError]                │
│       listUsers:  ServerStream[UserFilter, User, ApiError]   │
│       ...                                                   │
│   }                                                         │
└─────────────────────────────┬───────────────────────────────┘
                              │ konsumiert via RuntimeEngine[Spec, Desc, Out]
            ┌─────────────────┼─────────────────┐
┌───────────▼──────┐ ┌───────▼────────┐ ┌──────▼───────────┐
│  Transport-Schicht│ │                │ │                  │
│  jdl::http       │ │  jdl::grpc     │ │  jdl::ws         │
│  HttpTransport   │ │  GrpcTransport │ │  WsTransport     │
│                  │ │                │ │                  │
│  Rpc → POST      │ │  Rpc → Unary   │ │  Channel → Frame │
│  Stream → SSE    │ │  Stream → gRPC │ │  Signal → Send   │
│  Channel → WS    │ │  Stream        │ │                  │
└────────┬─────────┘ └───────┬────────┘ └──────┬───────────┘
         │                   │                  │
         └───────────────────┼──────────────────┘
                             │ produziert
              ┌──────────────┼──────────────┐
              ▼                             ▼
        ServerHandle                  TypedClient
        (Lifecycle)                   (typisierter Proxy)

Schicht 1 — Bedeutungswelt (Compile-Zeit). Die InteractionMode-Tags und der Phantom-Parameter auf Op existieren ausschließlich zur Compile-Zeit. Sie erzeugen keine Runtime-Repräsentation, beeinflussen kein Memory-Layout und kosten keine Laufzeit. Ihre einzige Funktion ist, dem Typsystem Semantik mitzuteilen: "Diese Operation ist ein Request-Response" oder "Diese Operation ist bidirektional."

Schicht 2 — Contract (jdl::api). Der Blueprint beschreibt die API-Semantik. Er ist ein regulärer Laufzeitwert — serialisierbar, über das Netzwerk übertragbar, von Engines inspizierbar. Er lebt im jdl::api-Paket und kennt keine Transport-Details.

Schicht 3 — Transport (jdl::http, jdl::grpc, etc.). Die Transport-Engines konsumieren den Contract über das RuntimeEngine[Spec, Desc, Out]-Protocol. Sie lesen die Phantom-Modi der Felder und bilden sie auf ihr Transport-Äquivalent ab. Jede Engine wählt auch das Wire-Format für die Serialisierung der In/Out-Typen.

Schicht 4 — Artefakte (ServerHandle, TypedClient). Was die Engine produziert. Ein ServerHandle repräsentiert einen laufenden Server mit Lifecycle-API (stop, pause, status). Ein TypedClient ist ein typisierter Proxy, dessen Methoden die Contract-Operationen widerspiegeln. Beide sind reguläre JDL-Werte mit eigenen Handles — Lifecycle liegt auf den Handles, nicht auf den Engines, konsistent mit der Blueprint-Spec (Section 6.6).


7. Wire-Serialisierung — Engine-Verantwortung, nicht Contract-Semantik

Die Frage "wie werden User, UserId, ApiError in Bytes konvertiert?" wird nicht im Contract beantwortet. Der Contract erzwingt nur, dass alle In- und Out-Typen Serializable implementieren — das ist eine bestehende Compiler-Regel für Blueprint-Felder. Welches Wire-Format verwendet wird, entscheidet die Transport-Engine.

Die Wire-Spec (Dokument 04, Section 6) definiert WireType als Protocol für formatspezifische Serialisierung. Für einen Typ wie User können mehrere Wire-Implementierungen koexistieren:

provide Wire[Json] for User { ... }
provide Wire[Protobuf] for User { ... }
provide Wire[MessagePack] for User { ... }

Die Transport-Engine wählt basierend auf ihrer Konfiguration:

val httpEngine = HttpTransport {
    wireFormat: Wire.Json
    port: 8080
}

Alle In/Out-Typen werden über die konfigurierte Wire-Implementierung serialisiert und deserialisiert. Der Handler sieht nie Bytes — er arbeitet ausschließlich mit getypten JDL-Werten.

Warum Wire nicht am Op-Typ hängt

Es wäre verlockend, den Op-Typ direkt mit einem ParserSpec zu verbinden — etwa Rpc[UserId, User, ApiError, Json]. Das würde die Transport-Agnostik brechen: derselbe User-Typ soll über HTTP als JSON, über gRPC als Protobuf und über lokale IPC möglicherweise gar nicht serialisiert werden (weil er im selben Adressraum bleibt). Die Entscheidung "welches Wire-Format" gehört zur Transport-Engine, nicht zum Contract.

                  Contract
          "was fließt" (Typen)
         ┌───────────┼───────────┐
         ▼           ▼           ▼
    HTTP Engine   gRPC Engine   IPC Engine
    Wire[Json]    Wire[Proto]   (kein Wire)
         │           │           │
    Bytes (JSON)  Bytes (PB)   Direkte Referenz

8. ServiceBinding — Handler an den Contract binden

Der Contract beschreibt was die API kann. Die Transport-Engine beschreibt wie der Transport funktioniert. Aber wer liefert die eigentliche Logik? Wenn getUser: Rpc[UserId, User, ApiError] im Contract steht, muss irgendwo eine Funktion (UserId) -> Result[User, ApiError] existieren.

Die Lösung ist ein ServiceBinding — ein Blueprint, der Contract und Handler-Implementierungen bündelt.

type ServiceBinding[C] : blueprint {
    contract:  C
    handlers:  HandlerMap[C]
}

HandlerMap[C] ist eine TypeFn, die aus den Contract-Feldern die passenden Funktionssignaturen ableitet. Die Transformation ist vom Phantom-Mode der jeweiligen Operation abhängig:

Feld im Contract                         Abgeleitete Handler-Signatur
──────────────────────────────────────    ─────────────────────────────────────────────
getUser:    Rpc[UserId, User, E]      →  getUser:    (UserId) -> Result[User, E]
listUsers:  ServerStream[Filter, U, E]→  listUsers:  (Filter) -> Generator[Result[U, E]]
upload:     ClientStream[Chunk, R, E] →  upload:     (Generator[Chunk]) -> Result[R, E]
chat:       Channel[MsgIn, MsgOut, E] →  chat:       (Generator[MsgIn]) -> Generator[Result[MsgOut, E]]
notify:     Signal[Event]             →  notify:     (Event) -> ()

Die Ableitung ist rein mechanisch: der Phantom-Mode bestimmt, ob Input/Output einzelne Werte oder Generatoren sind, und ob ein Result-Wrapper existiert. Signal hat keinen Output und keinen Fehlertyp.

Eine konkrete Instanziierung:

val binding = ServiceBinding {
    contract: userService
    handlers: {
        getUser:      (id) => db.findById(id)
        listUsers:    (filter) => db.query(filter)
        createUser:   (req) => db.insert(req)
        deleteUser:   (id) => db.delete(id)
        watchChanges: (config) => db.watchStream(config)
        notifyLogin:  (event) => audit.log(event)
    }
}

Der Compiler prüft, dass jedes Contract-Feld einen Handler mit passender Signatur hat. Fehlt ein Handler oder stimmt der Typ nicht, ist das ein Compile-Fehler — nicht ein Laufzeitproblem.

Asymmetrie: Server braucht Binding, Client nicht

Die Server-Engine konsumiert das ServiceBinding, weil sie Handler braucht:

provide RuntimeEngine[ServiceBinding[UserService], HttpServerDescriptor, ServerHandle] for HttpTransport { ... }

Die Client-Engine konsumiert den nackten Contract, weil sie keine Handler braucht — sie generiert einen typisierten Proxy:

provide DescriptorEngine[UserService, TypedClientDescriptor] for HttpClient { ... }

Das ist die natürliche Asymmetrie von Client/Server: der Server implementiert, der Client konsumiert. Beide teilen den Contract, aber nur der Server hat ein Binding.

Offener Punkt: HandlerMap als TypeFn

Die aktuelle TypeFn-Spec definiert TypeFns als kontrollierte Compile-Time-Operationen auf TypeSubjects, die Patches und Descriptoren vorbereiten. HandlerMap bräuchte eine strukturelle Transformation auf Blueprint-Feldern — die Fähigkeit, aus den Feldern eines Typs die Felder eines anderen Typs abzuleiten. Das ist konzeptionell dasselbe Prinzip (Typ-Transformation zur Compile-Zeit), aber ein Capability-Ausbau der TypeFn-Mechanik, der in einer eigenen Spec adressiert werden sollte.


9. Function Unions als Handler-Typen

Die Sprachgrundlagen-Spec (Dokument 01, Section 14) definiert Function Unions — eine Union von Funktionssignaturen, bei der jeder Wert genau eine Operation inklusive ihrer Argumente repräsentiert.

Function Unions sind das natürliche Primitiv für Actor-Messages in diesem Kontext. Statt separate Message-Structs und Response-Enums zu definieren, trägt die Function Union die komplette Signatur pro Operation — Input und Output — in einem Typ:

type UserCommand =
    | def getUser(id: UserId)                            -> Result[User, ApiError]
    | def createUser(req: CreateUserRequest)              -> Result[User, ApiError]
    | def listUsers(filter: UserFilter)                   -> Result[[User], ApiError]
    | def deleteUser(id: UserId)                          -> Result[(), ApiError]

Die Function Union lässt sich direkt als Message-Typ eines Actors verwenden:

val userActor = ActorSpec[UserState, UserCommand] {
    init: () => UserState { db: connectDb() }
    receive: (state, cmd) => match cmd {
        | getUser(id)        => (state, state.db.findById(id))
        | createUser(req)    => (state, state.db.insert(req))
        | listUsers(filter)  => (state, state.db.query(filter))
        | deleteUser(id)     => (state, state.db.delete(id))
    }
    supervision: SupervisionPolicy.Restart { maxRestarts: 5 }
}

Der Compiler erzwingt exhaustives Matching — vergisst man eine Variante, ist das ein Compile-Fehler. Und jeder Arm gibt direkt den Rückgabetyp seiner Variante zurück — der Compiler prüft die Typkonsistenz pro Arm.

Verbindung zum Contract

Die Function Union ist im Grunde die HandlerMap in einer anderen Form. Wenn der Contract die Operationen als Op[In, Out, E, Mode] definiert, dann ist die zugehörige Function Union die natürliche Ableitung davon — eine Function Union, deren Varianten exakt die Signaturen der Contract-Felder widerspiegeln. Das ist kein neuer Mechanismus, sondern ein bestehender Typ, der sich aus dem Contract ableiten lässt.


10. Actor-Integration — Blueprints verschachteln

Die API-Schicht, die Actor-Schicht und die Transport-Schicht lassen sich zusammenstecken, ohne dass eine davon die andere kennen muss. Das demonstriert die Kompositionsfähigkeit von Blueprints (Blueprint-Spec, Section 8.1).

Ein konkretes Szenario: der HTTP-Server nimmt Requests entgegen, die Handler routen sie an einen Actor-Pool, die Actors verarbeiten sie mit Supervision. Drei Blueprints, drei Engines, jede auf ihrer Ebene:

┌──────────────────────────────────────────────────────────────────┐
│ ServiceBinding                                                   │
│                                                                  │
│   contract: UserService                                          │
│   handlers: {                                                    │
│       getUser:    (id) => actorPool.ask(getUser(id))             │
│       createUser: (req) => actorPool.ask(createUser(req))        │
│   }                                                              │
│       │                                                          │
│       │ actorPool kommt aus:                                     │
│       ▼                                                          │
│   ┌──────────────────────────────────────────────────────────┐   │
│   │ ActorSpec[UserState, UserCommand]                         │   │
│   │                                                          │   │
│   │   init:    () => UserState { db: connectDb() }           │   │
│   │   receive: (state, cmd) => match cmd { ... }             │   │
│   │   supervision: Restart { maxRestarts: 5 }                │   │
│   │                                                          │   │
│   │   UserCommand = Function Union der Operationen           │   │
│   └──────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────┘
         │ konsumiert von
   HttpTransport Engine → ServerHandle

Die Typsicherheit zieht sich durch alle Grenzen: der Actor muss UserCommand als Message akzeptieren und den passenden Rückgabetyp pro Variante liefern. Der Handler im ServiceBinding muss den Rückgabetyp des Actors auf den Contract-Rückgabetyp abbilden. Der Contract erzwingt, dass der Client die richtigen Typen sendet. Stimmt irgendwo eine Signatur nicht, ist das ein Compile-Fehler.

Der Actor ist in diesem Setup austauschbar. In der Entwicklung könnte der Handler direkt auf die Datenbank zugreifen, ohne Actor-Pool. In der Produktion schaltet man den Actor-Pool dazwischen, um Throughput zu erhöhen. Der Contract bleibt identisch, nur die Handler-Verdrahtung ändert sich.


11. Fehlerbehandlung — drei Schichten

Die Fehlerbehandlung teilt sich entlang der Architekturschichten auf. Jede Schicht ist für ihre eigenen Fehler verantwortlich.

┌─────────────────────────────────────────────────────────┐
│  Application-Errors (Handler-Ebene)                      │
│                                                         │
│  Das E in Op[In, Out, E, Mode]. ApiError, UserError,    │
│  was auch immer der Handler zurückgibt. Typisiert,       │
│  dem Compiler bekannt, fließen über den Contract.        │
│  Die Engine serialisiert sie via Wire[Format] und        │
│  schickt sie als typisierte Fehlerantwort zurück.        │
├─────────────────────────────────────────────────────────┤
│  Transport-/Wire-Errors (Engine-Ebene)                   │
│                                                         │
│  Netzwerk bricht ab, JSON ist kaputt, Timeout, TLS-     │
│  Handshake fehlgeschlagen. Der Handler weiß nichts       │
│  davon, der Contract weiß nichts davon. Die Engine       │
│  fängt sie und übersetzt sie in ihr Transport-           │
│  Äquivalent (HTTP 400, 503, Connection-Reset etc.)       │
├─────────────────────────────────────────────────────────┤
│  Engine-Errors (Infrastruktur-Ebene)                     │
│                                                         │
│  Routing findet keine Operation, validate() schlägt      │
│  fehl, Engine-Konfiguration ungültig. Rein interne       │
│  Fehler, die nie beim Handler ankommen.                  │
└─────────────────────────────────────────────────────────┘

Server-Seite

Auf dem Server ist die Schichtung sauber: die Engine deserialisiert den Request, routet zur passenden Operation, ruft den Handler, nimmt das Result entgegen und serialisiert die Antwort. Fehler in jeder Schicht werden an der Schichtgrenze behandelt, nicht durchgereicht. Der Handler arbeitet ausschließlich mit getypten JDL-Werten — er sieht nie Bytes, nie HTTP-Headers, nie JSON.

Client-Seite

Auf der Client-Seite muss der Rückgabetyp beide Fehlerklassen abbilden. Application-Errors kommen typisiert vom Server zurück, aber Transport-Fehler kommen obendrauf. Die Lösung ist ein Union-Typ:

type ClientError[E] : union = E | TransportError | WireError

Der TypedClient.getUser(id) gibt Result[User, ClientError[ApiError]] zurück. Der Aufrufer kann differenziert matchen:

match client.getUser(id) {
    | Ok(user)                    => handleUser(user)
    | Err(ApiError.NotFound { }) => show404()
    | Err(TransportError { })    => retryOrFail()
    | Err(WireError { })         => logCorruption()
}

Composable Refinements

Die Refinements aus der Blueprint-Spec (Section 8.2) greifen genau hier: :> Retry fängt TransportError und wiederholt den Versuch, :> Timeout fügt eine Deadline hinzu, :> CircuitBreaker trackt Fehlerraten und öffnet den Schutzschalter. Die Refinement-Middleware operiert auf dem Result bevor es beim Aufrufer ankommt — der Handler sieht davon nichts, der Client-Code sieht nur das Endergebnis nach allen Retry-Versuchen.

Die Typconstraints regeln automatisch, wo was passt. Retry funktioniert nur wenn der Output ein Result ist. Ein Retry auf einer Engine die ActorRef zurückgibt ist ein Typfehler, weil kein provide EngineMiddleware[Retry, ActorRef] existiert — genau wie in der Blueprint-Spec beschrieben.


12. Request-Lifecycle — der vollständige Durchlauf

Ein konkreter Request-Response-Aufruf durchläuft die Schichten wie folgt. Das Beispiel zeigt getUser über HTTP mit JSON-Serialisierung.

           CLIENT                                  SERVER
           ══════                                  ══════

     ┌────────────────┐
     │  Client Code   │
     │  client.       │
     │  getUser(id)   │
     └───────┬────────┘
     ┌────────────────┐
     │  TypedClient   │
     │                │
     │  1. Op-Modus   │
     │     lesen:     │
     │     → Rpc      │
     │  2. Wire[Json] │
     │     .serialize │
     │     (UserId)   │
     │  3. HTTP POST  │
     │     bauen      │
     └───────┬────────┘
       HTTP Request
       POST /rpc/getUser                    ┌────────────────┐
       {"id": "u-123"}  ──────────────────► │ HttpTransport  │
                                            │                │
                                            │ 1. Route →     │
                                            │    "getUser"   │
                                            │ 2. Wire[Json]  │
                                            │    .deserialize│
                                            │    → UserId    │
                                            └───────┬────────┘
                                            ┌────────────────┐
                                            │    Handler     │
                                            │                │
                                            │  fn(UserId)    │
                                            │  → Result[     │
                                            │    User,       │
                                            │    ApiError]   │
                                            └───────┬────────┘
                                            ┌────────────────┐
                                            │ HttpTransport  │
                                            │                │
                                            │ 1. Wire[Json]  │
                                            │    .serialize  │
                                            │    (Result)    │
     ┌────────────────┐                     │ 2. HTTP 200    │
     │  TypedClient   │    HTTP Response    │    + body      │
     │                │ ◄────────────────── └────────────────┘
     │  1. Wire[Json] │    200 OK
     │     .deserialize│   {"name":...}
     │     → Result   │
     │       [User,   │
     │        ApiError│
     │       ]        │
     └───────┬────────┘
     ┌────────────────┐
     │  Client Code   │
     │                │
     │  Result[User,  │
     │   ClientError  │
     │   [ApiError]]  │
     └────────────────┘

Das Wesentliche: Wire[Json] taucht an vier Stellen auf — zweimal auf dem Client (serialize/deserialize), zweimal auf dem Server (deserialize/serialize). Aber weder der Contract noch der Handler referenzieren es. Beide Seiten arbeiten mit getypten JDL-Werten. Die Byte-Konvertierung ist vollständig in der Transport-Engine gekapselt.


13. Das Drei-Ebenen-Pattern — Deklarativ, Operativ, Imperativ

Über die gesamte Jade-Architektur hinweg zeigt sich ein wiederkehrendes Muster. Jede Domäne, die mit Blueprints modelliert wird, folgt derselben dreistufigen Struktur:

Ebene 1 — Deklarativ (Blueprint)
    Beschreibt WAS passieren soll.
    Datenstruktur, serialisierbar, inspizierbar.
    Keine Ausführungslogik, keine Seiteneffekte.

Ebene 2 — Operativ (Engine)
    Interpretiert WIE der Blueprint umgesetzt wird.
    Validiert, verdrahtet, orchestriert.
    Entscheidet über Transport, Scheduling, Fehlerstrategien.

Ebene 3 — Imperativ (Handler / Funktion)
    TUT es. Reine Geschäftslogik.
    Nimmt getypte Werte, gibt getypte Ergebnisse zurück.
    Weiß nichts über die Ebenen darüber.

Dieses Muster ist kein Zufall und kein einzelnes Feature — es ist ein emergentes Architekturmuster der Sprache. Es taucht überall auf, wo Blueprints verwendet werden:

Domäne             Blueprint           Engine              Handler/Funktion
──────────────     ──────────────      ──────────────      ──────────────────
API-Kommunikation  Contract            HttpTransport       Handler-Funktionen
Parsing            ParserSpec          ParserEngine        Transform-Funktion
Actor-Systeme      ActorSpec           Supervisor          receive-Funktion
Effektsystem       CallGraph           EffectRuntime       outputs-Funktion
Code-Emission      CodeEmissionSpec    JbcEmitter          EmissionRules
Scheduling         SchedulerSpec       SchedulerEngine     action-Funktionen
Testing            TestSuiteSpec       TestRunner          test-Funktionen
Deployment         DeploySpec          DeployEngine        Service-Definitionen
Projektmanifest    ProjectSpec         BuildEngine         Target-Definitionen

Die Konsequenz: jede neue Domäne braucht nur einen neuen Blueprint-Typ und eine passende Engine. Die Mechanik darunter — Phantom-Parameter, Refinements, Wire, die generische run()-Funktion, die Middleware-Chain — ist immer dieselbe. Die Komplexität wird einmal im Fundament bezahlt und jede weitere Domäne baut darauf auf.

Dieses Muster entspricht dem Drei-Phasen-Modell aus der Blueprint-Spec (Section 2): "Beschreiben, Beweisen, Ausführen." Aber es ist mehr als ein Implementierungsdetail — es ist ein Design-Pattern, das die gesamte Jade-Architektur durchzieht. Es könnte als Blueprint-Engine-Handler-Pattern oder als Deklarativ-Operativ-Imperativ-Pattern benannt werden.


14. Die Engine als Execution Context

Die Engine ist mehr als ein Interpreter — sie ist der Execution Context des jeweiligen Blueprints. Diese Bezeichnung trifft die Verantwortlichkeit präziser als "Interpreter" oder "Runtime", weil sie die Gesamtheit dessen ausdrückt, was die Engine leistet: Verdrahtung, semantische Validierung, Aktivierung, Fehlerbehandlung, Lifecycle-Management und Orchestrierung.

Ohne eine Engine ist ein Blueprint eine tote Beschreibung — ein Dictionary mit Metadaten. Die Engine gibt dem Blueprint Bedeutung, indem sie ihn in einen laufenden Prozess überführt. Das ist die zentrale Funktion jeder Engine, unabhängig von der Domäne.

Verantwortungstrennung: Compiler vs. Engine

Die Korrektheit eines Blueprints wird auf zwei Ebenen geprüft, und die Trennung ist bewusst:

Der Compiler und die Type Engine prüfen die statische Korrektheit. Stimmen die Typen? Sind alle Felder vorhanden? Sind die Phantom-Tags gültige Varianten? Implementieren alle In/Out-Typen Serializable? Das ist alles, was ohne Laufzeit-Kontext entscheidbar ist.

Die Engine prüft die semantische Korrektheit via validate(). Kann mein Transport alle Interaktionsmuster des Contracts abbilden? Ist der konfigurierte Port frei? Existiert ein gültiges TLS-Zertifikat? Sind alle Wire-Implementierungen für das gewählte Format vorhanden? Das sind Fragen, die erst im konkreten Deployment-Kontext beantwortbar sind.

Erst nach erfolgreicher Descriptor-Erzeugung aktiviert die Engine den Blueprint über instantiate(descriptor). Ab diesem Punkt ist die Engine der Execution Context — sie orchestriert den gesamten Ablauf, managed Fehler auf ihrer Ebene und gibt nach oben nur Result[T, E] zurück.

Oberflächenminimalität

Die Engine exponiert bewusst eine minimale öffentliche Schnittstelle:

┌──────────────────────────────────────────────────────────┐
│                       Engine                              │
│                                                          │
│   Öffentlich:                                            │
│   ├── Factory[F].create(...)  — Initialisierung          │
│   ├── describe(spec: B)       — semantische Prüfung → Descriptor      │
│   └── instantiate(desc)        — Aktivierung → Handle     │
│                                                          │
│   Intern (gekapselt):                                    │
│   ├── Dispatch-Tabelle                                   │
│   ├── Wire-Serialisierung                                │
│   ├── Connection-Management                              │
│   ├── Thread-Pool / Scheduling                           │
│   ├── Middleware-Chain                                    │
│   └── Error-Recovery                                     │
│                                                          │
│   Erweiterbar via:                                       │
│   └── EngineMiddleware                                   │
└──────────────────────────────────────────────────────────┘

Die Factory[F]-Funktion erlaubt unterschiedliche Initialisierungsformen derselben Engine — etwa HttpTransport.withTls(...) vs. HttpTransport.insecure(...). Die Komplexität bleibt intern; von außen sieht man nur die drei Slots.

Engine-Steuerung über Handles

Der von instantiate() zurückgegebene Handle ist nicht nur ein opaker Token — er ist eine typisierte Schnittstelle zum laufenden Execution Context. Über den Handle kann der Besitzer den Lifecycle des aktivierten Blueprints steuern:

┌────────────────────────────────────────────────────────┐
│                     ServerHandle                        │
│                                                        │
│   Lifecycle-Operationen (typisiert):                   │
│   ├── pause()        — Ausführung pausieren             │
│   ├── resume()       — pausierte Ausführung fortsetzen  │
│   ├── reinitialize() — Blueprint neu initialisieren     │
│   ├── shutdown()     — geordnetes Herunterfahren        │
│   ├── dumpState()    — aktuellen Zustand extrahieren    │
│   └── status()       — aktuellen Status abfragen        │
│                                                        │
│   Steuerbar via Message Passing / IPC                  │
└────────────────────────────────────────────────────────┘

Wenn der Handle selbst über eine Function Union typisiert ist, hat man denselben exhaustiven Match-Schutz wie bei Actor-Messages. Die Engine steuert sich nicht selbst — sie wird über ihren Handle gesteuert. Das ist konsistent mit der Blueprint-Spec: "Lifecycle-Verantwortung liegt auf den zurückgegebenen Handles, nicht auf der Engine selbst."

Die Steuerung über Message Passing oder IPC macht Engines aus der Ferne verwaltbar — ein Supervisor kann eine Engine pausieren, einen Stack Trace dumpen oder die Ausführung neu initialisieren, ohne direkten Zugriff auf den Engine-Prozess zu haben.

Refinements auf Engine vs. Blueprint

Refinements können sowohl an der Engine als auch am Blueprint angehängt werden. Die Semantik ist unterschiedlich, weil die Messpunkte unterschiedlich sind:

┌────────────────────────────────────────────────────────────────┐
│  :> Metrics(FullReport) am BLUEPRINT                           │
│                                                                │
│  Misst die fachliche Ebene:                                    │
│  — Wie oft wird getUser aufgerufen?                            │
│  — Wie lange dauert der Handler?                               │
│  — Welche Application-Error-Rates kommen aus der Logik?        │
│  — Welche Operationen werden am häufigsten genutzt?            │
├────────────────────────────────────────────────────────────────┤
│  :> Metrics(FullReport) an der ENGINE                          │
│                                                                │
│  Misst die operative Ebene:                                    │
│  — Wie lange dauert Wire-Serialisierung?                       │
│  — Wie viele TCP-Connections sind offen?                       │
│  — Wie groß ist der Thread-Pool, wie ausgelastet?              │
│  — Wie oft greift der Circuit-Breaker?                         │
│  — Wie hoch ist die Engine-interne Latenz?                     │
└────────────────────────────────────────────────────────────────┘

Dasselbe Refinement, unterschiedliche provide EngineMiddleware-Implementierungen, unterschiedliche Messpunkte. Das ist kein Sonderfall — es folgt direkt aus der Composable-Refinements-Architektur der Blueprint-Spec.

Ressourcenbindung und Performance-Modi

Engines können ressourcengebunden werden — limitiert in CPU-Kernen, Memory-Budgets, Connection-Pools. Das ermöglicht unterschiedliche Performance-Modi für dieselbe Engine-Konfiguration. In JadeOS würde das die Performance live in der VM beobachtbar und steuerbar machen.

Ein limitierter Engine-Execution-Context verhält sich wie ein Containment: derselbe Blueprint, dieselbe Logik, aber mit expliziten Obergrenzen für Ressourcenverbrauch. Das ist eine Engine-Konfiguration, kein Blueprint-Concern — der Blueprint weiß nichts über seine Ressourcengrenzen, die Engine erzwingt sie.


15. Blueprint-Komposition und Progressive Skalierung

Blueprints können andere Blueprints referenzieren (Blueprint-Spec, Section 8.1). In Kombination mit der Engine-als-Execution-Context-Architektur ergibt sich eine mächtige Kompositions- und Optimierungsstrategie.

Der Application-Blueprint-Tree

Eine Jade-Applikation besteht typischerweise aus einem Baum verschachtelter Blueprints. An der Spitze steht ein Application-Blueprint, der die Gesamtstruktur beschreibt. Darunter hängen spezialisierte Blueprints für einzelne Domänen — HTTP-Routing, Actor-Systeme, Datenbank-Pools, Scheduling.

ApplicationSpec
├── ServiceBinding[UserService]
│   ├── Contract: UserService
│   └── Handlers → ActorSpec[UserState, UserCommand]
├── ServiceBinding[ProfileService]
│   ├── Contract: ProfileService
│   └── Handlers → ActorSpec[ProfileState, ProfileCommand]
├── ServiceBinding[SettingsService]
│   ├── Contract: SettingsService
│   └── Handlers → direct DB calls
└── HttpRouterSpec
    └── Routes → dispatches to ServiceBindings

Jeder Knoten im Baum ist ein eigener Blueprint mit eigener Engine. Die Kommunikation zwischen den Knoten ist typisiert — nach unten über Handler-Aufrufe und Actor-Messages, nach oben ausschließlich über Result[T, E]. Ein Blueprint kann nie nach oben callen, nur nach unten. Nach oben steht nur der Weg über typisierte Rückgabewerte zur Verfügung. Das verhindert zirkuläre Abhängigkeiten und macht den Datenfluss nachvollziehbar.

Progressive Skalierung

Die Schichtentrennung ermöglicht eine progressive Skalierungsstrategie, bei der die Leistungscharakteristik geändert wird, ohne den Contract oder die Handler-Logik zu berühren:

Phase 1 — Einfach
    Handler rufen direkt die Datenbank auf.
    Kein Actor-Pool, keine Nebenläufigkeit.
    Gut für Entwicklung und kleine Deployments.

Phase 2 — Actor-Pool
    Handler routen an leichtgewichtige Actors.
    Nebenläufigkeit über Actor-Mailboxen.
    Nur ein Feld im Blueprint ändert sich.

Phase 3 — Supervised Threading
    Actors laufen auf supervised Threads.
    Hardware-Parallelismus wird ausgenutzt.
    Wieder nur ein Feld — die Supervision-Strategie.

Phase 4 — Verteilung
    Actors werden auf mehrere Nodes verteilt.
    Der Contract bleibt identisch.
    Die Transport-Engine handled die Verteilung.

In jeder Phase bleiben der Contract, die Handler-Funktionen und die Typgarantien identisch. Nur die Engine-Konfiguration und die Blueprint-Verschachtelung ändern sich. Das ist keine Theorie — es folgt direkt aus der Tatsache, dass die Engine die operative Ebene ist und der Handler die imperative. Die Handler-Logik ((UserId) -> Result[User, ApiError]) ändert sich nicht, egal ob sie direkt aufgerufen, über einen Actor geroutet oder über ein Netzwerk verteilt wird.

Blueprint-Merging als Optimierung

Ein Baum aus vielen kleinen, hochspezialisierten Blueprints hat architektonische Vorteile (klare Grenzen, unabhängige Testbarkeit, modulare Zusammensetzung), aber auch Overhead (Handle-Tabellen-Einträge, Indirektionen, Dispatch-Kosten).

Wenn zwei benachbarte Blueprints im Baum kompatible Contracts haben, können sie über einen Merge-Try-Cast zu einem einzigen Blueprint verschmolzen werden. Das ist dasselbe Prinzip wie Inlining im Compiler — nur auf der Blueprint-Ebene. Der Merge-Try-Cast stellt sicher, dass die Verschmelzung typsicher ist: nur wenn die Contracts kompatibel sind, wird gemergt. Handle-Tabellen-Einträge, die durch die Verschmelzung überflüssig werden, können aufgelöst werden.

Das ergibt eine zweistufige Strategie: man definiert die Architektur aus vielen kleinen Blueprints (für Klarheit und Modularität), und der Optimizer verschmilzt kompatible Nachbarn (für Performance). Die semantische Korrektheit bleibt durch den Merge-Try-Cast garantiert.

Queryability

Dass Engines und Blueprints über :> Queryable() adressierbar sind, macht Komposition, Orchestrierung, Testing, Mocking, Monitoring und Benchmarking nahezu trivial. Ein Test-Framework kann alle Engines eines bestimmten Typs finden, sie mit Mock-Handlern verdrahten und die Ergebnisse sammeln. Ein Monitoring-System kann alle aktiven Engines abfragen und ihre Metriken aggregieren. Ein Benchmark-Tool kann Engines isoliert ausführen und ihre Performance vergleichen. All das ist regulärer JDL-Code, der über das Query-System auf die Blueprint-Landschaft zugreift.


16. Performance und Laufzeit-Overhead

Eine berechtigte Frage: was kostet diese Architektur zur Laufzeit?

Die kurze Antwort: der Großteil der Komplexität ist Compile-Zeit, nicht Runtime. Die Abstraktion existiert im Typsystem, nicht im generierten Code.

Was nichts kostet

Phantom-Parameter haben null Runtime-Kosten. Ontologie-Spec, Regel T-8: "Phantom-Parameter erzeugen keine Runtime-Repräsentation." Der InteractionMode-Tag beeinflusst die Typprüfung, erzeugt aber keinen Code.

Tags haben kein Runtime-Layout. Ontologie-Spec, Regel T-3: "Ein tag besitzt kein Runtime-Layout."

TypeFns sind reine Compile-Zeit-Transformationen. HandlerMap, Queryable, Versioned — sie erzeugen Meta-/Type-/Descriptor-Patches, die der Materializer committed. Zur Laufzeit existieren sie nicht.

Der Contract-Blueprint selbst ist ein Dictionary mit Funktionsreferenzen — kein Overhead gegenüber einem handgeschriebenen Dispatch. Die Felder sind zur Compile-Zeit bekannt, sodass der Feldzugriff optimiert werden kann.

Was minimal kostet

Protocol-Dispatch für Engine.execute — eine Indirektion. Bei bekanntem Engine-Typ kann der Compiler monomorphisieren und inlinen.

Die Middleware-Chain für Refinements — eine Closure pro Refinement-Layer. Die Chain-Struktur ist zur Compile-Zeit bekannt und kann geflacht werden.

Wire-Serialisierung — dieselben Kosten wie in jedem Framework. JSON-Parsing, Protobuf-Encoding, etc. sind I/O-gebunden, nicht CPU-gebunden. Die Abstraktion fügt keinen Overhead hinzu.

Der Vergleich

Der tatsächliche Runtime-Overhead entspricht dem eines gut strukturierten HTTP-Frameworks in jeder anderen Sprache. Die Abstraktion existiert im Typsystem (wo sie zur Compile-Zeit aufgelöst wird), nicht im generierten Code (wo sie Laufzeit kosten würde). Im besten Fall generiert der Compiler denselben Code, den man von Hand geschrieben hätte — mit Typgarantien obendrauf.


17. Die Stdlib als Runtime-Framework

Die Jade-Stdlib ist keine klassische Standardbibliothek im Sinne einer Sammlung von Werkzeugen. jdl:: bildet die user-facing Ring-2-Systemschicht; jade:: bildet die dünne Ring-1-nahe Implementierungsschicht über Intrinsics und Descriptoren.

Ohne die Stdlib sind Blueprints inerte Dictionaries. Die Engines in der Stdlib geben den Blueprints Bedeutung. EffectRuntime interpretiert CallGraph-Blueprints, ActorSupervisor interpretiert ActorSpec-Blueprints, HttpTransport interpretiert ServiceBinding-Blueprints. Diese Engines sind keine optionalen Werkzeuge — sie sind die Runtime.

Die korrekte Analogie ist Erlangs OTP, nicht Pythons import os. OTP ist das Framework, das dem Actor-Modell seine operationale Semantik gibt. Jade macht dasselbe, aber breiter: nicht nur Actors, sondern jede Domäne die als Blueprint ausdrückbar ist.

Implikationen für die Architektur

Die jdl::-Stdlib-Version ist ein Teil der Runtime-/Semantik-Version. Ein Stdlib-Update kann das Verhalten jeder Engine ändern — Retry-Logik, Serialisierungsformat, Dispatch-Strategie. Das ist näher an einer JVM-Version als an einem pip install.

jdl:: kann nicht beliebig vom Benutzer ersetzt werden. Sie ist ein System-Layer, der auf jade:: und den Ring-1-Descriptoren aufbaut. Der Benutzer kann neue Blueprint-Typen definieren, neue Engines implementieren, neue Wire-Formate bereitstellen — aber er kann nicht das Engine-Protocol umdefinieren, nicht das Refinement-Middleware-System ersetzen, nicht Serializable austauschen.

Einordnung in die Ring-Architektur

Die Design Constitution definiert die Ring-Architektur: Ring 0 (JME) ist die Hardware-Abstraktion, Ring 1 (VM, Type Engine, Verifier) ist der Kernel. Die Stdlib als dritte Schicht ist konzeptionell der Systemspace — nicht austauschbar, nicht optional, aber auch nicht privilegiert im Sinne von Ring-0/1-Zugriff.

Ring 0 — JME-zentrierter Maschinenkern
    Handles, Memory, Arena-Management, Runtime-Facts
    Trust Anchor und operative Integrität

Ring 1 — jade:: / semantischer Systemraum
    TypeEngine, ParserEngine, ConstitutionEngine, Descriptor-Materialisierung,
    Intrinsic-Wrapping, CompilerDB-Provider

jdl:: — Ring 2 Stdlib-Systemschicht
    user-facing Engines, Protocols, Wire, Refinements, Services
    Nicht beliebig austauschbar durch den Benutzer

Userland — Ring 2 User-Layer
    Anwendungscode, Plugins, eigene Engines, Package-Constitutions
    Baut auf jdl:: auf, ersetzt sie nicht

Der jdl::-Namespace markiert System-Pakete. jdl::api, jdl::http, jdl::node sind keine austauschbaren Bibliotheken, sondern Systemkomponenten. Ein User kann mycompany::http schreiben, das jdl::api-Contracts konsumiert und eine eigene Transport-Engine liefert — aber jdl::api selbst ist gesetzt.

Das Bild: JME haucht der Hardware Leben ein, die VM haucht dem Bytecode Leben ein, und die Stdlib haucht der Sprache Leben ein. Drei Schichten, jede unverzichtbar, jede nicht austauschbar. Was der User austauscht, ist alles was obendrauf kommt.


18. Die Semantik-Schichtung von Jade

Jade besitzt nicht eine einzelne "Semantik", sondern ein geschichtetes Semantik-Modell. Jede Schicht macht etwas anderes und constrainted die Schichten darunter auf unterschiedliche Weise.

┌─────────────────────────────────────────────────────────┐
│  Typsemantik                                             │
│  Was Dinge SIND. (Struktur)                              │
│                                                         │
│  User hat Felder name: str, age: i32.                   │
│  Rpc[In, Out, E] ist ein Request-Response-Pattern.      │
│  Tags, Phantoms, Structs, Enums, Unions.                │
├─────────────────────────────────────────────────────────┤
│  Operationale Semantik                                   │
│  Was Dinge TUN. (Verhalten)                              │
│                                                         │
│  Engine.execute aktiviert einen Blueprint.               │
│  match ist exhaustiv. =? propagiert Fehler.             │
│  Generatoren yielden Werte, Pipelines transformieren.   │
├─────────────────────────────────────────────────────────┤
│  Effektsemantik                                          │
│  Was Dinge BEWIRKEN. (Wirkung)                           │
│                                                         │
│  Effect[R, E, D] deklariert Dependencies.               │
│  Wire markiert Serialisierung als Effekt.               │
│  Qualifier markieren Sichtbarkeit und Sharability.      │
├─────────────────────────────────────────────────────────┤
│  Constitutional Semantik                                 │
│  Was ERLAUBT und was PFLICHT ist. (Leitplanken)          │
│                                                         │
│  Keine öffentlichen mutablen Felder in Engines.         │
│  Blueprints callen nur nach unten, nie nach oben.       │
│  Alle öffentlichen Funktionen geben Result zurück.      │
└─────────────────────────────────────────────────────────┘

Die ersten drei Ebenen sind positive Semantik — sie definieren was existiert und was passiert. Die Constitutional Semantik ist primär negative Semantik — sie definiert den Raum des Erlaubten, indem sie den Raum des Verbotenen markiert. Aber sie hat auch eine positive Seite: manche Invarianten sind Pflichten ("jede Engine muss validate vor execute aufrufen"), nicht nur Verbote ("Engines dürfen keine öffentlichen mutablen Felder haben").

Die Constitutional Semantik prüft nicht einzelne Werte oder Ausdrücke — sie prüft die Struktur des Programms selbst: welche Typen existieren, wie sie zueinander stehen, welche Regeln ihre Module einhalten. Sie ist Meta-Semantik: Semantik über Semantik.


19. Constitution as Contract — Descriptor-basierte semantische Analyse auf Namespace-Ebene

Die Grundidee

Eine Constitution ist eine deklarative Beschreibung von Regeln, die für einen Ring, Namespace oder Package-Scope gelten. Sie ist kein freier JDL-Code, der während des Compiles beliebige Queries ausführt. Das wäre kein Contract, sondern ein Compiler-Plugin mit Robe und Richterhammer.

Aktuelles Modell:

ConstitutionSpec        Blueprint mit deklarativen Regeln
ConstitutionEngine      validiert Spec gegen Parent-Constitution
ConstitutionDescriptor  materialisierte, indexierte Regelbasis
Compiler-Gates          konsumieren Descriptoren und melden Diagnostics

Die Constitution beschreibt damit keine Werte und keine Runtime-Operationen, sondern den erlaubten semantischen Raum eines Gültigkeitsbereichs.

Rule-Form

Jede Regel wird auf eine endliche, querybare Form normalisiert:

Rule {
    trigger:   Wann wird geprüft?
    selector:  Worauf passt die Regel?
    predicate: Welche Bedingung muss gelten?
    action:    Was passiert bei Verletzung?
}

Typische Trigger:

OnNamespaceLoad
OnImport
OnTypeSubject
OnApplyRefinement
OnProvide
OnEffectDescriptor
OnDescriptorCommit
OnIrVerify
OnArtifactEmit

Eine Regel darf nur definierte CompilerDB-Queries und materialisierte Descriptoren lesen. Sie darf keine Runtime-Funktion aufrufen, keinen RuntimeBus berühren, keine I/O durchführen und keine freie Rekursion erzeugen.

Beispiel

type HttpConstitution : constitution {
    scope: Namespace("jdl::http")

    rule noPublicMutableState : Rule {
        trigger:  OnDescriptorCommit
        selector: PublicField(in: Namespace("jdl::http"))
        require:  not Mutable
        severity: Error
        message:  "jdl::http erlaubt keine öffentlichen mutablen Felder"
    }

    rule allPublicFunctionsReturnResult : Rule {
        trigger:  OnDescriptorCommit
        selector: PublicFunction(in: Namespace("jdl::http"))
        require:  ReturnType(IsResultLike)
        severity: Error
        message:  "Alle öffentlichen Funktionen in jdl::http müssen Result zurückgeben"
    }

    rule noDependencyOnInternals : Rule {
        trigger:  OnImport
        selector: Import(from: Namespace("jdl::http"), to: Namespace("jade::internal"))
        action:   Deny(Error)
        message:  "jdl::http darf nicht auf jade::internal zugreifen"
    }
}

Die Syntax ist informativ. Normativ ist das Datenmodell: Regeln sind Descriptor-fähige Daten, nicht freie CompilerDB-Lambdas.

Namespace-basierte Geltungsbereiche

Der Geltungsbereich ergibt sich aus Namespace-Hierarchie und Package-Autorität:

jade::                          Ring 1 Constitution
  jade::bootstrap
  jade::intrinsics

jdl::                           Ring 2 Stdlib Constitution
  jdl::api
  jdl::http
  jdl::engines

app::                           Ring 2 Project Constitution
  app::models
  app::services
  app::handlers

Für jedes Modul bildet der Compiler die effektive Constitution aus allen übergeordneten ConstitutionDescriptoren.

Monotonie-Regel

Constitutions sind monoton. Jede Ebene kann Regeln hinzufügen oder verschärfen, aber keine bestehende Regel entfernen oder lockern.

Ring0Constitution
  sealed, D/JME-enforced, nicht durch JDL lockbar

Ring1Constitution
  jade::, validiert gegen Ring 0

Ring2StdlibConstitution
  jdl::, validiert gegen Ring 0 + Ring 1

Ring2UserConstitution
  User-Package, validiert gegen Ring 0 + Ring 1 + jdl::

Deny gewinnt. Require kann hinzugefügt, aber nicht entfernt werden. Warn kann zu Error verschärft werden. Ring-0-Facts sind nie überschreibbar.

ConstitutionEngine

Die ConstitutionEngine ist eine Ring-1-nahe DescriptorEngine. Sie konsumiert ConstitutionSpecs und erzeugt ConstitutionDescriptoren:

ConstitutionSpec
  -> ConstitutionEngine.describe
  -> ConstitutionDescriptor
  -> CompilerDB Materializer

Die Compilerphasen fragen später nicht live eine allmächtige Engine, sondern konsumieren den Descriptor an klaren Gates:

ImportResolver          OnImport
TypeEngine              OnTypeSubject / OnApplyRefinement
ProvideResolver         OnProvide
EffectChecker           OnEffectDescriptor
Materializer            OnDescriptorCommit
Verifier                OnIrVerify
GeneratorEngine         OnArtifactEmit

Das ist Design by Contract auf Namespace-Ebene, aber passend zu Jade: nicht freie Query-Ausführung, sondern materialisierte Rule-Descriptoren mit Proof Trace.

Benutzer-Constitutions

Benutzer-Projekte dürfen eigene Constitutions definieren, aber nur restriktiv. Ein Projekt kann verlangen, dass alle Services Result zurückgeben oder direkte DB-Imports verbieten. Es kann nicht jade::intrinsics:: freischalten, Ring-0-Facts überschreiben oder System-Constitutions lockern.

Bootstrap-Seeding

Ring 0 validiert die Ring0Constitution selbst. Ring 1 lädt und validiert jade::constitution gegen Ring 0. Danach können jdl:: und User-Constitutions als normale, aber monotone ConstitutionSpecs materialisiert werden.

Damit ist Constitution as Contract kein separates Lint-Framework, sondern eine normale Jade-Descriptor-Domäne.

20. Die Zwei Zugangspunkte — CompilerDB und RuntimeBus

JDL hat exakt zwei kontrollierte Zugangspunkte zu den tieferen Ringen. Sie sind symmetrisch aufgebaut und decken die zwei Lebensphasen eines Programms ab.

Compile-Zeit:    typefn  →  db.*        →  CompilerDB
                 Typ-Transformationen, Meta-Record-Einträge,
                 Validierung, Diagnostics.

Runtime:         def     →  intrinsic:: →  RuntimeBus
                 Memory-Management, Atomic-Operationen,
                 I/O, Threading, Scheduling.

TypeFns sind die JDL-Schnittstelle zur CompilerDB während der Compile-Zeit. Sie laufen in einem TypeFnContext, lesen Typstrukturen, validieren Refinements, konstruieren neue Typen und erzeugen MaterializationPatches. Erst der Materializer committed diese Patches in CompilerDB und DescriptorStore.

Intrinsics sind die JDL-Schnittstelle zum RuntimeBus zur Laufzeit. Sie führen Operationen aus die JDL selbst nicht ausdrücken kann: atomare Speicheroperationen, Handle-Allokation, Event-Loop-Anbindung, Thread-Management.

Beide Zugangspunkte laufen ausschließlich über die Stdlib. Kein User-Code greift direkt auf CompilerDB oder RuntimeBus zu. Die Stdlib ist die einzige Brücke zwischen den Welten — CompilerDB nach oben (via TypeFns), RuntimeBus nach unten (via Intrinsics).


21. Die Intrinsics-Brücke — wie JDL mit D spricht

21.1 Kein JadeValue intern

Innerhalb der VM arbeiten Register mit rohen, getypten Werten — kein JadeValue-Wrapping, kein Tag-Checking. Die Typisierung ist zur Compile-Zeit bewiesen, die VM vertraut dem Beweis.

JadeValue ist ein Grenztyp der nur an den Rändern des Systems lebt: Plugin-Boundaries, FFI, REPL-Inspektion, Debugger. Dort ist die Typisierung nicht compile-time-bewiesen und JadeValue trägt die Typinformation zur Laufzeit mit. Intern ist alles unverpackt.

21.2 Was D sieht wenn es auf einen Handle blickt

Ein Handle ist ein Index in die Handle-Tabelle. D sieht keine JDL-Semantik — keine Blueprints, keine Protocols, keine Refinements. D sieht rohe Verwaltungsdaten:

HandleEntry (D-Struct):
    data:        void*          Zeiger auf die Daten in der Arena
    typeId:      TypeId         Verweis auf den Typ-Deskriptor (Größe, Alignment, Offsets)
    arenaId:     ArenaId        Welche Arena besitzt den Speicher
    refCount:    u32            Referenzzähler (nur bei Shared)
    generation:  u32            Generationszähler (Use-after-free-Schutz)
    flags:       HandleFlags    Ownership, Sharing, Status (Bit-Flags)

Die flags sind das einzige was vom Meta-Record nach unten durchsickert. Own(Shared) wird zu einem SHARED-Bit. Share(Sync) wird zu einem SYNC-Bit. D braucht keine Tags, D braucht Bits um Entscheidungen zu treffen.

Die generation ist der Use-after-free-Schutz: wenn ein Handle freigegeben und der Slot wiederverwendet wird, wird die Generation inkrementiert. Ein alter Handle mit veralteter Generation auf einem wiederverwendeten Slot ist ungültig — die JME erkennt das und meldet einen Fehler.

21.3 Handle-Selektion über TypeKind und Meta-Record

Die JME wählt den konkreten Handle-Typ basierend auf zwei Informationen: dem TypeKind (was der Typ ist) und den Meta-Record-Policies (wie er gemanagt wird). Beide zusammen bestimmen den Handle.

TypeKind          Meta-Record              JME-Handle
──────────        ──────────────────       ─────────────────────────
struct            Own(Unique), Value       Stack-Wert, kein Handle
struct            Own(Unique), Heap        Unique-Heap-Handle
struct            Own(Shared)              Refcounted-Handle
enum              (nach Größe)             Inline oder Handle
blueprint         (immer)                  Dictionary-Handle
tag               —                        kein Handle (T-3)
protocol          —                        kein Handle (Vertrag)
constitution      —                        kein Handle (Compile-Zeit)

21.4 Der Intrinsic-Aufruf — kein Bridge, kein Marshalling

Ein Intrinsic-Aufruf ist ein direkter Funktionsaufruf innerhalb des VM-Prozesses. D ist die VM — der Intrinsic-Handler ist eine D-Funktion die im selben Adressraum auf denselben Datenstrukturen arbeitet wie der Bytecode-Dispatch.

Bytecode:   INTRINSIC ATOMIC_LOAD, R3, R_target
VM-Dispatch:    Handler-Funktion aus Dispatch-Tabelle aufrufen
Handler (D):    Handle-Index aus Register lesen (Array-Zugriff)
                Handle-Tabelle nachschlagen (Array-Zugriff)
                Generation prüfen (Integer-Vergleich)
                Flags prüfen (Bit-Test)
                Atomaren Load ausführen (CPU-Instruktion)
                Ergebnis ins Ziel-Register (Array-Zugriff)
VM:             nächste Instruktion

Kein JadeValue, kein Marshalling, kein Kontextwechsel. Sechs Schritte, jeder ein primitiver Speicherzugriff oder eine CPU-Instruktion.


22. Asynchrones I/O — VM-Infrastruktur, nicht Actor

22.1 Der Event-Loop lebt in D

Der I/O-Event-Loop ist VM-Infrastruktur in D (Ring 0/1), kein JDL-Actor in Ring 2. Der Event-Loop pollt das Betriebssystem (epoll, kqueue, io_uring), sammelt Completions und meldet sie an den VM-Scheduler.

Actors sind ein Ring-2-Konzept. Sie brauchen I/O um zu kommunizieren. Wenn der I/O-Service selbst ein Actor wäre, bräuchte er I/O um I/O zu machen — das ist zirkulär. Der Event-Loop lebt unter den Actors, nicht als Actor.

Actor (Ring 2, JDL)
    ↓ Intrinsic-Call
VM-Scheduler (Ring 1, D)
    ↓ registriert I/O-Request
Event-Loop (Ring 0, D)
    ↓ epoll/kqueue/io_uring
OS-Kernel
Event-Loop
    ↓ Completion-Event
VM-Scheduler
    ↓ resumed Actor mit Ergebnis
Actor
    → hat seine Daten

22.2 Async per Default, synchrone Oberfläche

I/O in der Stdlib ist async unter der Haube, synchron an der Oberfläche. Der User schreibt sequentiellen Code, die VM handled die Asynchronität transparent:

// Sieht synchron aus — ist es aber nicht
val data = file.read("config.yaml")
val parsed = parser.parse(data)

Unter der Haube: file.read ist ein Intrinsic der async I/O startet, einen Yield-Point erzeugt, den Actor suspendiert und bei Completion resumed. Der User merkt nichts. Sein Code liest sich wie sequentieller Code, aber die VM blockiert nie.

Für explizite Parallelisierung gibt es Async-Handles:

val h1 = file.readAsync("a.yaml")
val h2 = file.readAsync("b.yaml")
val h3 = file.readAsync("c.yaml")

val a = h1.join()   // alle drei liefen parallel
val b = h2.join()
val c = h3.join()

Kein async-Keyword, kein await-Operator, keine Colored Functions. Die VM ist der Scheduler, das Actor-Modell ist die Abstraktion, und I/O ist immer non-blocking.

22.3 Die VM pausiert nicht bei Intrinsics

Schnelle Intrinsics (atomic_load, handle_alloc, refcount_inc) sind synchrone Funktionsaufrufe im Dispatch-Loop — Nanosekunden, kein Yield. Blockierende Intrinsics (file_read, network_recv) sind Yield-Points: der Handler initiiert die Operation und gibt "pending" zurück, die VM suspendiert den Actor und scheduled den nächsten. Bei I/O-Completion wird der Actor geweckt.


23. Namespace-Zugriffshierarchie

23.1 Vier Ebenen

Die Stdlib ist in zwei Namespaces geschichtet: jade:: (Maschinenraum) und jdl:: (Abstraktionsschicht). Zusammen mit dem User-Namespace app:: und den rohen Intrinsics ergibt sich eine vierstufige Zugriffshierarchie:

app::               → darf jdl:: importieren
                    → darf jade:: NICHT importieren
                    → darf jade::intrinsics:: NICHT importieren

jdl::               → darf jade:: importieren
                    → darf jade::intrinsics:: NICHT importieren

jade::              → darf jade::intrinsics:: importieren

jade::intrinsics::  → spricht direkt mit dem RuntimeBus (D)

Jede Grenze ist durch die Constitution enforced — Compile-Fehler, kein Workaround.

23.2 Was wo lebt

Das durchgängige Prinzip: Beschreibungen (Blueprints, Protocols, Typen) leben in jdl::. Implementierungen die Intrinsics brauchen, leben in jade::. Die Faustregel: sobald ein provide das Wort intrinsic:: enthält, gehört es nach jade::.

jdl::fs             FsSpec : blueprint { ... }         WAS kann das Dateisystem
jade::fs            provide Engine[FsSpec, ...] { ... } WIE es funktioniert (via Intrinsics)

jdl::concurrent     AtomicCounter, Atomic[T]            WAS existiert
jade::atomic        provide Atomic for ... { ... }      WIE es funktioniert (via Intrinsics)

jdl::net            NetSpec, TcpListener, ...            WAS existiert
jade::net           provide Engine[NetSpec, ...] { ... } WIE es funktioniert (via Intrinsics)

23.3 Umfang des jade::-Namespace

Der jade::-Namespace ist die dünnste Schicht im System — wenig Code, aber der härteste:

jade::memory        Handle-Alloc, Handle-Free, Arena-Management
jade::atomic        Atomic Load/Store/CAS/Add
jade::sync          Mutex, Semaphore, Condition Variable
jade::io            Event-Loop-Registration, Poll, Completion
jade::fs            File Open/Read/Write/Close
jade::net           Socket Bind/Listen/Accept/Send/Recv
jade::thread        Thread Spawn/Join/Yield
jade::time          Clock, Timer, Sleep

Ungefähr acht Module mit je 10–20 Intrinsic-Wrappern. Insgesamt 100–200 Provides, geschätzt 2.000–4.000 SLOC. Das ist der gesamte Maschinenraum — der Rest der Stdlib (100.000+ SLOC) ist reines JDL in jdl::.

Einmal stabil, wird jade:: selten angefasst. Alles was sich ändert — neue Engines, neue Blueprints, neue Patterns — passiert in jdl::.


24. Implementierungsrealität

Die theoretische Eleganz dieser Architektur darf nicht darüber hinwegtäuschen, dass die Implementierung ein Sequenzierungsproblem ist. Die Stdlib-Konzepte sind bewusst verflochten — das ist das Zeichen, dass die Abstraktionen tatsächlich zusammenpassen. Aber diese Verflechtung erzeugt Abhängigkeitsketten, die in der richtigen Reihenfolge hochgezogen werden müssen.

Die wesentlichen Abhängigkeiten:

Blueprints brauchen die Blueprint-TypeKind im Compiler und Dictionary-Handles in der JME. Engines brauchen das Protocol-System (RuntimeEngine[Spec, Desc, Out]), das braucht provide, das braucht den Typ-Resolver. Wire braucht das Serializable-Protocol, das braucht das Protocol-System. Refinements brauchen Meta-Records und TypeFns. Die Middleware-Chain braucht Refinements, Engines und Protocols. HandlerMap braucht eine TypeFn-Erweiterung, die es heute noch nicht gibt. Constitution as Contract braucht die CompilerDB-Query-API, die braucht die CompilerDB selbst.

Nichts davon lässt sich isoliert hochziehen, ohne die anderen zumindest als Stubs zu haben. Das ist kein Wasserfall, sondern ein Geflecht — und das Geflecht muss in sorgfältig geschichteter Reihenfolge zum Leben erweckt werden. Der D-basierte Ring-1-Kern (Bootstrap-Spec) liefert die Primitive, auf denen Schicht für Schicht aufgebaut wird.

Offene Punkte

Die folgenden Designfragen sind in dieser Notiz aufgeworfen aber nicht abschließend beantwortet:

Die HandlerMap-TypeFn und die prozedurale TypeFn-Mechanik sind in einer separaten Spec ("TypeFn-Spezifikation — Prozedurale Typ-Funktionen") adressiert. Die CompilerDB-Query-API für TypeFns (Leseseite und Schreibseite) ist dort definiert.

Die Ableitung einer Function Union aus einem Contract-Blueprint ist konzeptionell klar, aber der Mechanismus (manuell, via TypeFn, via Compiler-Derivation) ist noch nicht festgelegt.

Die genaue API des TypedClient — wie die Contract-Operationen als aufrufbare Methoden auf dem Client-Proxy erscheinen — muss spezifiziert werden. Ein Beispiel einer TypeFn die einen TypedClient ableitet ist in der TypeFn-Spec enthalten.

Die Versionierung und Kompatibilitätsprüfung von Contracts (:> Versioned) braucht eine eigene Spec, die Semver-Regeln, Wire-Kompatibilität und Migration-Strategien adressiert.

Die Interaktion zwischen Modul-Constitutions (Komposition, Vererbung, Konflikterkennung) braucht eine formale Spezifikation der Monotonie-Regel und der Lattice-Struktur.

Die konkrete Event-Loop-Architektur (epoll vs. io_uring, Single-Thread vs. Multi-Thread, Scheduler-Integration) muss in einer eigenen VM-I/O-Spec adressiert werden.


25. Zusammenfassung

Diese Design-Notiz beschreibt drei zusammenhängende Architektur-Erkenntnisse für die Jade-Plattform.

Transport-agnostische API-Contracts — eine generalisierte Schnittstellenbeschreibung, die auf bestehenden Jade-Konzepten aufbaut: Blueprints als deklarative Beschreibungen, Tags als Compile-Zeit-Bedeutung, Phantom-Parameter als Zero-Cost-Typ-Annotation, Function Unions als typisierte Operationsbeschreibungen, und das Wire-System als formatflexible Serialisierung. Die Architektur führt keine neuen Sprachprimitive ein. Sie komponiert bestehende Konzepte zu einem neuen Anwendungsbereich.

Die Engine als Execution Context — die Erkenntnis, dass Engines nicht nur Interpreter sind, sondern der vollständige Ausführungskontext eines Blueprints: Verdrahtung, semantische Validierung, Aktivierung, Fehlerbehandlung, Lifecycle-Management und Orchestrierung. Von außen minimal (Factory, validate, execute), intern komplex, über Handles steuerbar, über Refinements erweiterbar, über Queries adressierbar. Engines sind der Dreh- und Angelpunkt jeder Jade-Applikation, und Blueprints sind ihre Beschreibung.

Constitution as Contract — semantische Analyse auf Modulebene, inspiriert von Bertrand Meyers Design by Contract in Eiffel. Jedes Modul definiert Preconditions, Postconditions und Invarianten als deklarative Rule-Descriptoren über CompilerDB-Queries. Der Geltungsbereich ergibt sich automatisch aus der Namespace-Hierarchie — Amendments propagieren nach unten und constrainen den erlaubten Raum weiter, können aber nie bestehende Constraints entfernen. Die ConstitutionEngine materialisiert ConstitutionDescriptoren; Compilerphasen konsumieren diese an Enforcement-Gates. Im Bootstrap-Prozess validiert sie die Ring- und Namespace-Constitutions, bevor JDL weiterführende Semantik übernimmt. Der Mechanismus steht Benutzern ebenso zur Verfügung wie System-Modulen — das System privilegiert seinen Autor nicht.

JDL als Self-Carrying Language — die Erkenntnis, dass JDL sich selbst trägt. Die einzige Verbindung zum Maschinenraum sind die Intrinsics. Alles darüber — Engines, Protocols, Wire, Blueprints, Constitutions — ist JDL-Code. D stellt die CompilerDB-Tabellen bereit, JDL gibt ihnen Bedeutung. Der Bootstrap ist der Moment, in dem die Tür zum Maschinenraum zugeht und JDL alleine weiterläuft.

Die Zwei Zugangspunkte — TypeFns sind die Compile-Zeit-Schnittstelle zur CompilerDB, Intrinsics sind die Runtime-Schnittstelle zum RuntimeBus. Symmetrisch aufgebaut, beide ausschließlich über die Stdlib zugänglich. Die Stdlib ist die einzige Brücke zwischen JDL und den tieferen Ringen.

Namespace-Zugriffshierarchiejade:: enthält die dünne Provide-Schicht über Intrinsics (~2.000–4.000 SLOC), jdl:: enthält die Abstraktionen darüber (100.000+ SLOC). Blueprints und Protocols leben in jdl::, Engine-Provides die Intrinsics nutzen leben in jade::. Die Zugriffsgrenzen zwischen app::, jdl::, jade:: und jade::intrinsics:: sind durch die Constitution compiler-enforced.

Asynchrones I/O — der Event-Loop ist VM-Infrastruktur in D, kein JDL-Actor. I/O ist async per Default unter der Haube, synchron an der Oberfläche. Der User schreibt sequentiellen Code, die VM handled Suspendierung und Resumption transparent über den Actor-Scheduler. Kein async/await-Keyword, keine Colored Functions.

Das sich durch alle Erkenntnisse ziehende Muster — Deklarativ (Blueprint), Operativ (Engine), Imperativ (Handler) — ist kein einzelnes Feature, sondern ein emergentes Architekturmuster, das die gesamte Jade-Plattform durchzieht. Es taucht in API-Contracts auf, in Parser-Generatoren, in Actor-Systemen, in der Constitution-Verifikation und in jeder weiteren Domäne die mit Blueprints modelliert wird. Es ist die konsequente Anwendung der Blueprint-Philosophie: "Beschreibe deine Absicht, die Engine gibt ihr Bedeutung."