feat(sdk,iace): add Personalized Drafting Pipeline v2 and IACE engine
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
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>
This commit is contained in:
303
admin-compliance/lib/sdk/drafting-engine/cache.ts
Normal file
303
admin-compliance/lib/sdk/drafting-engine/cache.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user