# Modul-Guide

<Lead>
Ein Modul ist die Code-Einheit hinter jedem Tool: ein eigenständiger Ordner mit festem Vertrag. Diese Seite führt dich vom Scaffold bis zum validierten, dokumentierten Modul.
</Lead>

<BetaNotice title="Externe Einreichung folgt mit Phase 4">
Heute entstehen Module im Plattform-Monorepo. Die Einreichungs-Pipeline für externe Vendor-Module (dev.reosa.de, Review, Revenue-Share) ist für Phase 4 geplant. Der hier beschriebene Modul-Vertrag ist derselbe, der dann auch für Vendor-Module gilt.
</BetaNotice>

## Ordner-Struktur

Jedes Modul lebt unter `apps/app/src/modules/<id>/`. Das Mindest-Set:

```
apps/app/src/modules/<id>/
├── manifest.ts        # Single Source of Truth (ID, Kosten, Permissions, UI, Lifecycle)
├── compute.ts         # exportiert runTool (ModuleCompute), der einzige Harness-Eintrag
├── ui/
│   └── index.tsx      # Tool-Komponente (Default-Export, ModuleToolProps)
├── lib/               # modul-interne Utilities, nie Cross-Module-Imports
├── lifecycle/
│   ├── on-install.ts  # optionaler Hook (Default-Export, ModuleLifecycleHook)
│   └── on-uninstall.ts
├── docs/
│   ├── index.de.mdx   # Tool-Doku, gespiegelt in die Docs-Site
│   └── index.en.mdx
├── tests/             # alle Modul-Tests leben IM Modul
├── assets/            # icon.svg + Marketplace-Bilder
├── CHANGELOG.md       # user-facing, wird im Marketplace angezeigt
└── README.md          # Entwickler-Notizen zum Modul
```

Ein Modul ist eine Black-Box für andere Module: Cross-Module-Imports sind verboten. Kommunikation läuft über Platform-Events oder geteilte Platform-Entities.

## Scaffold und Registrierung

```bash
bun create:module --name=<id>
```

Das Kommando klont das `_template`-Modul und ersetzt Platzhalter. Danach drei Schritte, die kein Script für dich erledigt:

1. **Registry:** Das Modul in `apps/app/src/modules/registry.ts` eintragen (Manifest + Lazy-Loader für `ui/`, `compute.ts`, `lifecycle/`). Der Validator erzwingt: Ordner und Registry-Eintrag gehören zusammen.
2. **Permissions:** `tools.use.<id>` (und bei Settings-Panel `tools.configure.<id>`) in der SDK-Permission-Registry deklarieren. `bun validate:permissions` gleicht jedes `manifest.permissions[]`-Entry gegen die Registry ab.
3. **Doku:** `docs/index.{de,en}.mdx` füllen und `bun sync:module-docs` laufen lassen (siehe unten).

## Das Manifest

`manifest.ts` ist die einzige Datei im Modul-Root und wird beim Laden gegen das Schema geparst:

```typescript
import { type ModuleManifest, ModuleManifestSchema } from "@reosa/sdk";

const manifest: ModuleManifest = ModuleManifestSchema.parse({
  id: "homestaging",              // kebab-case, muss dem Ordnernamen entsprechen
  version: "0.1.0",               // semver
  displayName: "AI Homestaging",
  category: "visualisierung",     // marketing | visualisierung | texterstellung | ...
  shortDescription: "...",        // max 280 Zeichen, Marketplace-Kachel
  longDescription: "...",         // Marketplace-Detailseite
  minimumPlan: "compact",         // compact | standard | max | max-plus
  creditsPerUse: 8,               // 0 nur bei aiRiskClass "none" oder pricing "addon"
  pricing: "credits",             // "credits" (Default) oder "addon"
  aiRiskClass: "minimal",         // none | minimal | limited | high | prohibited
  status: "coming_soon",          // erst auf "available" schalten, wenn das Tool real ist
  ui: { tool: true },             // + optional settingsPanel, chrome, openWith
  lifecycle: { onInstall: "lifecycle/on-install" },
  permissions: ["tools.use.homestaging"],
  marketplaceListing: { visibility: "public", tags: ["fotos"] },
});

export default manifest;
```

Wichtige Regeln:

- **`status` startet auf `coming_soon`.** So geht ein frisch gescaffoldetes Modul nie versehentlich live; `coming_soon` rendert nur die Marketing-Kachel.
- **`marketplaceListing`** trägt die Marketplace-Darstellung (Sichtbarkeit, Summary, Tagline, Highlights, Use-Cases). Alles optional, die Detailseite rendert nur vorhandene Blöcke.
- **`availableInTrial`** (Default `true`) schaltet Trial-Workspaces aus, wenn dein Tool nach außen zeigende Flächen hat (z.B. öffentliche Buchungslinks), die nach Trial-Ende sterben würden.
- **Lifecycle-Referenzen** (`lifecycle/on-install`) sind Datei-Pfade ohne Endung; der Struktur-Validator prüft, dass die Files existieren.

