JDL Blueprint-Typsystem — Die deklarative API¶
Status: Entwurf, architektonisch fundierend
Geltungsbereich: JDL-Sprache, Jade-Typsystem, Jade-VM, Compiler, Runtime-Engines
Ergänzt: Grammatik-Skizze, Bootstrap-Spec, Design Constitution, Typsystem, Effektsystem, Plugin-System
Autor: Elias
1. Zweck dieses Dokuments¶
Dieses Dokument definiert den blueprint-TypeKind als eigenständiges Sprachkonzept in JDL. Blueprints sind die deklarative API von Jade — die Schnittstelle, über die Benutzer Absichten beschreiben, die von spezialisierten Runtime-Engines interpretiert und ausgeführt werden.
Die Trennung zwischen deklarativer Beschreibung und imperativer/funktionaler Berechnung ist ein Architekturprinzip, kein syntaktischer Zufall. Dieses Dokument begründet diese Trennung, definiert die Semantik von Blueprints, zeigt ihre Anwendung in verschiedenen Domänen und benennt die Fallstricke.
2. Die Kern-Idee¶
JDL unterscheidet zwei fundamentale Ausdrucksebenen:
Imperativ/Funktional — beschreibt was berechnet wird. Funktionen, Pattern Matching, Pipelines, Kontrollfluss. Das ist der Code, der Werte transformiert, Algorithmen implementiert und Logik ausdrückt.
Deklarativ (Blueprints) — beschreibt wie orchestriert wird. Abhängigkeiten, Reihenfolgen, Fehlerstrategien, Supervision, Routing, Scheduling. Das ist der Plan, den eine Engine interpretiert.
Die Brücke zwischen beiden Ebenen sind Compiler und Engine: der Compiler validiert die Typen innerhalb des Blueprints zur Compile-Zeit. Die Engine validiert die Semantik zur Laufzeit — ob Dependencies auflösbar sind, ob Konfigurationen sinnvoll sind, ob referenzierte Services verfügbar sind. Beide Validierungsebenen sind komplementär: der Compiler beweist die Typkorrektheit, die Engine beweist die operationale Korrektheit.
Das Drei-Phasen-Modell:
Phase 1 — Beschreiben: Der Benutzer deklariert seine Absicht im Blueprint.
Phase 2 — Beweisen: Der Compiler verifiziert die Typkorrektheit.
Die Engine validiert die operationale Semantik.
Phase 3 — Ausführen: Die zuständige Engine interpretiert den Blueprint zur Laufzeit.
3. Warum eine eigene TypeKind¶
Ein Blueprint ist kein Struct. Structs beschreiben Datenlayout — Felder, Speicher, Werte. Blueprints beschreiben Verhalten — was passieren soll, in welcher Reihenfolge, mit welchen Abhängigkeiten, unter welchen Bedingungen.
Der Unterschied ist nicht kosmetisch, er ist semantisch:
Ein Struct-Feld enthält einen Wert. Ein Blueprint-Feld enthält eine Beschreibung. Wenn ein Struct-Feld den Typ UserDb -> Result[User, UserError] hat, dann enthält es eine aufrufbare Funktion. Wenn ein Blueprint-Feld denselben Typ hat, enthält es die Absicht, diese Operation auszuführen — wann und wie liegt bei der Engine.
Diese Unterscheidung in einen eigenen TypeKind zu kodieren hat konkrete Vorteile:
Der Compiler kann Blueprint-spezifische Prüfungen durchführen, die für Structs keinen Sinn ergeben — etwa ob alle deklarierten Abhängigkeiten im env-Feld gebunden sind.
Die VM weiß, dass ein Blueprint-Wert als Handle gehalten und an eine Engine übergeben wird, nicht als Datenstruktur im klassischen Sinne traversiert wird.
Tooling (LSP, Docs-Generator, Visualisierer) kann Blueprints als Orchestrierungsgraphen darstellen statt als flache Feldlisten.
Blueprints sind intrinsisch serialisierbar. Diese Eigenschaft ist nicht optional via Refinement, sondern folgt aus der Natur des TypeKinds: ein Blueprint ist eine Beschreibung, und Beschreibungen müssen kommunizierbar sein — an andere Engines, andere Nodes, andere Prozesse, Debugger, Visualisierer. Hinter den Kulissen ist ein Blueprint eine geordnete Key-Value-Struktur (Dictionary), deren Werte entweder Daten (serialisiert als Werte) oder Funktionsreferenzen (serialisiert als qualifizierte Namen) sind. Der Leitsatz: "Gehe nicht davon aus, dass dein Blueprint lokal verarbeitet wird."
4. Syntax und Grammatik¶
4.1 TypeBody-Erweiterung¶
Die Grammatik-Skizze wird um BlueprintBody erweitert:
TypeBody ::= StructBody
| EnumBody
| UnionBody
| AliasBody
| BlueprintBody ← NEU
BlueprintBody ::= "blueprint" "{" BlueprintFieldDecl* "}"
BlueprintFieldDecl ::= FieldName ":" TypeExpr
Die Syntax innerhalb eines Blueprint-Bodys ist identisch mit Struct-Feldern. Der Unterschied liegt nicht in der Felddeklaration, sondern in der Semantik: alle Felder sind Beschreibungen, keine Datenwerte.
4.2 TypeKind-Erweiterung¶
Die hardcoded TypeKind-Liste wird erweitert:
4.3 Beispielhafte Blueprint-Typ-Definitionen¶
type CallGraph[R, E, D] : blueprint {
inputs: D
outputs: D -> Result[R, E]
env: D
} :> Queryable()
type ActorSpec[S, M, R] : blueprint {
init: () -> S
receive: (S, M) -> (S, R)
supervision: SupervisionPolicy
} :> Queryable()
type PipelineSpec[In, Out, E] : blueprint {
stages: [Stage[In, Out, E]]
source: Source[In]
sink: Sink[Out]
backpressure: BackpressurePolicy
} :> Queryable()
type SchedulerSpec[T] : blueprint {
tasks: [TaskDef[T]]
strategy: SchedulingStrategy
timing: TimingPolicy
} :> Queryable()
4.4 Blueprint-Instanziierung¶
Ein Blueprint wird instanziiert wie ein Struct-Literal, aber die Semantik ist deskriptiv:
val graph = CallGraph {
id: "request-handler-v1"
name: "Request Handler"
class: [Api, Critical]
inputs: { db: UserDb, log: LogService }
outputs: { db, log } =>
val user =? db.getUserByEmail(email)
log.info(f"Found: {user.name}")
Ok(user)
env: {
db: PostgresUserDb { connectionString: dbUrl }
log: StructuredLogger { level: LogLevel.Info }
}
} :> Retry(max: 3, backoff: Exponential)
:> Timeout(5000ms)
Der outputs-Body wird nicht sofort ausgeführt. Er beschreibt, was die EffectEngine tun soll, wenn sie den Blueprint interpretiert. Der Compiler prüft nur, dass die Typen stimmen: dass db.getUserByEmail existiert, dass der Rückgabetyp zu Result[R, E] passt, dass alle Felder in env die richtigen Services implementieren. :> Serializable ist kein notwendiges Refinement — Serialisierbarkeit ist eine intrinsische Eigenschaft des Blueprint-TypeKinds.
5. Compiler-Semantik¶
5.1 Typprüfung¶
Der Compiler behandelt Blueprint-Felder wie normale typisierte Ausdrücke. Keine Sondersemantik, keine extra Passes. Die Typprüfung stellt sicher:
Jedes Feld im Blueprint-Literal hat den deklarierten Typ. Funktionsausdrücke in Feldern sind typkorrekt. Generische Parameter werden korrekt aufgelöst. Refinements sind mit dem Blueprint-Typ kompatibel.
5.2 Zusätzliche Blueprint-spezifische Prüfungen¶
Über die normale Typprüfung hinaus kann der Compiler für Blueprints kontextspezifische Validierungen durchführen, die vom konkreten Blueprint-Typ abhängen:
Für einen CallGraph prüft der Compiler, ob alle in inputs deklarierten Services in env gebunden sind. Für einen ActorSpec prüft er, ob die receive-Funktion den State-Typ korrekt zurückgibt. Für einen PipelineSpec prüft er, ob die Typen zwischen aufeinanderfolgenden Stages kompatibel sind.
Diese Prüfungen sind keine Compiler-Magie — sie ergeben sich aus den Typconstraints der Blueprint-Definition. Wenn inputs den Typ D hat und env ebenfalls D, dann erzwingt die Typunifikation, dass beide übereinstimmen.
5.3 Kein Compiler-Sonderwissen über Engines¶
Der Compiler weiß nicht, was eine EffectEngine ist. Er weiß nicht, was ein Supervisor tut. Er kennt nur den Blueprint-Typ und seine Felder. Die gesamte Interpretationslogik liegt in der Stdlib oder in Plugins. Der Compiler liefert Typgarantien, keine Ausführungsgarantien.
5.4 Serialisierbarkeit als Compiler-Regel¶
Der Compiler erzwingt, dass jeder Typ der in einem Blueprint-Feld vorkommt, das Serializable-Protocol implementiert. Das ist keine Empfehlung, sondern ein Typfehler wenn es nicht erfüllt ist.
Funktionsreferenzen in Blueprint-Feldern können qualifizierte Top-Level-Funktionen oder capture-freie Lambdas sein. Da ein Blueprint ein Dictionary ist und keinen lexikalischen Scope besitzt, existiert kein umgebender Kontext aus dem Variablen eingefangen werden könnten. Captures sind nicht verboten — sie sind strukturell ausgeschlossen. Eine Funktion in einem Blueprint-Feld bezieht ihre Inputs ausschließlich über ihre Parameter, die von der Engine aus den anderen Dictionary-Feldern aufgelöst werden.
Custom Types die spezielle Serialisierungsbehandlung benötigen, definieren ihre eigene provide Serializable-Implementierung. Primitive Typen haben Built-in-Serialisierung. Structs serialisieren feldweise per AutoDerive. Die Compiler-Regel garantiert: wenn ein Blueprint kompiliert, ist er vollständig serialisierbar und netzwerkübertragbar.
6. Runtime-Semantik¶
6.1 Blueprints als Laufzeitwerte¶
Ein Blueprint ist ein regulärer Laufzeitwert, der als Handle von der JME verwaltet wird. Er ist intrinsisch serialisierbar — diese Eigenschaft folgt aus dem TypeKind, nicht aus einem Refinement. Ein Blueprint kann:
- an Funktionen übergeben werden,
- in Variablen gehalten werden,
- serialisiert und über das Netzwerk übertragen werden,
- auf einem Remote-Node deserialisiert und von einer dortigen Engine interpretiert werden,
- über Task-Grenzen gesendet werden,
- zur Laufzeit inspiziert werden (Felder sind lesbar).
Funktionsreferenzen in Blueprints werden bei der Serialisierung als qualifizierte Namen übertragen. Der Zielknoten löst den Namen gegen seine lokale Symboltabelle auf. Voraussetzung: beide Knoten haben dieselben Pakete geladen. Fehlende Pakete können über den Content-Addressable Store nachgeladen werden.
6.2 Lazy Evaluation¶
Kein Feld eines Blueprints wird bei der Instanziierung ausgewertet. Der Blueprint beschreibt, was passieren soll — die Engine entscheidet wann. Das ist kein Spezialfall, sondern folgt aus der Semantik des TypeKinds: Blueprint-Felder sind Beschreibungen, nicht Berechnungen.
6.3 Engines als dynamische Runtimes¶
Eine Engine ist die dynamische Runtime für einen Blueprint. Ohne Engine ist ein Blueprint ein inertes Datenobjekt — die Engine gibt ihm Bedeutung. Sie empfängt einen typisierten Blueprint, validiert seine operationale Semantik, baut daraus ihre Ausführungsstruktur auf und liefert ein typisiertes Ergebnis zurück.
Die Engine ist normaler JDL-Code in der Stdlib oder in einem Plugin. Sie hat keine privilegierte Position im System. Sie ist Ring-2-Code, der einen typisierten Blueprint als Eingabe nimmt.
6.4 Das Engine-Protocol¶
Das Engine[B, Out]-Protocol definiert den Vertrag zwischen Blueprint und Engine mit zwei Typparametern: B ist der Blueprint-Typ, Out ist was die Engine produziert.
protocol Engine[B, Out] {
def validate(spec: B) -> Result[(), [DiagnosticEntry]]
def execute(spec: B) -> Out
}
validate prüft die operationale Semantik des Blueprints, bevor er ausgeführt wird. Das ist komplementär zur Typprüfung des Compilers: der Compiler beweist, dass die Typen stimmen. validate beweist, dass die Konfiguration operativ Sinn ergibt — ob ein Connection-String gültig ist, ob ein Cron-Ausdruck parsebar ist, ob referenzierte Services zur Laufzeit verfügbar sind.
execute interpretiert den Blueprint und liefert ein typisiertes Ergebnis. Der Return-Typ Out ist bewusst nicht auf Result festgelegt — die Engine entscheidet, was sie produziert:
provide Engine[CallGraph[R, E, D], Result[R, E]] for EffectRuntime {
def validate(spec: CallGraph[R, E, D]) -> Result[(), [DiagnosticEntry]] {
validateDependencies(spec.inputs, spec.env)
}
def execute(spec: CallGraph[R, E, D]) -> Result[R, E] {
val deps = resolveDeps(spec.env)
spec.outputs(deps)
}
}
provide Engine[ActorSpec[S, M, R], ActorRef] for ActorSupervisor {
def validate(spec: ActorSpec[S, M, R]) -> Result[(), [DiagnosticEntry]] {
validateSupervisionPolicy(spec.supervision)
}
def execute(spec: ActorSpec[S, M, R]) -> ActorRef {
spawn(spec.init, spec.receive, spec.supervision)
}
}
Für denselben Blueprint-Typ können mehrere Engines existieren:
provide Engine[CallGraph[R, E, D], Result[R, E]] for EffectRuntime { ... } // Production
provide Engine[CallGraph[R, E, D], Result[R, E]] for TestSimulator { ... } // Test
provide Engine[CallGraph[R, E, D], TraceLog] for DebugTracer { ... } // Debug
Selber Blueprint, verschiedene Interpretationen. Der Blueprint bleibt identisch, die Engine bestimmt die Interpretation und den Return-Typ. Das formalisiert die Engine-Blueprint-Beziehung über das Typsystem — der Compiler kann prüfen, ob für einen gegebenen Blueprint-Typ eine Engine existiert.
Ein DryRunner ist kein Sonderfall mehr — er ruft validate auf und verzichtet auf execute.
6.4.2 CompilerDB-Integration¶
Compiler-nahe Engines wie ParserEngine, TypeEngine, ProtocolDispatcherEngine, FfiBindingEngine, IRBuilderEngine und GeneratorEngine können ab Stage 2 als QueryProvider der CompilerDB auftreten. Sie werden dabei nicht zu CompilerDB-Interna. Die CompilerDB stellt nur Query-Auswertung, Cache, Dependency-Tracking, SeedStore, DescriptorStore und Materializer bereit.
Eine Engine, die eine CompilerDB-Query beantwortet, liest andere Phasen ausschließlich über Queries und gibt Descriptoren oder MaterializationPatches zurück. Direkter Zugriff auf fremde Engine- oder Ring-0-Zustände ist verboten. Ja, auch dann, wenn es gerade praktisch wäre. Besonders dann.
6.5 Optionale Engine-Capabilities¶
Nicht jede Engine muss alles können. Zusätzliche Fähigkeiten werden über separate Protocols modelliert — konsistent mit dem bestehenden provide-Kompositionsmodell:
Eine Engine die das kann, implementiert beide Protocols. Eine die's nicht kann, lässt es weg. Kein Zwang, keine leeren Default-Methoden.
6.6 Lifecycle auf Handles, nicht auf Engines¶
Die Engine startet Dinge, sie managed sie nicht. Lifecycle-Management (Stop, Pause, Status) lebt auf den Werten die execute zurückgibt — ActorRef, ServerHandle, PipelineHandle. Diese Handles bieten eigene APIs die zur jeweiligen Domäne passen. Das ist konsistent mit der Blueprint-Philosophie: die Engine interpretiert die Beschreibung und übergibt Kontrolle an den laufenden Wert.
7. Anwendungsbereiche¶
7.1 Effect-Orchestrierung (CallGraph)¶
Der primäre Anwendungsfall. Ein CallGraph beschreibt die Orchestrierung von effektbehafteten Operationen mit deklarativem Dependency-Wiring:
type CallGraph[R, E, D] : blueprint {
inputs: D
outputs: D -> Result[R, E]
env: D
}
val userRegistration = CallGraph {
id: "user-reg-v1"
name: "User Registration"
class: [Api, UserManagement, Critical]
inputs: { db: UserDb, log: LogService, mail: MailService }
outputs: { db, log, mail } =>
val validated =? validateEmail(email)
val user =? db.insertUser(User { name, email: validated, age })
log.info(f"Registered: {user.name}")
mail.sendWelcome(user.email)
Ok(user)
env: {
db: PostgresUserDb { connectionString: env.DATABASE_URL }
log: StructuredLogger { level: LogLevel.Info }
mail: SmtpMailService { host: "smtp.example.com" }
}
} :> Retry(max: 3, backoff: Exponential)
:> Timeout(30s)
:> CircuitBreaker(threshold: 5, resetAfter: 60s)
Die EffectEngine interpretiert diesen Blueprint, löst Dependencies auf, führt den outputs-Body aus, wendet die Refinements (Retry, Timeout, Circuit Breaker) als Laufzeitverhalten an — alles ohne dass der Compiler etwas über Retry-Logik wissen muss.
7.2 Actor-Systeme (ActorSpec)¶
Aktoren werden deklarativ als Blueprints beschrieben. Ein Supervisor verwendet den Blueprint, um Aktoren zu spawnen, zu überwachen und bei Fehlern neu zu starten:
type ActorSpec[S, M, R] : blueprint {
init: () -> S
receive: (S, M) -> (S, R)
supervision: SupervisionPolicy
}
type SupervisorSpec : blueprint {
children: [ChildDef]
strategy: SupervisionStrategy
}
val userActor = ActorSpec {
id: "user-actor-v1"
name: "User Actor"
class: [Worker, UserManagement]
init: () => UserState { users: [], requestCount: 0 }
receive: (state, msg) => match msg {
| RegisterUser { name, email } =>
val user = User { name, email, id: generateId() }
val newState = UserState {
users: state.users ++ [user]
requestCount: state.requestCount + 1
}
(newState, Ok(user))
| GetUser { id } =>
val found = state.users |> filter(u => u.id == id) |> first
(state, found)
}
supervision: SupervisionPolicy.Restart {
maxRestarts: 3
withinSeconds: 60
}
}
val userSystem = SupervisorSpec {
children: [
ChildDef { spec: userActor, name: "user-worker", instances: 4 }
]
strategy: SupervisionStrategy.OneForOne
}
Der Supervisor ist eine Engine, die SupervisorSpec konsumiert. Sie kann den Blueprint inspizieren, die Kindkonfiguration lesen und Aktoren dynamisch spawnen oder ersetzen — alles basierend auf dem typgeprüften Blueprint.
7.3 HTTP-Server und Routing¶
Routing-Konfiguration als Blueprint statt als imperatives Setup:
type RouteSpec[Ctx] : blueprint {
routes: [RouteDef[Ctx]]
middleware: [Middleware[Ctx]]
errorHandler: (Ctx, AppError) -> HttpResponse
}
val apiRoutes = RouteSpec {
routes: [
Route.get("/users", listUsers)
Route.get("/users/{id}", getUser)
Route.post("/users", createUser)
Route.delete("/users/{id}", deleteUser)
]
middleware: [
Middleware.cors(CorsConfig { origins: ["https://app.example.com"] })
Middleware.rateLimit(RateLimitConfig { maxRequests: 100, windowMs: 60000 })
Middleware.auth(AuthConfig { provider: JwtAuth { secret: env.JWT_SECRET } })
]
errorHandler: (ctx, err) => match err {
| AppError.NotFound { msg } => HttpResponse.notFound(msg)
| AppError.Forbidden { msg } => HttpResponse.forbidden(msg)
| _ => HttpResponse.internalError("Unexpected error")
}
} :> Logging(access: true, errors: true)
:> Metrics(endpoint: "/metrics")
Der HTTP-Server ist eine Engine, die den RouteSpec konsumiert, den Routing-Baum aufbaut, Middleware anwendet und Requests dispatcht.
7.4 Stream-Processing und Pipelines¶
Datenströme als deklarative Verarbeitungspipelines:
type PipelineSpec[In, Out, E] : blueprint {
source: Source[In]
stages: [Stage]
sink: Sink[Out]
backpressure: BackpressurePolicy
errorPolicy: StreamErrorPolicy
}
val logPipeline = PipelineSpec {
source: Source.kafka(KafkaConfig {
brokers: ["kafka-1:9092", "kafka-2:9092"]
topic: "application-logs"
group: "log-processor"
})
stages: [
Stage.filter(log => log.level >= LogLevel.Warn)
Stage.map(log => EnrichedLog {
original: log
hostname: resolveHostname(log.sourceIp)
region: geoLocate(log.sourceIp)
})
Stage.batch(size = 100, flushInterval = 5s)
]
sink: Sink.elasticsearch(ElasticConfig {
nodes: ["es-1:9200"]
index: "enriched-logs"
})
backpressure: BackpressurePolicy.Bounded { bufferSize: 10000 }
errorPolicy: StreamErrorPolicy.SkipAndLog { maxSkips: 100 }
} :> Checkpoint(interval: 30s)
:> Metrics(tags: ["pipeline:logs"])
Die Pipeline-Engine baut aus diesem Blueprint die Verarbeitungskette auf, verwaltet Backpressure, Checkpointing und Fehlerbehandlung.
7.5 Scheduling und Cron-Jobs¶
Zeitgesteuerte Aufgaben als Blueprints:
type SchedulerSpec : blueprint {
tasks: [TaskDef]
timezone: Timezone
overlap: OverlapPolicy
}
val maintenanceTasks = SchedulerSpec {
tasks: [
TaskDef {
name: "cleanup-expired-sessions"
schedule: Cron("0 */6 * * *")
action: cleanExpiredSessions
}
TaskDef {
name: "generate-daily-report"
schedule: Cron("0 2 * * *")
action: generateDailyReport
}
TaskDef {
name: "health-check"
schedule: Interval(30s)
action: runHealthCheck
}
]
timezone: Timezone.UTC
overlap: OverlapPolicy.Skip
} :> AlertOnFailure(channel: "ops-alerts")
7.6 Client/Server-Architektur¶
API-Verträge als geteilte Blueprints zwischen Client und Server:
type ApiContract[Endpoints] : blueprint {
endpoints: Endpoints
version: SemVer
transport: TransportConfig
}
val userApi = ApiContract {
endpoints: {
listUsers: Endpoint.get("/users") -> Result[[User], ApiError]
getUser: Endpoint.get("/users/{id}") -> Result[User, ApiError]
createUser: Endpoint.post("/users", CreateUserRequest) -> Result[User, ApiError]
}
version: SemVer(1, 0, 0)
transport: TransportConfig.Http {
baseUrl: "https://api.example.com"
serialization: Wire.Json
}
} :> Versioned
:> DocumentedOpenApi
Derselbe Blueprint wird von zwei verschiedenen Engines konsumiert: die Server-Engine generiert Handler-Stubs und Routing, die Client-Engine generiert einen typisierten HTTP-Client. Beide Seiten teilen denselben Vertrag — Typänderungen am Blueprint erzwingen Anpassungen auf beiden Seiten zur Compile-Zeit.
7.7 Deployment und Infrastruktur¶
Deployment-Konfiguration als Blueprint, typsicher und versionierbar:
type DeploySpec : blueprint {
services: [ServiceDef]
network: NetworkConfig
storage: [StorageDef]
scaling: ScalingPolicy
}
val productionDeploy = DeploySpec {
services: [
ServiceDef {
name: "api-server"
image: "registry.example.com/api:latest"
replicas: 3
ports: [Port { container: 8080, host: 443, protocol: Https }]
env: { DATABASE_URL: secret("db-credentials") }
health: HealthCheck.http("/health", interval = 10s)
}
ServiceDef {
name: "worker"
image: "registry.example.com/worker:latest"
replicas: 2
env: { QUEUE_URL: secret("queue-credentials") }
}
]
network: NetworkConfig {
ingress: IngressDef { domain: "api.example.com", tls: true }
internal: NetworkDef { subnet: "10.0.0.0/16" }
}
scaling: ScalingPolicy.Auto {
minReplicas: 2
maxReplicas: 10
metric: CpuUtilization(target: 70)
}
}
7.8 Test-Orchestrierung¶
Test-Suites als Blueprints mit deklarativer Konfiguration von Fixtures, Mocks und Ausführungsstrategien:
type TestSuiteSpec[Ctx] : blueprint {
setup: () -> Ctx
teardown: Ctx -> ()
tests: [TestDef[Ctx]]
strategy: TestStrategy
}
val userTests = TestSuiteSpec {
setup: () => TestCtx {
db: InMemoryUserDb { users: [] }
log: NullLogger {}
}
teardown: (ctx) => ctx.db.reset()
tests: [
TestDef {
name: "register valid user"
run: (ctx) =>
val result = registerUser(ctx.db, ctx.log, "Alice", "alice@test.com", 30)
match result {
| Ok(_) => assert(true)
| Err(e) => fail(f"Expected Ok, got Err: {e}")
}
}
TestDef {
name: "reject duplicate email"
run: (ctx) =>
registerUser(ctx.db, ctx.log, "Alice", "alice@test.com", 30)
val result = registerUser(ctx.db, ctx.log, "Bob", "alice@test.com", 25)
match result {
| Err(UserError.AlreadyExists { email }) =>
assert(email == "alice@test.com")
| other => fail(f"Expected AlreadyExists, got {other}")
}
}
]
strategy: TestStrategy.Parallel { maxConcurrency: 4 }
} :> Timeout(perTest: 5s, total: 60s)
:> Retry(flakyTests: 2)
7.9 Projektmanifest (ProjectSpec)¶
Das Jade-Projektmanifest ist selbst ein Blueprint. Kein TOML, kein YAML, kein separates Dateiformat — die Projektkonfiguration ist JDL-Code, der denselben Regeln folgt wie jeder andere Blueprint:
type ProjectSpec : blueprint {
name: str
version: SemVer
dependencies: [Dependency]
targets: [Target]
profiles: [Profile]
}
type Dependency : struct {
module: ModulePath
version: VersionConstraint
}
type Target : enum =
| Executable { entry: str }
| Library { exports: [ModulePath] }
| Plugin { manifest: PluginManifest }
type Profile : enum =
| Release { optimize: bool }
| Dev { debugSymbols: bool }
| Test { coverage: bool }
Eine konkrete Projektkonfiguration:
val project = ProjectSpec {
name: "user-service"
version: SemVer { major: 1, minor: 0, patch: 0 }
dependencies: [
Dependency { module: "jade::stdlib", version: Compatible("1.2.0") }
Dependency { module: "mycompany::auth", version: Compatible("3.0.0") }
Dependency { module: "community::postgres", version: Exact("2.1.0") }
]
targets: [
Target.Executable { entry: "main.jdl" }
]
profiles: [
Profile.Release { optimize: true }
Profile.Dev { debugSymbols: true }
Profile.Test { coverage: true }
]
} :> LockDeps(hashAlgorithm: Sha256)
:> MinJadeVersion(SemVer { major: 1, minor: 0, patch: 0 })
Die Build-Engine ist eine weitere Engine, die einen Blueprint konsumiert. Sie liest das ProjectSpec, löst Dependencies auf, generiert ein Lockfile und orchestriert den Build-Prozess. Der Compiler prüft, dass alle Felder typkorrekt sind — ein falscher VersionConstraint oder ein fehlender entry-Pfad wird zur Compile-Zeit gemeldet, nicht erst beim Build.
Das Bootstrapping-Problem existiert hier nicht: die Build-Engine ist ein vollständig initialisiertes Jade-Programm mit geladenem Seed und Stdlib. Sie liest das Projektmanifest als JDL-Input — genau wie der Nix-Evaluator ein flake.nix liest. Der Evaluator ist schon da, ProjectSpec ist ein ganz normaler Stdlib-Typ.
Der Vorteil gegenüber einem separaten Dateiformat: kein Formatbruch, kein Kontextwechsel, kein zweites Ökosystem an Tooling für Manifestdateien. Das Jade-Tooling (REPL, LSP, Playground) versteht das Manifest nativ, weil es JDL ist.
7.10 Code-Emission (CodeEmissionSpec)¶
Der Code Generator — die Komponente die validierte IR in ausführbaren Code übersetzt — ist selbst ein Blueprint mit Engine. Das ist möglich, weil die IR an dem Punkt bereits vollständig validiert ist: der Code Generator transformiert bewiesene Artefakte, er muss keine Constitution-Regeln erzwingen.
type CodeEmissionSpec[Target] : blueprint {
rules: [EmissionRule]
allocator: AllocatorStrategy
passes: [IrPass]
constants: ConstantPoolPolicy
output: OutputConfig[Target]
} :> Queryable()
type EmissionRule : struct {
pattern: IrPattern
emit: (IrNode, EmissionContext) -> [Instruction]
cost: i32
}
Der cost-Wert in jeder Rule ermöglicht Instruction Selection bei Alternativen — wenn dasselbe IR-Pattern durch verschiedene Instruktionssequenzen abgedeckt werden kann, gewinnt die günstigere. Verschiedene Engines produzieren verschiedene Outputs:
provide Engine[CodeEmissionSpec[JbcFormat], JbcOutput] for JbcEmitter { ... }
provide Engine[CodeEmissionSpec[NativeFormat], NativeOutput] for CraneliftBackend { ... }
provide Engine[CodeEmissionSpec[WasmFormat], WasmOutput] for WasmBackend { ... }
Selber Blueprint-Typ, drei Outputs: JBC-Bytecode, nativer Code via Cranelift, WebAssembly. Die Cranelift-Integration erfolgt über FFI (C-ABI). Der Code Generator selbst ist Ring-2-JDL-Code.
7.11 Parsing und Formatverarbeitung (ParserSpec)¶
ParserSpec-Blueprints beschreiben Grammatiken für beliebige Dateiformate:
type ParserSpec[T] : blueprint {
grammar: GrammarDef
tokenizer: TokenizerDef
transform: AstNode -> Result[T, ParseError]
validation: T -> Result[T, ValidationError]
} :> Queryable()
Die ParserEngine interpretiert den Blueprint und liefert getypte JDL-Daten zurück. Anwendungsfälle: HTML → getypter DOM-Baum, YAML → validiertes Config-Schema, Markdown → strukturierter Dokumentbaum, CSS → querybare Style-Regeln, Protobuf → typsichere Nachrichtenstrukturen.
Der wichtigste Anwendungsfall: der JDL-Parser selbst kann als ParserSpec-Blueprint in JDL implementiert werden. Der D-basierte Bootstrap-Parser versteht genug JDL um das System zu initialisieren. Ab dem Zeitpunkt übernimmt der JDL-Parser-Blueprint. Die Grammatik-Spezifikation und die Parser-Implementierung sind dasselbe Artefakt — inspizierbar, erweiterbar, self-documenting.
Das gibt der REPL und dem LSP Fähigkeiten, die in statisch kompilierten Sprachen normalerweise nicht verfügbar sind: Live-Parsing, Live-Typechecking, Hot-Swap von Blueprints im laufenden System, Grammatik-Abfragen für kontextuelle Code-Completion.
8. Komposition und Erweiterbarkeit¶
8.1 Blueprints sind kompositionsfähig¶
Blueprints können andere Blueprints referenzieren. Ein SupervisorSpec enthält ActorSpec-Referenzen. Ein DeploySpec kann PipelineSpec und SchedulerSpec als Service-Definitionen einbetten. Die Komposition ist statisch typisiert — der Compiler prüft, dass innere Blueprints mit den äußeren Erwartungen kompatibel sind.
8.2 Refinements: Composable und Domain-spezifisch¶
Refinements (:>) fügen Laufzeitverhalten zu Blueprints hinzu, ohne den Blueprint-Typ zu ändern. Sie fallen in zwei Kategorien:
Composable Refinements — Retry, Timeout, CircuitBreaker, Metrics, Tracing. Diese brauchen keine Engine-Kooperation. Sie wickeln sich als Middleware um den execute-Call der Engine — ein Zwiebel-Modell, bei dem jedes Refinement eine Schale hinzufügt. Die Reihenfolge der Refinements bestimmt die Verschachtelung und ist semantisch relevant: :> Retry(3) :> Timeout(5s) gibt jedem Versuch 5 Sekunden, :> Timeout(5s) :> Retry(3) gibt allen Versuchen zusammen 5 Sekunden.
Composable Refinements implementieren das EngineMiddleware-Protocol:
protocol EngineMiddleware[Config, Out] {
def apply(inner: () -> Out, config: Config) -> Out
}
provide EngineMiddleware[Retry, Result[R, E]] {
def apply(inner: () -> Result[R, E], config: Retry) -> Result[R, E] {
var attempts = 0
loop {
attempts = attempts + 1
match inner() {
| Ok(v) => Ok(v)
| Err(e) =>
if attempts >= config.max
Err(e)
else
sleep(config.backoff.delay(attempts))
}
}
}
}
Die Typconstraints regeln automatisch, wo was passt. Retry funktioniert nur wenn Out ein Result ist — ein Retry auf eine Engine die ActorRef zurückgibt ist ein Typfehler, weil kein provide EngineMiddleware[Retry, ActorRef] existiert.
Domain-Refinements — Checkpoint auf einem PipelineSpec, spezifische Supervision-Strategien auf einem ActorSpec. Diese werden von der Engine direkt aus dem Blueprint gelesen, weil nur die jeweilige Engine ihre Semantik kennt.
Die generische run-Funktion orchestriert beides:
def run[B, Desc, Out](spec: B, engine: RuntimeEngine[B, Desc, Out]) -> Result[Out, EngineError] {
val desc =? engine.describe(spec)
val inner = () => engine.instantiate(desc)
val chain = spec
|> refinements
|> filter(r => hasMiddleware(r, Out))
|> fold(inner, (acc, r) => () => r.apply(acc))
chain()
}
Die Engine liefert einen Descriptor und eine Instanziierungsfunktion. Domain-Refinements liest die Engine selbst aus dem Blueprint beziehungsweise materialisiert sie im Descriptor; composable Refinements werden von run automatisch angewendet.
Der Compiler prüft: jedes Refinement auf einem Blueprint muss entweder ein EngineMiddleware-provide für den Out-Typ der Engine haben, oder ein Domain-Refinement sein das der Blueprint-Typ explizit akzeptiert. Alles andere ist ein Typfehler.
8.3 Neue Blueprint-Typen ohne Sprachänderung¶
Ein neuer Blueprint-Typ erfordert keine Änderung am Compiler oder an der Grammatik. Er erfordert nur eine Typdefinition und eine Engine, die ihn konsumiert:
// Neuer Blueprint-Typ — reiner Userland-/Stdlib-Code
type StateMachineSpec[S, E] : blueprint {
initial: S
transitions: [(S, E) -> Option[S]]
onEnter: S -> ()
onExit: S -> ()
}
// Engine, die diesen Blueprint konsumiert — ebenfalls Userland-Code
provide Engine[StateMachineSpec[S, E], S] for StateMachineRunner {
def validate(spec: StateMachineSpec[S, E]) -> Result[(), [DiagnosticEntry]] {
if spec.transitions |> isEmpty
Err([DiagnosticEntry { msg: "StateMachine has no transitions" }])
else
Ok(())
}
def execute(spec: StateMachineSpec[S, E], events: Generator[E]) -> S {
var state = spec.initial
spec.onEnter(state)
for event in events {
val candidates = spec.transitions
|> map(t => t(state, event))
|> filter(opt => match opt { | Some(_) => true | None => false })
match candidates |> first {
| Some(Some(nextState)) =>
spec.onExit(state)
state = nextState
spec.onEnter(state)
| _ => ()
}
}
state
}
}
Kein Parser-Patch, kein Compiler-Update, kein VM-Patch. Das ist Erweiterbarkeit auf Sprachebene, nicht auf Toolchain-Ebene.
8.4 Queryable — Standard-Metadaten via TypeFns¶
Blueprints, die von einer Engine verwaltet werden, brauchen drei Standard-Metadaten: eine idempotente ID für gezielte Adressierung, einen menschenlesbaren Namen für Logs und Debugging, und eine Tag-basierte Klassifikation für Gruppen-Selektion.
Diese Metadaten gehören nicht ins Root-Level des Meta-Objekts — das ist für sealed/derived Compiler-Policies reserviert (share, memory, drop, own). Sie gehören in den meta.fields-Pfad, der die Compile-Zeit überlebt und zur Runtime von der Engine gelesen werden kann.
Die Injection erfolgt über TypeFns — den einzigen Mechanismus, über den das Meta-Objekt eines Typs befüllt wird:
typefn Identifiable() = <{ meta: { fields: { id: BlueprintId } } }>
typefn Named() = <{ meta: { fields: { name: str } } }>
typefn Classifiable() = <{ meta: { fields: { class: [Tag] } } }>
typefn Queryable() = Identifiable() + Named() + Classifiable()
// ergibt: <{ meta: { fields: { id: BlueprintId, name: str, class: [Tag] } } }>
Anwendung am Blueprint-Typ:
Der Compiler mergt das Meta-Record-Fragment in das Meta-Objekt des Typs. Bei der Instanziierung erzwingt er, dass der User die Meta-Felder angibt:
val userRegistration = CallGraph {
id: "user-reg-v1"
name: "User Registration"
class: [Api, UserManagement, Critical]
inputs: { db: UserDb, log: LogService }
outputs: { db, log } => ...
env: { ... }
} :> Retry(max: 3, backoff: Exponential)
Die Engine kann die Meta-Felder zur Laufzeit lesen und darüber selektieren:
// ID: gezielte Adressierung einer Instanz
val target = supervisor.findById("user-reg-v1")
// Name: menschenlesbare Anzeige in Logs und Tooling
log.info(f"Starting: {graph.name}")
// Class: Gruppen-Selektion über Tags — Integer-Vergleich via TagId
supervisor.children
|> filter(child => child.spec.class |> contains(BackgroundTask))
|> each(child => supervisor.restart(child))
Die Tag-basierte Klassifikation ist bewusst kein [str] sondern [Tag]. Tags haben eine TagId im Backend — Selektion ist Integer-Lookup, nicht String-Matching. Und der Compiler prüft, dass referenzierte Tags existieren:
Blueprints ohne :> Queryable() — wie ein ProjectSpec — haben diese Felder nicht. Das ist gewollt: nicht jeder Blueprint wird von einer Engine verwaltet, die Adressierung und Selektion braucht.
Dieser Mechanismus erfindet nichts Neues. TypeFns existieren, Meta-Record-Merging existiert, der meta.fields-Pfad für User-Metadaten existiert, Tags mit Runtime-Präsenz existieren. Es ist eine Anwendung bestehender Konzepte auf einen neuen Kontext.
9. Abgrenzung zu bestehenden Ansätzen¶
9.1 Gegenüber Nix Flakes¶
Nix Flakes waren die Inspiration für diesen Ansatz. Die Ähnlichkeiten: uniformes Record-Format, deklarative Beschreibung, Separation von Inputs und Outputs, Engine-basierte Interpretation.
Der entscheidende Unterschied: Nix Flakes sind ungetypt. Das Schema ist Konvention — wenn ein Feld falsch benannt oder falsch getypt ist, merkt das erst der Evaluator, und die Fehlermeldung zeigt auf eine Stelle tief in der Lazy-Evaluation-Kette, nicht auf die fehlerhafte Deklaration. JDL-Blueprints sind statisch getypt. Fehler werden vom Compiler an der Deklarationsstelle gemeldet.
9.2 Gegenüber Effect-TS / ZIO¶
Effect-TS und ZIO sind Effektsysteme, die Beschreibungen von Ausführung trennen. Der Ansatz ist ähnlich — Effects sind lazy Beschreibungen, die ein Runtime interpretiert. JDL geht denselben Weg mit EffectSpec als Blueprint-Typ und der EffectEngine als Runtime.
Der Unterschied: In Effect-TS und ZIO ist die Beschreibung über Combinators aufgebaut (pipe, flatMap, tap, provideLayer). Das ist funktional korrekt, aber die Lesbarkeit leidet bei komplexen Graphen. JDL-Blueprints sind Record-basiert — die Struktur ist visuell klar, ohne dass man eine Kette von Combinators entschlüsseln muss. Komposition entsteht durch Blueprint-Verschachtelung und imperativen Code in den run-Funktionen, nicht durch monadische Chains.
Außerdem: Effect-TS und ZIO sind Bibliotheken, die innerhalb einer bestehenden Sprache operieren. Sie können das Typsystem der Host-Sprache nutzen, aber nicht erweitern. JDL hat einen dedizierten TypeKind — die Sprache weiß, was ein Blueprint ist. Und weil Blueprints intrinsisch serialisierbar sind, kann ein EffectSpec über das Netzwerk gesendet und auf einem anderen Node ausgeführt werden — etwas das Effect-TS strukturell nicht kann.
9.3 Gegenüber Terraform / Pulumi / Kubernetes¶
Infrastruktur-als-Code-Tools wie Terraform (deklarativ, ungetypt) und Pulumi (imperativ, getypt) adressieren nur den Deployment-Bereich. Kubernetes verwendet YAML-Manifeste die in API-Calls übersetzt werden. JDL-Blueprints sind ein allgemeines Orchestrierungskonzept, das Deployment als einen von vielen Anwendungsfällen behandelt. Derselbe Mechanismus, der einen HTTP-Server konfiguriert, konfiguriert auch ein Actor-System, eine Stream-Pipeline oder ein Betriebssystem. Bei JadeOS ist der Blueprint die API — der Node empfängt ihn und die Engine interpretiert ihn direkt, ohne Übersetzungsschicht.
9.4 Gegenüber Erlang / OTP¶
Erlang/OTP hat Actor-Supervision als Runtime-Konvention. JDL hat Actor-Supervision als getypten Blueprint. Der Supervisor kann den ActorSpec inspizieren, Kindkonfigurationen lesen, Aktoren über Tags selektieren und das gesamte Setup validieren bevor ein Aktor gestartet wird. In Erlang ist die Supervisor-Konfiguration ein Erlang-Term — korrekt getypt nur durch Konvention.
9.5 Gegenüber Odin / Zig¶
Odin und Zig lösen das Problem von 1985 besser — Low-Level-Kontrolle ohne C's schlimmste Fehler. Die Architektur ist klassisch: Compiler → nativer Code → Ausführung. Das setzt natürliche Grenzen bei Introspection, Hot-Reload, Plugin-Loading, Runtime-Parsing. Jade hat diese Grenzen nicht, weil die VM als Ausführungsschicht dazwischensteht.
9.6 Das Alleinstellungsmerkmal¶
Keine bestehende Sprache vereint alle folgenden Eigenschaften in einem Konzept:
Blueprints sind ein Sprach-Primitiv (eigener TypeKind), nicht eine Bibliothekskonvention. Sie sind statisch getypt mit vollständiger Compile-Zeit-Validierung. Sie sind ein uniformes Format für alle Orchestrierungskontexte. Sie sind erweiterbar ohne Sprachänderungen. Die Engines, die sie konsumieren, sind normaler Userland-Code.
Andere Sprachen haben Teile davon. Nix hat das uniforme Format, aber kein Typsystem. Effect-TS hat die Effekt-Trennung, aber kein Sprach-Primitiv. Erlang/OTP hat Actor-Supervision, aber als Runtime-Konvention, nicht als getypten Blueprint. Terraform hat deklarative Infrastruktur, aber als eigene Sprache (HCL), nicht integriert in eine General-Purpose-Sprache.
JDL ist die erste Sprache, die deklarative Orchestrierung als getyptes Sprach-Primitiv behandelt.
10. Interaktion mit bestehenden Jade-Konzepten¶
10.1 Services und Provide¶
Blueprints ersetzen service und provide nicht — sie nutzen sie. Ein CallGraph deklariert Dependencies als Services in inputs. Die konkreten Implementierungen in env sind provide-Instanzen. Das bestehende Service/Provide-System liefert die Abstraktion, der Blueprint liefert die Verdrahtung.
10.2 Effektsystem¶
Effekte in JDL sind Blueprints. Ein EffectSpec[R, E, D] beschreibt eine Berechnung die noch nicht ausgeführt wurde — mit Erfolgstyp R, Fehlertyp E und Dependencies D:
Das ist kein separates Konzept neben Blueprints — es ist ein Blueprint-Typ mit eigener Engine. Die EffectEngine interpretiert den EffectSpec, löst Dependencies auf und führt die run-Funktion aus. Composable Refinements (:> Retry, :> Timeout) werden von der generischen run-Funktion als Middleware angewendet.
Die Komposition von Effekten erfolgt nicht über monadische Combinatoren wie in Effect-TS oder ZIO, sondern über Blueprint-Verschachtelung und imperativem Code in den run-Funktionen. Sequenzierung ist =? in einer Funktion. Orchestrierung ist ein CallGraph oder eine StageChain die mehrere EffectSpecs verdrahtet. Cross-Cutting Concerns sind Refinements. Drei Ebenen, jede mit eigenem Mechanismus, ohne Vermischung.
10.3 Refinements, Meta-Records und Meta-Objekt-Struktur¶
Blueprints unterstützen sowohl Suffix-Refinements (:>) als auch Präfix-Meta-Records (<{ }>). Refinements am Blueprint werden von der Engine als Laufzeitkonfiguration interpretiert. Meta-Records am Blueprint-Typ werden vom Compiler als Compile-Zeit-Constraints behandelt.
Das Meta-Objekt eines Blueprint-Typs folgt der generellen JDL-Struktur: das Root-Level (<{ share: ..., memory: ..., drop: ... }>) ist für sealed/derived Compiler-Policies reserviert. User-facing Metadaten, die die Compile-Zeit überleben — wie die Queryable-Felder id, name, class — leben im meta.fields-Pfad. Diese Trennung stellt sicher, dass Compiler-Policies und User-Metadaten sich nicht gegenseitig stören.
10.4 Ring-Architektur¶
Blueprints und ihre Engines leben in Ring 2 (Plugins und Userland). Sie haben keinen privilegierten Zugriff auf Ring 0 (JME) oder Ring 1 (VM, Type Engine). Die VM hält Blueprint-Werte als Handles, weiß aber nicht, was sie bedeuten. Die Interpretation liegt vollständig in Ring 2.
10.5 Plugin-System¶
Neue Blueprint-Typen und ihre Engines können als Plugins verteilt werden. Ein Plugin kann einen StateMachineSpec-Typ definieren und eine StateMachineEngine bereitstellen. Das Plugin-System behandelt sie wie jeden anderen Typ und jede andere Funktion — keine Sonderbehandlung.
10.6 Wire/Serialisierung¶
Blueprints sind intrinsisch serialisierbar — das ist eine Eigenschaft des TypeKinds, kein optionales Refinement. Ein serialisierter Blueprint kann über das Netzwerk gesendet und auf einem anderen Knoten von einer Engine interpretiert werden. Funktionsreferenzen werden als qualifizierte Namen serialisiert und auf dem Zielknoten gegen die lokale Symboltabelle aufgelöst. Fehlende Pakete können über den Content-Addressable Store nachgeladen werden.
Die Transport-Pipeline ist reine Komposition bestehender Konzepte: Blueprint → Serializable.serialize → Stream → Transform (SSL/Compression) → Network → Transform (Decrypt) → Stream → Serializable.deserialize → Blueprint. Jeder Schritt ist ein Protocol, ein Generator oder ein Transformer.
10.7 Generators und Collectors¶
Generators und Collectors bleiben imperative/funktionale Konstrukte. Ein Blueprint kann Generator-basierte Datenquellen in seinen Feldern referenzieren (etwa als Source in einem PipelineSpec), aber der Generator selbst ist kein Blueprint — er ist ein Wert, der Daten produziert.
10.8 JadeOS und Node-Architektur¶
Blueprints sind das Fundament der geplanten JadeOS-Architektur. Ein JadeOS-Node ist eine laufende Jade VM, die Blueprints nativ versteht. SystemSpec-Blueprints beschreiben vollständige Node-Konfigurationen. Nodes bilden ein Mesh-Netzwerk, discovern sich gegenseitig über Queryable-Tags und tauschen Blueprints über das Netzwerk aus.
Der Provisioning-Flow: ein frischer Node bootet, empfängt seinen SystemSpec als serialisierten Blueprint von einem aktiven Node, löst Pakete über den Content-Addressable Store auf und appliziert die Konfiguration atomar. Die Backup-Strategie ergibt sich aus der Architektur: der gesamte Zustand eines Nodes ist sein SystemSpec plus die aktuelle Generation im Store.
Infrastructure as Code ist kein separates Tool — es ist die Natur des Systems. Software as a Service ergibt sich aus ServiceSpec-Blueprints die auf Nodes deployed, von Supervisors verwaltet und über Refinements skaliert werden.
Die JadeOS-Funktionalität lebt nicht in der Stdlib, sondern im nachladbaren jdl::node-Namespace als First-Party-Plugin. Die Stdlib bleibt schlank und stellt nur den kleinsten gemeinsamen Nenner bereit.
11. VM-Repräsentation¶
11.1 Handle-basiert¶
Ein Blueprint-Wert wird von der JME als Handle verwaltet. Die VM kennt den TypeKind Blueprint und weiß, dass solche Werte nicht wie Structs traversiert werden — sie werden an Engines übergeben, die sie interpretieren.
11.2 Dictionary-Repräsentation¶
Zur Laufzeit ist ein Blueprint eine geordnete Key-Value-Struktur (Dictionary). Werte sind entweder Daten (direkte Werte) oder Funktionsreferenzen (Funktionszeiger, vom Compiler als normale Funktionswerte kompiliert). Die Engine liest Felder über normalen Feldzugriff und ruft Funktionsreferenzen über den Standard-Dispatch der VM auf — kein Reflection, keine spezielle Instruktion.
11.3 Felder sind lesbar¶
Blueprint-Felder sind zur Laufzeit lesbar, wie bei Structs. Die Engine kann graph.inputs, graph.env, graph.outputs lesen und auswerten. Der Feldzugriff ist ein normaler VM-Befehl.
11.4 Kein spezielles Instruction-Set¶
Blueprints erfordern keine neuen Core-Instruktionen in der VM. Sie werden als Handle-basierte Werte gehalten und über Feldzugriff und Funktionsaufrufe gelesen. Die gesamte Interpretation ist Ring-2-Code.
12. Fallstricke und Einschränkungen¶
12.1 Blueprints sind kein Allheilmittel¶
Nicht jedes Problem braucht einen Blueprint. Eine einfache Funktion, die zwei Zahlen addiert, braucht keine deklarative Orchestrierung. Blueprints sind sinnvoll, wenn es um Komposition, Konfiguration, Dependency-Wiring oder Laufzeitverhalten-Steuerung geht. Sie für triviale Logik zu verwenden wäre Over-Engineering.
Faustregel: Wenn der Code keine externen Dependencies hat, keinen Fehlerbehandlungsstrategie-Entscheid braucht und nicht inspiziert oder transformiert werden muss — ist eine normale Funktion die bessere Wahl.
12.2 Funktionsreferenzen und Dictionary-Kontext¶
Blueprint-Felder können Funktionsreferenzen enthalten (z.B. outputs: D -> Result[R, E]). Da ein Blueprint ein Dictionary ist — eine geordnete Key-Value-Struktur — und keinen lexikalischen Scope besitzt, sind Captures nicht verboten, sondern strukturell ausgeschlossen. Es gibt keinen umgebenden Kontext aus dem Variablen eingefangen werden könnten.
Funktionsreferenzen in Blueprints sind entweder qualifizierte Top-Level-Funktionen oder capture-freie Lambdas. Beide sind serialisierbar: Top-Level-Funktionen werden als qualifizierte Namen übertragen (z.B. myapp::registration::processOrder), capture-freie Lambdas werden als Funktionszeiger kompiliert. Der Zielknoten löst den Namen gegen seine lokale Symboltabelle auf. Voraussetzung: beide Knoten haben dieselben Pakete geladen. Fehlende Pakete können über den Content-Addressable Store nachgeladen werden.
Werte die zur Blueprint-Konstruktionszeit bekannt sind, werden als Felder im Dictionary abgelegt — nicht als Captures in einer Closure. Der Blueprint ist nach der Konstruktion self-contained:
// region wird nicht gecaptured — sie befüllt Dictionary-Felder
def createOrderProcessing(region: Region) -> CallGraph {
CallGraph {
id: f"order-{region.code}"
env: OrderDeps { payment: region.paymentProvider }
outputs: processOrder
}
}
12.3 Typ-Explosion bei generischen Blueprints¶
Blueprint-Typen mit vielen Typparametern (CallGraph[R, E, D], PipelineSpec[In, Out, E]) können zu komplexen Typausdrücken führen. Das ist ein allgemeines Problem generischer Typen, nicht spezifisch für Blueprints. TypeFns und Typ-Aliase können die Lesbarkeit verbessern:
typefn WebGraph(R) = CallGraph[R, AppError, WebDeps]
val handler: WebGraph(HttpResponse) = CallGraph { ... }
12.4 Engine-Kompatibilität¶
Verschiedene Versionen einer Engine können denselben Blueprint-Typ unterschiedlich interpretieren. Wenn sich die Semantik von Retry(max: 3) ändert, betrifft das alle Blueprints, die dieses Refinement verwenden. Die Lösung ist einfach: Blueprint-Typ und Engine leben im selben Modul und werden über das Modulsystem gemeinsam versioniert. Wer den Typ importiert, bekommt die passende Engine aus derselben Paketversion. Kein Sondermechanismus nötig — das ist normales Dependency-Management.
12.5 Debugging von Blueprints¶
Da Blueprints lazy sind und ihre Ausführung von einer Engine gesteuert wird, ist das Debugging anders als bei imperativem Code. Ein Breakpoint in einem outputs-Body zeigt den Aufrufstack der Engine, nicht den Kontext, in dem der Blueprint deklariert wurde. Tooling muss Blueprint-Deklaration und Engine-Ausführung verknüpfen können. Das ist lösbar, aber erfordert explizite Unterstützung im Debugger.
12.6 Kein neues Primitive für jede Domäne¶
Die Versuchung wird groß sein, für jede Anwendungsdomäne einen eigenen Blueprint-Typ zu definieren. Das ist gewollt und korrekt — aber jeder Blueprint-Typ braucht eine Engine, die ihn interpretiert. Ein Blueprint ohne Engine ist ein toter Typ. Blueprint-Typ und Engine gehören ins selbe Modul.
13. Bootstrap-Implikationen¶
Der Blueprint-TypeKind muss in der Bootstrap-Spec als hardcoded TypeKind registriert werden. Der Bootstrap Seed selbst enthält keine konkreten Blueprint-Definitionen — CallGraph, ActorSpec und Co. sind Stdlib- oder Plugin-Typen, keine Bootstrap-Primitive.
Der Seed enthält nur die TypeKind-Registrierung:
Blueprint als TypeKind ist vor dem Laden der Stdlib bekannt.
Konkrete Blueprint-Typen werden durch die Stdlib oder Plugins definiert.
Das ist konsistent mit BSP-11 (der Seed enthält nur transitive Bootstrap-Notwendigkeiten) und BSP-14 (der Seed definiert keine neuen TypeKinds — Blueprint ist ein bestehender TypeKind ab diesem Zeitpunkt).
14. Auswirkungen auf bestehende Spezifikationen¶
14.1 Grammatik-Skizze (08)¶
BlueprintBody wird als neue Produktion unter TypeBody eingefügt. Die Feldsyntax ist identisch mit StructBody. Die Trennung ist rein semantisch.
14.2 Bootstrap-Spec (11)¶
Die TypeKind-Liste wird um Blueprint erweitert. Der Seed muss Blueprint als TypeKind kennen, damit die Stdlib Blueprint-Typen definieren kann.
14.3 Typsystem-Spec (02)¶
Blueprint wird als TypeKind mit eigener Semantik definiert: Felder sind Beschreibungen, nicht Werte. Die Typprüfung für Blueprint-Instanziierungen wird beschrieben.
14.4 Runtime-Modell (03) und JadeValue/Register-Layout (11)¶
Blueprint-Werte werden als Handle-basierte Laufzeitwerte definiert. Feldzugriff funktioniert wie bei Structs. Keine neuen VM-Instruktionen erforderlich.
14.5 Sprachüberblick (01)¶
Der CallGraph-Abschnitt wird auf Blueprint-Syntax umgeschrieben. Die bestehenden CallGraph-Keywords (Node, flow, requires, env als spezialisierte Grammatik-Konstrukte) werden durch die uniforme Blueprint-Feldsyntax ersetzt.
14.6 Design Constitution (12)¶
Die Deklarativ/Imperativ-Trennung wird als Architekturprinzip verankert. Ein neuer Abschnitt beschreibt, warum JDL zwei Ausdrucksebenen hat und wie sie interagieren.
14.7 Namespaces & Modulsystem (spec-namespaces-und-module)¶
Das Projektmanifest als Blueprint ersetzt externe Manifestformate. Die Modulsystem-Spec muss beschreiben, wie die Build-Engine das ProjectSpec liest und Dependencies über das Modulsystem auflöst.
14.8 Tag-System und Konsolidierung¶
Die Tag-Spec muss die Unterscheidung zwischen sealed/derived Tags (compile-time-only, Root-Level des Meta-Objekts) und User-Tags (runtime-fähig, meta.fields-Pfad) explizit machen. Blueprint-Klassifikation via class: [Tag] ist ein konkreter Anwendungsfall für User-Tags mit Runtime-Präsenz. Die TagId-basierte Runtime-Repräsentation muss für User-Tags spezifiziert werden.
14.9 Code Generator (06)¶
Der Code Generator (IR → JBC Emission) wandert von Ring 1 nach Ring 2 als CodeEmissionSpec-Blueprint mit Engine. Die Code-Generator-Spec muss den Blueprint-Typ, die Emission-Rules und die Engine-Schnittstelle definieren. Die IR-Erzeugung selbst bleibt in Ring 1 (D), da sie eng mit der Type Engine gekoppelt ist.
14.10 JadeOS Node-Architektur (neue Spec)¶
Eine eigenständige Spec für die JadeOS-Node-Architektur muss geschrieben werden. Sie definiert: NodeSpec und MeshPolicy als Blueprint-Typen, das Discovery-Protokoll, den Provisioning-Flow, die verteilte Store-Architektur (Content-Addressable, Node-zu-Node-Replikation), die Serialisierungs-Anforderungen für Netzwerk-Transport, und die Consensus-Mechanik für Mesh-Entscheidungen. Diese Spec lebt im jdl::node-Namespace.
15. Normative Regeln¶
BP-1: Blueprint ist ein eigenständiger TypeKind, kein Struct mit Tag.
BP-2: Blueprint-Felder sind Beschreibungen, nicht Werte. Ihre Auswertung
liegt bei der konsumierenden Engine.
BP-3: Der Compiler validiert die Typkorrektheit von Blueprints.
Er hat kein Wissen über spezifische Engines.
BP-4: Engines sind Userland-Code (Ring 2). Sie haben keinen
privilegierten Zugang zu VM oder JME. Engines sind
provide-Implementierungen des Engine[B, Out]-Protocols.
Das Protocol definiert zwei Methoden: validate (operationale
Semantik prüfen) und execute (Blueprint interpretieren).
Optionale Capabilities (z.B. Explainable) werden über
separate Protocols modelliert.
BP-5: Blueprints sind intrinsisch serialisierbar. Dies ist eine
Eigenschaft des TypeKinds, kein optionales Refinement.
Alle Typen in Blueprint-Feldern müssen Serializable
implementieren. Da Blueprints Dictionaries sind und
keinen lexikalischen Scope besitzen, sind Captures
strukturell ausgeschlossen — nicht verboten, sondern
unmöglich.
BP-6: Neue Blueprint-Typen erfordern keine Compiler- oder
Grammatik-Änderungen.
BP-7: Refinements an Blueprints fallen in zwei Kategorien:
Composable Refinements (Retry, Timeout, Metrics, etc.)
implementieren das EngineMiddleware-Protocol und werden
von der generischen run-Funktion als Middleware um den
execute-Call gewickelt. Domain-Refinements werden von
der Engine direkt aus dem Blueprint gelesen. Der Compiler
prüft, dass jedes Refinement entweder ein EngineMiddleware-
provide hat oder vom Blueprint-Typ akzeptiert wird.
BP-8: Der Blueprint-TypeKind ist Teil des Bootstrap-Seeds.
Konkrete Blueprint-Typen gehören in die Stdlib oder Plugins.
BP-9: Blueprint-Typen und ihre Engines leben im selben Modul
und werden über das Modulsystem gemeinsam versioniert.
BP-10: Die Deklarativ/Imperativ-Trennung ist ein Architekturprinzip.
Imperativer Code beschreibt Berechnung, Blueprints beschreiben
Orchestrierung. Vermischung ist ein Designfehler.
BP-11: Das Projektmanifest ist ein Blueprint (ProjectSpec). Kein
separates Dateiformat. Die Build-Engine konsumiert es wie
jede andere Engine ihren Blueprint.
BP-12: Blueprint-Typen und ihre Engines werden über das Modulsystem
gemeinsam versioniert. Gleiches Paket, gleiche Version.
BP-13: Standard-Metadaten (id, name, class) werden über TypeFns
in den meta.fields-Pfad des Meta-Objekts injiziert.
Das Root-Level des Meta-Objekts bleibt sealed/derived
Compiler-Policies vorbehalten.
BP-14: Meta-Feld-Injection via TypeFns ist Blueprint-exklusiv.
Structs unterstützen keine Feld-Injection — ihr
Speicherlayout ist statisch determiniert.
BP-15: Blueprint-Klassifikation via class nutzt Tags mit TagId-
Backend (Integer-Lookup), nicht Strings.
BP-16: Die Runtime-Repräsentation eines Blueprints ist ein
Handle auf eine geordnete Key-Value-Struktur (Dictionary).
Funktionsreferenzen werden als Funktionszeiger gehalten
und bei Serialisierung als qualifizierte Namen übertragen.
BP-17: Für denselben Blueprint-Typ können mehrere Engines
existieren (Production, Test, Debug). Der Blueprint
bleibt identisch, die Engine bestimmt die Interpretation.
Ein DryRun ist kein eigener Engine-Typ, sondern ein
Aufruf von validate ohne execute.
BP-18: Lifecycle-Management (Stop, Pause, Status) lebt auf den
Handles die execute zurückgibt, nicht auf der Engine.
Die Engine startet Dinge, sie managed sie nicht.
BP-19: Der Code Generator (IR → JBC/Native) ist selbst ein
Blueprint mit Engine und lebt in Ring 2. Die Grenze
zwischen Ring 1 und Ring 2 liegt an der Vertrauensgrenze:
Ring 1 beweist, Ring 2 transformiert.
16. Merksatz¶
Funktionen berechnen.
Blueprints beschreiben.
Engines führen aus.
Der Compiler beweist, dass die Beschreibung typkorrekt ist.
Die Engine beweist, dass die Beschreibung operativ Sinn ergibt.
Was kompiliert, ist serialisierbar und übertragbar.
Oder kürzer: