feat: Template-Spec v1 Phase C — IF-Renderer + HOSTING/FEATURES + 4 neue DE-Templates
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 36s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
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 36s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
- contextBridge.ts: HostingCtx + FeaturesCtx (35 Felder), ~50 neue Platzhalter-Aliases - ruleEngine.ts: buildBoolContext() + applyConditionalBlocks() (IF/IF_NOT/IF_ANY) - ruleEngine.test.ts: 67 Tests (+18 für Phase C), alle grün - page.tsx: IF-Renderer in Pipeline, HOSTING+FEATURES Formular-Sections, erweiterter SDK-Prefill - scripts/apply_templates_023.py: 4 neue DE-Templates (Cookie v2, DSE, AGB, Impressum) - migrations/023_new_templates_de.sql: Dokumentation + Verifikations-Query Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
} from './contextBridge'
|
||||
import {
|
||||
runRuleset, getDocType, applyBlockRemoval,
|
||||
buildBoolContext, applyConditionalBlocks,
|
||||
type RuleInput, type RuleEngineResult,
|
||||
} from './ruleEngine'
|
||||
|
||||
@@ -59,6 +60,8 @@ const SECTION_LABELS: Record<keyof TemplateContext, string> = {
|
||||
SECURITY: 'Sicherheit & Logs',
|
||||
NDA: 'Geheimhaltung (NDA)',
|
||||
CONSENT: 'Cookie / Einwilligung',
|
||||
HOSTING: 'Hosting-Provider',
|
||||
FEATURES: 'Dokument-Features & Textbausteine',
|
||||
}
|
||||
|
||||
type FieldType = 'text' | 'email' | 'number' | 'select' | 'textarea' | 'boolean'
|
||||
@@ -156,6 +159,55 @@ const SECTION_FIELDS: Record<keyof TemplateContext, FieldDef[]> = {
|
||||
{ key: 'ANALYTICS_TOOLS', label: 'Analytics-Tools (leer = kein Block)', nullable: true },
|
||||
{ key: 'MARKETING_PARTNERS', label: 'Marketing-Partner (leer = kein Block)', nullable: true },
|
||||
],
|
||||
HOSTING: [
|
||||
{ key: 'PROVIDER_NAME', label: 'Hosting-Anbieter' },
|
||||
{ key: 'COUNTRY', label: 'Hosting-Land' },
|
||||
{ key: 'CONTRACT_TYPE', label: 'Vertragstyp (z. B. AVV nach Art. 28 DSGVO)' },
|
||||
],
|
||||
FEATURES: [
|
||||
// ── DSI / Cookie ─────────────────────────────────────────────────────────
|
||||
{ key: 'CONSENT_WITHDRAWAL_PATH', label: 'Einwilligungs-Widerrufspfad' },
|
||||
{ key: 'SECURITY_MEASURES_SUMMARY', label: 'Sicherheitsmaßnahmen (kurz)' },
|
||||
{ key: 'DATA_SUBJECT_REQUEST_CHANNEL', label: 'Kanal für Betroffenenanfragen' },
|
||||
{ key: 'HAS_THIRD_COUNTRY', label: 'Drittlandübermittlung möglich', type: 'boolean' },
|
||||
{ key: 'TRANSFER_GUARDS', label: 'Garantien (z. B. SCC)' },
|
||||
// ── Cookie/Consent ───────────────────────────────────────────────────────
|
||||
{ key: 'HAS_FUNCTIONAL_COOKIES', label: 'Funktionale Cookies aktiviert', type: 'boolean' },
|
||||
{ key: 'CMP_NAME', label: 'Consent-Manager-Name (optional)' },
|
||||
{ key: 'CMP_LOGS_CONSENTS', label: 'Consent-Protokollierung aktiv', type: 'boolean' },
|
||||
{ key: 'ANALYTICS_TOOLS_DETAIL', label: 'Analyse-Tools (Detailtext)', type: 'textarea', span: true },
|
||||
{ key: 'MARKETING_TOOLS_DETAIL', label: 'Marketing-Tools (Detailtext)', type: 'textarea', span: true },
|
||||
// ── Service-Features ─────────────────────────────────────────────────────
|
||||
{ key: 'HAS_ACCOUNT', label: 'Nutzerkonten vorhanden', type: 'boolean' },
|
||||
{ key: 'HAS_PAYMENTS', label: 'Zahlungsabwicklung vorhanden', type: 'boolean' },
|
||||
{ key: 'PAYMENT_PROVIDER_DETAIL', label: 'Zahlungsanbieter (Detailtext)', type: 'textarea', span: true },
|
||||
{ key: 'HAS_SUPPORT', label: 'Support-Funktion vorhanden', type: 'boolean' },
|
||||
{ key: 'SUPPORT_CHANNELS_TEXT', label: 'Support-Kanäle / Zeiten' },
|
||||
{ key: 'HAS_NEWSLETTER', label: 'Newsletter vorhanden', type: 'boolean' },
|
||||
{ key: 'NEWSLETTER_PROVIDER_DETAIL', label: 'Newsletter-Anbieter (Detailtext)', type: 'textarea', span: true },
|
||||
{ key: 'HAS_SOCIAL_MEDIA', label: 'Social-Media-Präsenz', type: 'boolean' },
|
||||
{ key: 'SOCIAL_MEDIA_DETAIL', label: 'Social-Media-Details', type: 'textarea', span: true },
|
||||
// ── AGB ──────────────────────────────────────────────────────────────────
|
||||
{ key: 'HAS_PAID_PLANS', label: 'Kostenpflichtige Pläne', type: 'boolean' },
|
||||
{ key: 'PRICES_TEXT', label: 'Preise (Text/Link)', type: 'textarea', span: true },
|
||||
{ key: 'PAYMENT_TERMS_TEXT', label: 'Zahlungsbedingungen', type: 'textarea', span: true },
|
||||
{ key: 'CONTRACT_TERM_TEXT', label: 'Laufzeit & Kündigung', type: 'textarea', span: true },
|
||||
{ key: 'HAS_SLA', label: 'SLA vorhanden', type: 'boolean' },
|
||||
{ key: 'SLA_URL', label: 'SLA-URL' },
|
||||
{ key: 'HAS_EXPORT_POLICY', label: 'Datenexport/Löschung geregelt', type: 'boolean' },
|
||||
{ key: 'EXPORT_POLICY_TEXT', label: 'Datenexport-Regelung (Text)', type: 'textarea', span: true },
|
||||
{ key: 'HAS_WITHDRAWAL', label: 'Widerrufsrecht (B2C digital)', type: 'boolean' },
|
||||
{ key: 'CONSUMER_WITHDRAWAL_TEXT', label: 'Widerrufsbelehrung (Text)', type: 'textarea', span: true },
|
||||
{ key: 'LIMITATION_CAP_TEXT', label: 'Haftungsdeckel B2B (Text)' },
|
||||
// ── Impressum ────────────────────────────────────────────────────────────
|
||||
{ key: 'HAS_REGULATED_PROFESSION', label: 'Reglementierter Beruf', type: 'boolean' },
|
||||
{ key: 'REGULATED_PROFESSION_TEXT', label: 'Berufsrecht-Text', type: 'textarea', span: true },
|
||||
{ key: 'HAS_EDITORIAL_RESPONSIBLE', label: 'V.i.S.d.P. (redaktionell)', type: 'boolean' },
|
||||
{ key: 'EDITORIAL_RESPONSIBLE_NAME', label: 'V.i.S.d.P. Name' },
|
||||
{ key: 'EDITORIAL_RESPONSIBLE_ADDRESS', label: 'V.i.S.d.P. Adresse' },
|
||||
{ key: 'HAS_DISPUTE_RESOLUTION', label: 'Streitbeilegungshinweis', type: 'boolean' },
|
||||
{ key: 'DISPUTE_RESOLUTION_TEXT', label: 'Streitbeilegungstext', type: 'textarea', span: true },
|
||||
],
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -401,16 +453,25 @@ function GeneratorSection({
|
||||
...extraPlaceholders,
|
||||
}), [context, extraPlaceholders, ruleResult])
|
||||
|
||||
// Boolean context for {{#IF}} rendering
|
||||
const boolCtx = useMemo(
|
||||
() => ruleResult ? buildBoolContext(ruleResult.contextAfterDefaults, ruleResult.computedFlags) : {},
|
||||
[ruleResult]
|
||||
)
|
||||
|
||||
const renderedContent = useMemo(() => {
|
||||
// Apply block removal BEFORE placeholder substitution
|
||||
// 1. Remove ruleset-driven blocks ([BLOCK:ID])
|
||||
let content = applyBlockRemoval(template.text, ruleResult?.removedBlocks ?? [])
|
||||
// 2. Evaluate {{#IF}} / {{#IF_NOT}} / {{#IF_ANY}} directives
|
||||
content = applyConditionalBlocks(content, boolCtx)
|
||||
// 3. Substitute placeholders
|
||||
for (const [key, value] of Object.entries(allPlaceholderValues)) {
|
||||
if (value) {
|
||||
content = content.replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value)
|
||||
}
|
||||
}
|
||||
return content
|
||||
}, [template.text, allPlaceholderValues, ruleResult])
|
||||
}, [template.text, allPlaceholderValues, ruleResult, boolCtx])
|
||||
|
||||
// Compute which modules are relevant (mentioned in violations/warnings)
|
||||
const relevantModules = useMemo(() => {
|
||||
@@ -776,22 +837,42 @@ function DocumentGeneratorPageInner() {
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Pre-fill context from company profile
|
||||
// Pre-fill context from company profile + SDK state
|
||||
useEffect(() => {
|
||||
if (state?.companyProfile) {
|
||||
const profile = state.companyProfile
|
||||
const p = profile as Record<string, string>
|
||||
setContext((prev) => ({
|
||||
...prev,
|
||||
PROVIDER: {
|
||||
...prev.PROVIDER,
|
||||
LEGAL_NAME: profile.companyName || prev.PROVIDER.LEGAL_NAME,
|
||||
EMAIL: (profile as Record<string, string>).email || prev.PROVIDER.EMAIL,
|
||||
LEGAL_NAME: profile.companyName || prev.PROVIDER.LEGAL_NAME,
|
||||
LEGAL_FORM: p.legalForm || prev.PROVIDER.LEGAL_FORM,
|
||||
ADDRESS_LINE: p.addressLine || prev.PROVIDER.ADDRESS_LINE,
|
||||
POSTAL_CODE: p.postalCode || prev.PROVIDER.POSTAL_CODE,
|
||||
CITY: p.city || prev.PROVIDER.CITY,
|
||||
COUNTRY: p.country || prev.PROVIDER.COUNTRY,
|
||||
EMAIL: p.email || prev.PROVIDER.EMAIL,
|
||||
PHONE: p.phone || prev.PROVIDER.PHONE,
|
||||
WEBSITE_URL: p.websiteUrl || prev.PROVIDER.WEBSITE_URL,
|
||||
CEO_NAME: p.representedByName || p.ceoName || prev.PROVIDER.CEO_NAME,
|
||||
REGISTER_COURT: p.registerCourt || prev.PROVIDER.REGISTER_COURT,
|
||||
REGISTER_NUMBER: p.registerNumber || prev.PROVIDER.REGISTER_NUMBER,
|
||||
VAT_ID: p.vatId || prev.PROVIDER.VAT_ID,
|
||||
},
|
||||
PRIVACY: {
|
||||
...prev.PRIVACY,
|
||||
DPO_NAME: profile.dpoName || prev.PRIVACY.DPO_NAME,
|
||||
DPO_NAME: profile.dpoName || prev.PRIVACY.DPO_NAME,
|
||||
DPO_EMAIL: profile.dpoEmail || prev.PRIVACY.DPO_EMAIL,
|
||||
CONTACT_EMAIL: profile.dpoEmail || prev.PRIVACY.CONTACT_EMAIL,
|
||||
SUPERVISORY_AUTHORITY_NAME: p.supervisoryAuthorityName || prev.PRIVACY.SUPERVISORY_AUTHORITY_NAME,
|
||||
SUPERVISORY_AUTHORITY_ADDRESS: p.supervisoryAuthorityAddress || prev.PRIVACY.SUPERVISORY_AUTHORITY_ADDRESS,
|
||||
},
|
||||
FEATURES: {
|
||||
...prev.FEATURES,
|
||||
DATA_SUBJECT_REQUEST_CHANNEL: p.email
|
||||
? `per E-Mail an ${p.email}`
|
||||
: prev.FEATURES.DATA_SUBJECT_REQUEST_CHANNEL,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user