Architektur: Craftvia-Domänenmodell, Verträge und Team-Schnitte

- Migration 0002_craftvia_domain: 27 Fachtabellen inkl. RLS (enable_tenant_rls)
- TENANT_MODELS (db.ts, backup/topology.ts) um alle Fachmodelle ergänzt
- moduleGuard liefert DB-autoritative Rechte; ServiceCtx für Domänen-Services
- Verträge: Statusmaschine, Events, Nummernkreise, Sichtbarkeits-Scopes,
  Job-Queues + Worker, KI-Provider-Interfaces, Sync-Envelope
- docs/craftvia/ARCHITEKTUR.md mit Lanes, Ownership und DoD

Gate: tsc, lint, build, 22/22 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 11:49:21 +02:00
co-authored by Claude Opus 5
parent 1701db0a62
commit bf4456718e
21 changed files with 2670 additions and 12 deletions
+85
View File
@@ -0,0 +1,85 @@
// Provider abstraction for AI/OCR (spec §31). Concrete implementations:
// src/server/ai/extraction/anthropic.ts (imports lane)
// src/server/ai/transcription/openai-compatible.ts (lotse lane)
// src/server/ai/lotse/anthropic.ts (lotse lane)
// Every call must be recorded as AiGeneration. Without configuration the getters return null
// and callers degrade gracefully (manual entry).
export type ExtractedField<T = string> = {
value: T | null;
/** 0..1 */
confidence: number;
/** optional source snippet for the review mask */
source?: string;
};
export type WorkOrderExtraction = {
orderNumber: ExtractedField;
offerNumber: ExtractedField;
customerNumber: ExtractedField;
companyName: ExtractedField;
customerFirstName: ExtractedField;
customerLastName: ExtractedField;
customerAddress: ExtractedField<{ street?: string; houseNumber?: string; postalCode?: string; city?: string; country?: string }>;
siteName: ExtractedField;
siteAddress: ExtractedField<{ street?: string; houseNumber?: string; postalCode?: string; city?: string; country?: string }>;
contactName: ExtractedField;
phone: ExtractedField;
email: ExtractedField;
orderDate: ExtractedField; // ISO date
documentDate: ExtractedField; // ISO date
plannedStart: ExtractedField; // ISO date
plannedEnd: ExtractedField; // ISO date
title: ExtractedField;
description: ExtractedField;
positions: ExtractedField<Array<{ position?: string; name: string; articleNumber?: string; quantity?: number; unit?: string; isMaterial?: boolean }>>;
notes: ExtractedField;
totalAmount: ExtractedField<number>;
references: ExtractedField<string[]>;
};
export type ProviderMeta = { provider: string; model: string; inputTokens?: number; outputTokens?: number };
export interface DocumentExtractionProvider {
readonly name: string;
readonly model: string;
extract(input: { bytes: Buffer; mimeType: string; fileName: string }): Promise<{
text: string;
extraction: WorkOrderExtraction;
meta: ProviderMeta;
}>;
}
export interface TranscriptionProvider {
readonly name: string;
readonly model: string;
transcribe(input: { bytes: Buffer; mimeType: string; language: "de" | "en" }): Promise<{ text: string; meta: ProviderMeta }>;
}
export type ReportDraftInput = {
locale: "de" | "en";
addressForm: "sie" | "du";
workOrder: { title: string; description?: string | null; scope?: string | null; orderType?: string | null };
notes: Array<{ kind: string; text: string; at: string }>;
checklist: Array<{ label: string; checked: boolean; comment?: string | null }>;
materials: Array<{ name: string; planned?: string; actual: string; unit: string; status: string; reason?: string | null }>;
photos: Array<{ phase?: string | null; comment?: string | null; requirement?: string | null }>;
time: Array<{ type: string; minutes: number; user: string }>;
};
export type ReportDraftOutput = {
workPerformed: string;
deviations: string;
additionalWork: string;
openItems: string;
nextSteps: string;
hints: string;
missingInformation: string[];
meta: ProviderMeta;
};
export interface LotseProvider {
readonly name: string;
readonly model: string;
draftReport(input: ReportDraftInput): Promise<ReportDraftOutput>;
}