Files
Benjamin Boenisch 06711bad1c
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 44s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 20s
feat(sdk,iace): add Personalized Drafting Pipeline v2 and IACE engine
Drafting Engine: 7-module pipeline with narrative tags, allowed facts governance,
PII sanitizer, prose validator with repair loop, hash-based cache, and terminology
guide. v1 fallback via ?v=1 query param.

IACE: Initial AI-Act Conformity Engine with risk classifier, completeness checker,
hazard library, and PostgreSQL store for AI system assessments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 22:27:06 +01:00

304 lines
8.2 KiB
TypeScript

/**
* Cache Manager — Hash-basierte Prose-Block-Cache
*
* Deterministischer Cache fuer LLM-generierte Prosa-Bloecke.
* Kein TTL-basiertes Raten — stattdessen Hash-basierte Invalidierung.
*
* Cache-Key = SHA-256 ueber alle Eingabeparameter.
* Aendert sich ein Eingabewert → neuer Hash → Cache-Miss → Neu-Generierung.
*/
import type { AllowedFacts } from './allowed-facts'
import type { NarrativeTags } from './narrative-tags'
import type { ProseBlockOutput } from './prose-validator'
// ============================================================================
// Types
// ============================================================================
export interface CacheEntry {
block: ProseBlockOutput
createdAt: string
hitCount: number
cacheKey: string
}
export interface CacheKeyParams {
allowedFacts: AllowedFacts
templateVersion: string
terminologyVersion: string
narrativeTags: NarrativeTags
promptHash: string
blockType: string
sectionName: string
}
export interface CacheStats {
totalEntries: number
totalHits: number
totalMisses: number
hitRate: number
oldestEntry: string | null
newestEntry: string | null
}
// ============================================================================
// SHA-256 (Browser-kompatibel via SubtleCrypto)
// ============================================================================
/**
* Berechnet SHA-256 Hash eines Strings.
* Nutzt SubtleCrypto (verfuegbar in Node.js 15+ und allen modernen Browsern).
*/
async function sha256(input: string): Promise<string> {
// In Next.js API Routes laeuft Node.js — nutze crypto
if (typeof globalThis.crypto?.subtle !== 'undefined') {
const encoder = new TextEncoder()
const data = encoder.encode(input)
const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}
// Fallback: Node.js crypto
try {
const { createHash } = await import('crypto')
return createHash('sha256').update(input).digest('hex')
} catch {
// Letzer Fallback: Einfacher Hash (nicht kryptographisch)
return simpleHash(input)
}
}
/**
* Synchrone SHA-256 Berechnung (Node.js only).
*/
function sha256Sync(input: string): string {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const crypto = require('crypto')
return crypto.createHash('sha256').update(input).digest('hex')
} catch {
return simpleHash(input)
}
}
/**
* Einfacher nicht-kryptographischer Hash als Fallback.
*/
function simpleHash(input: string): string {
let hash = 0
for (let i = 0; i < input.length; i++) {
const char = input.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash
}
return Math.abs(hash).toString(16).padStart(16, '0')
}
// ============================================================================
// Cache Key Computation
// ============================================================================
/**
* Berechnet den deterministischen Cache-Key.
* Sortiert Keys um konsistente Serialisierung zu gewaehrleisten.
*/
export async function computeCacheKey(params: CacheKeyParams): Promise<string> {
const payload = JSON.stringify(params, Object.keys(params).sort())
return sha256(payload)
}
/**
* Synchrone Variante fuer Cache-Key (Node.js).
*/
export function computeCacheKeySync(params: CacheKeyParams): string {
const payload = JSON.stringify(params, Object.keys(params).sort())
return sha256Sync(payload)
}
// ============================================================================
// In-Memory Cache
// ============================================================================
/**
* In-Memory Cache fuer Prose-Bloecke.
*
* Sicherheitsmechanismen:
* - Max Eintraege (Speicher-Limit)
* - TTL als zusaetzlicher Sicherheitsmechanismus (24h default)
* - LRU-artige Bereinigung bei Overflow
*/
export class ProseCacheManager {
private cache = new Map<string, CacheEntry>()
private hits = 0
private misses = 0
private readonly maxEntries: number
private readonly ttlMs: number
constructor(options?: { maxEntries?: number; ttlHours?: number }) {
this.maxEntries = options?.maxEntries ?? 500
this.ttlMs = (options?.ttlHours ?? 24) * 60 * 60 * 1000
}
/**
* Sucht einen gecachten Block.
*/
async get(params: CacheKeyParams): Promise<ProseBlockOutput | null> {
const key = await computeCacheKey(params)
return this.getByKey(key)
}
/**
* Sucht synchron (Node.js).
*/
getSync(params: CacheKeyParams): ProseBlockOutput | null {
const key = computeCacheKeySync(params)
return this.getByKey(key)
}
/**
* Speichert einen Block im Cache.
*/
async set(params: CacheKeyParams, block: ProseBlockOutput): Promise<void> {
const key = await computeCacheKey(params)
this.setByKey(key, block)
}
/**
* Speichert synchron (Node.js).
*/
setSync(params: CacheKeyParams, block: ProseBlockOutput): void {
const key = computeCacheKeySync(params)
this.setByKey(key, block)
}
/**
* Gibt Cache-Statistiken zurueck.
*/
getStats(): CacheStats {
const entries = Array.from(this.cache.values())
const total = this.hits + this.misses
return {
totalEntries: this.cache.size,
totalHits: this.hits,
totalMisses: this.misses,
hitRate: total > 0 ? this.hits / total : 0,
oldestEntry: entries.length > 0
? entries.reduce((a, b) => a.createdAt < b.createdAt ? a : b).createdAt
: null,
newestEntry: entries.length > 0
? entries.reduce((a, b) => a.createdAt > b.createdAt ? a : b).createdAt
: null,
}
}
/**
* Loescht alle Eintraege.
*/
clear(): void {
this.cache.clear()
this.hits = 0
this.misses = 0
}
/**
* Loescht abgelaufene Eintraege.
*/
cleanup(): number {
const now = Date.now()
let removed = 0
for (const [key, entry] of this.cache.entries()) {
if (now - new Date(entry.createdAt).getTime() > this.ttlMs) {
this.cache.delete(key)
removed++
}
}
return removed
}
// ========================================================================
// Private
// ========================================================================
private getByKey(key: string): ProseBlockOutput | null {
const entry = this.cache.get(key)
if (!entry) {
this.misses++
return null
}
// TTL pruefen
if (Date.now() - new Date(entry.createdAt).getTime() > this.ttlMs) {
this.cache.delete(key)
this.misses++
return null
}
entry.hitCount++
this.hits++
return entry.block
}
private setByKey(key: string, block: ProseBlockOutput): void {
// Bei Overflow: aeltesten Eintrag entfernen
if (this.cache.size >= this.maxEntries) {
this.evictOldest()
}
this.cache.set(key, {
block,
createdAt: new Date().toISOString(),
hitCount: 0,
cacheKey: key,
})
}
private evictOldest(): void {
let oldestKey: string | null = null
let oldestTime = Infinity
for (const [key, entry] of this.cache.entries()) {
const time = new Date(entry.createdAt).getTime()
if (time < oldestTime) {
oldestTime = time
oldestKey = key
}
}
if (oldestKey) {
this.cache.delete(oldestKey)
}
}
}
// ============================================================================
// Checksum Utils (fuer Data Block Integritaet)
// ============================================================================
/**
* Berechnet Integritaets-Checksum ueber Daten.
*/
export async function computeChecksum(data: unknown): Promise<string> {
const serialized = JSON.stringify(data, Object.keys(data as Record<string, unknown>).sort())
return sha256(serialized)
}
/**
* Synchrone Checksum-Berechnung.
*/
export function computeChecksumSync(data: unknown): string {
const serialized = JSON.stringify(data, Object.keys(data as Record<string, unknown>).sort())
return sha256Sync(serialized)
}
/**
* Verifiziert eine Checksum gegen Daten.
*/
export async function verifyChecksum(data: unknown, expectedChecksum: string): Promise<boolean> {
const actual = await computeChecksum(data)
return actual === expectedChecksum
}