## Compute: der Runs-Harness-Vertrag

Dein Modul exportiert genau eine Funktion. Der Harness ruft sie auf und besitzt alles drumherum (Credits, Run-Zeile, Notification, Webhook-Events):

```typescript
// compute.ts
import type { ModuleCompute } from "@reosa/sdk";
import { z } from "zod";

const inputSchema = z.object({ style: z.string(), imageKey: z.string() });

export const runTool: ModuleCompute = async (ctx, { payload, propertyId }) => {
  const input = inputSchema.parse(payload);   // Zod zuerst, payload ist unknown
  const result = await generate(ctx, input);  // die eigentliche Arbeit
  return {
    output: { generationId: result.id },      // landet auf tool_runs.result
    summary: "Dein Homestaging ist fertig.",  // eine Zeile für die Notification
    resultHref: `/tools/homestaging?g=${result.id}`,
  };
};
```

- **Werfen genügt.** Wirft `runTool`, erstattet der Harness die Credits, markiert den Lauf als fehlgeschlagen und benachrichtigt die Nutzer:in. Kein eigenes Refund-Handling im Modul.
- **Die UI bekommt den Invoker als Prop.** Die Tool-Komponente erhält `runTool` (ein `ModuleRunInvoker`) über `ModuleToolProps`; Module importieren nie Server-Actions direkt, der Struktur-Validator blockt `@/server/...`-Imports.
- Das Ergebnis für die UI ist ein `ModuleRunOutcome`: `{ ok: true, runId, output, creditsUsed }` oder `{ ok: false, error, errorCode }` mit Codes wie `insufficient_credits`, `forbidden`, `invalid_input`.

## Lifecycle-Hooks

`lifecycle/on-install.ts` und `lifecycle/on-uninstall.ts` exportieren je eine `ModuleLifecycleHook`-Funktion (`(ctx) => Promise<void>`). Die Install-/Uninstall-Actions rufen sie fail-soft auf: ein werfender Hook bricht die Installation nicht ab, verhindert also keine Deinstallation. Typische Aufgaben: Default-Einstellungen anlegen, tool-eigene Daten aufräumen.

## Validierung

Vor jedem PR lokal laufen lassen, CI prüft dasselbe:

```bash
bun validate:module <id>          # alle Checks für ein Modul gebündelt
bun validate:manifests            # jedes Manifest gegen das Schema
bun validate:module-structure     # Ordner-Konventionen + Datei-Referenzen
bun validate:permissions          # manifest.permissions[] gegen die Registry
```

## Doku im Modul

Tool-Doku lebt **im Modul** unter `docs/<page>.<locale>.mdx` (flach, `index.de.mdx` ist die Landing) und wird gespiegelt:

```bash
bun sync:module-docs              # spiegelt in apps/docs/src/content/tools/
bun sync:module-docs --check      # CI-Drift-Guard
```

Der Sync validiert die Frontmatter (Section muss `tools` sein, Pflicht-Keys vorhanden) und markiert die Ziel-Dateien als auto-synced: bearbeitet wird immer im Modul, nie in `apps/docs`. Eine Phase gilt erst als fertig, wenn die Tool-Doku geschrieben, frontmatter-valid und indexiert ist.

## CHANGELOG: user-facing, ohne Interna

Die `CHANGELOG.md` deines Moduls sehen Immobilienmakler:innen im Marketplace. Deshalb:

- **Jeder Eintrag beschreibt, was sich für die Nutzer:in ändert**, nie wie es implementiert ist. Richtig: "DB-Leads ist jetzt ein Add-on-Tool." Falsch: "Manifest pricing addon, Migration 0076 rewritet Altwerte."
- **Verboten im Changelog:** Dateinamen, Funktions- und Komponenten-Namen, Manifest-Felder, Migrations-Nummern, Env-Variablen. Technik gehört in Commit-Messages und Architektur-Docs.
- **Struktur:** Keep-a-Changelog-Kategorien (Added/Changed/Fixed), ein Eintrag pro spürbarer Änderung, Deutsch, kurze Sätze. Rein interne Refactorings bekommen keinen Eintrag.

## Weiterführend

Die SDK-Namespaces, die dein Compute typischerweise nutzt: [Credits](/dev/sdk/credits), [Dateispeicher](/dev/sdk/files), [REOS AI](/dev/sdk/reos-ai), [Notifications](/dev/sdk/notifications), [Audit](/dev/sdk/audit) und [Webhooks und Events](/dev/sdk/webhooks-und-events). Zurück zum [Überblick](/dev/overview).
