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
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>
304 lines
8.2 KiB
TypeScript
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
|
|
}
|