UNPKG

@iflow-mcp/ansvar-systems-german-law-mcp

Version:

German legal research MCP server with statutes, case law, and preparatory works from gesetze-im-internet.de

855 lines (854 loc) • 32.6 kB
import { execFile } from "node:child_process"; import * as path from "node:path"; import { parseGermanCitation } from "../citation/german-citation.js"; import { citationExistsInGermanLawDatabase, getGermanCaseLawDocumentCount, getGermanDocumentByAnyId, getGermanLawDocumentsByCitation, getGermanLawDocumentsByStatuteId, getGermanLawDocumentById, getGermanLawDocumentCount, getGermanPreparatoryWorkCount, resolveGermanLawDatabasePath, searchGermanCaseLawDocuments, searchGermanLawDocuments, searchGermanPreparatoryWorks, } from "../db/german-law-db.js"; import { GERMAN_LEGISLATION } from "../sample-data/de-legislation.js"; import { getDocumentFromMemory, searchDocumentsInMemory, } from "../shell/adapter-kit.js"; const INGESTION_SCRIPTS = { "gesetze-im-internet": path.resolve(process.cwd(), "scripts/ingest_gesetze_im_internet.py"), "rechtsprechung-im-internet": path.resolve(process.cwd(), "scripts/ingest_rechtsprechung_im_internet.py"), "dip-bundestag": path.resolve(process.cwd(), "scripts/ingest_dip_bundestag.py"), }; const INGESTION_SOURCES = [ "gesetze-im-internet", "rechtsprechung-im-internet", "dip-bundestag", ]; export const germanyAdapter = { country: { code: "de", name: "Germany", defaultLanguage: "de", sources: [ "gesetze-im-internet", "rechtsprechung-im-internet", "dip-bundestag", ], }, capabilities: { documents: true, caseLaw: true, preparatoryWorks: true, citations: true, formatting: true, currency: true, legalStance: true, eu: true, ingestion: true, versionTracking: false, }, async searchDocuments(request) { const dbSearch = searchGermanLawDocuments(request.query, request.limit ?? 20, request.document_id); if (dbSearch) { return dbSearch; } return searchDocumentsInMemory(GERMAN_LEGISLATION, request); }, async searchCaseLaw(request) { const dbSearch = searchGermanCaseLawDocuments(request); if (dbSearch) { return dbSearch; } return { documents: [], total: 0 }; }, async getPreparatoryWorks(request) { const dbSearch = searchGermanPreparatoryWorks(request); if (dbSearch) { return dbSearch; } return { documents: [], total: 0 }; }, async getDocument(id) { const dbDocument = getGermanLawDocumentById(id); if (dbDocument !== undefined && dbDocument !== null) { return dbDocument; } return getDocumentFromMemory(GERMAN_LEGISLATION, id); }, async parseCitation(citation) { const parsed = parseGermanCitation(citation); if (!parsed) { return null; } return { original: citation, normalized: parsed.normalized, parsed: parsed.parsed, }; }, async validateCitation(citation) { const parsed = await this.parseCitation?.(citation); if (!parsed) { return { valid: false, reason: "Citation does not match supported German formats, for example '§ 823 Abs. 1 BGB' or 'Art. 1 Abs. 1 GG'.", }; } const existsInDb = citationExistsInGermanLawDatabase(citation); if (existsInDb === false) { return { valid: false, normalized: parsed.normalized, reason: "Citation format is valid, but no matching provision was found in the current German law database.", }; } return { valid: true, normalized: parsed.normalized, }; }, async formatCitation(request) { const style = request.style ?? "default"; const parsed = parseGermanCitation(request.citation); if (!parsed) { return { original: request.citation, formatted: request.citation.trim(), style, valid: false, reason: "Citation does not match supported German formats, for example '§ 823 Abs. 1 BGB' or 'Art. 1 Abs. 1 GG'.", }; } const formatted = formatGermanCitationByStyle(parsed, style); return { original: request.citation, formatted, style, valid: true, }; }, async checkCurrency(request) { const documents = collectCurrencyDocuments(request); const statuteId = request.statuteId?.trim(); const requestedCitation = request.citation?.trim(); if (documents.dbUnavailable && documents.documents.length === 0) { return { status: "unknown", ...(statuteId ? { statuteId } : {}), ...(requestedCitation ? { citation: requestedCitation } : {}), ...(request.asOfDate ? { asOfDate: request.asOfDate } : {}), reason: "Current corpus database is unavailable; currency cannot be verified deterministically.", }; } if (documents.documents.length === 0) { return { status: "not_found", ...(statuteId ? { statuteId } : {}), ...(requestedCitation ? { citation: requestedCitation } : {}), ...(request.asOfDate ? { asOfDate: request.asOfDate } : {}), reason: "No matching statute/provision found in the ingested corpus.", }; } const sourceDate = newestDate(documents.documents.map((doc) => doc.effectiveDate)); const asOfDate = normalizeIsoDate(request.asOfDate); if (asOfDate && sourceDate && asOfDate < sourceDate) { return { status: "unknown", ...(statuteId ? { statuteId } : {}), ...(requestedCitation ? { citation: requestedCitation } : {}), asOfDate, sourceDate, reason: "The corpus stores consolidated current text; historical in-force status before the known effective date is not guaranteed.", evidence: { matches: documents.documents.length, }, }; } return { status: "likely_in_force", ...(statuteId ? { statuteId } : {}), ...(requestedCitation ? { citation: requestedCitation } : {}), ...(asOfDate ? { asOfDate } : {}), ...(sourceDate ? { sourceDate } : {}), evidence: { matches: documents.documents.length, sampleDocumentId: documents.documents[0]?.id ?? null, }, }; }, async buildLegalStance(request) { const limit = clampLimit(request.limit ?? 10, 1, 100); const statutesResult = await this.searchDocuments?.({ query: request.query, limit, ...(request.document_id ? { document_id: request.document_id } : {}), }); const includeCaseLaw = request.includeCaseLaw ?? true; const includePreparatoryWorks = request.includePreparatoryWorks ?? true; const caseLawResult = includeCaseLaw ? await this.searchCaseLaw?.({ query: request.query, limit }) : undefined; const preparatoryResult = includePreparatoryWorks ? await this.getPreparatoryWorks?.({ query: request.query, limit }) : undefined; const statutes = statutesResult?.documents ?? []; const caseLaw = caseLawResult?.documents ?? []; const preparatoryWorks = preparatoryResult?.documents ?? []; const keyCitations = dedupeStrings([...statutes, ...caseLaw, ...preparatoryWorks] .map((document) => document.citation ?? "") .filter(Boolean)).slice(0, limit * 2); return { query: request.query, statutes, caseLaw, preparatoryWorks, keyCitations, }; }, async getEuBasis(request) { const limit = clampLimit(request.limit ?? 40, 1, 200); const { documents } = collectEuBasisDocuments(request, limit * 8); const references = extractEuReferencesFromDocuments(documents, limit * 8); return { references: references.slice(0, limit), total: references.length, }; }, async searchEuImplementations(request) { const limit = clampLimit(request.limit ?? 20, 1, 200); const documents = await collectDocumentsForEuSearch(this, request.query, Math.max(limit * 8, 120)); const references = extractEuReferencesFromDocuments(documents, limit * 24); const summaries = summarizeEuImplementations(references); return { results: summaries.slice(0, limit), total: summaries.length, }; }, async getNationalImplementations(request) { const limit = clampLimit(request.limit ?? 20, 1, 200); const documents = await collectDocumentsForEuSearch(this, request.euId, Math.max(limit * 10, 150)); const normalizedTarget = normalizeEuIdentifier(request.euId); const filteredReferences = extractEuReferencesFromDocuments(documents, limit * 24).filter((reference) => euIdentifiersMatch(reference.euId, normalizedTarget)); const summaries = summarizeEuImplementations(filteredReferences); return { results: summaries.slice(0, limit), total: summaries.length, }; }, async getProvisionEuBasis(request) { return this.getEuBasis({ documentId: request.documentId, ...(request.limit === undefined ? {} : { limit: request.limit }), }); }, async validateEuCompliance(request) { const normalizedTarget = normalizeEuIdentifier(request.euId); const dbAvailable = getGermanLawDocumentCount() !== null; if (request.citation || request.statuteId) { const basis = await this.getEuBasis({ ...(request.citation ? { citation: request.citation } : {}), ...(request.statuteId ? { statuteId: request.statuteId } : {}), limit: 200, }); const matches = basis.references.filter((reference) => euIdentifiersMatch(reference.euId, normalizedTarget)); const relatedStatutes = dedupeStrings(matches.map((reference) => reference.sourceStatuteId ?? "")); if (matches.length > 0) { return { euId: request.euId, status: "mapped", matches: matches.length, relatedStatutes, }; } return { euId: request.euId, status: dbAvailable ? "not_mapped" : "unknown", matches: 0, relatedStatutes: [], reason: dbAvailable ? "No linkage between requested EU act and selected national provision was found." : "Current corpus database is unavailable; compliance mapping is uncertain.", }; } const implementations = await this.getNationalImplementations({ euId: request.euId, limit: 200, }); const matchCount = implementations.results.reduce((total, item) => total + item.implementationCount, 0); const relatedStatutes = dedupeStrings(implementations.results.flatMap((item) => item.statutes)); if (matchCount > 0) { return { euId: request.euId, status: "mapped", matches: matchCount, relatedStatutes, }; } return { euId: request.euId, status: dbAvailable ? "not_mapped" : "unknown", matches: 0, relatedStatutes: [], reason: dbAvailable ? "No mapped national implementations were found for the requested EU act." : "Current corpus database is unavailable; compliance mapping is uncertain.", }; }, async runIngestion(request) { const requestedSource = normalizeSourceId(request.sourceId); const sources = requestedSource === "all" ? INGESTION_SOURCES : [requestedSource]; const startedAt = new Date().toISOString(); let totalIngested = 0; let totalSkipped = 0; let hadSuccessfulRun = false; for (const source of sources) { const args = buildIngestionArgs(source, request.dryRun ?? false); const report = await executeIngestionScript(args).catch(() => null); if (!report) { if (request.dryRun) { totalSkipped += getCurrentSourceCount(source); } continue; } hadSuccessfulRun = true; if (request.dryRun) { const selectedCount = extractDryRunSelectedCount(report); totalSkipped += selectedCount > 0 ? selectedCount : getCurrentSourceCount(source); } else { totalIngested += extractIngestedCount(report); totalSkipped += extractSkippedCount(report); } } if (!hadSuccessfulRun && !request.dryRun) { const currentCount = getGermanLawDocumentCount() ?? 0; totalSkipped += currentCount; } return { startedAt, finishedAt: new Date().toISOString(), sourceId: request.sourceId ?? "gesetze-im-internet", dryRun: request.dryRun ?? false, ingestedCount: totalIngested, skippedCount: totalSkipped, }; }, }; const EU_TYPED_PREFIX_PATTERN = /\b(?<type>Richtlinie|Directive|Verordnung|Regulation)\s*(?:\((?<jur1>EU|EG|EWG)\)|(?<jur2>EU|EG|EWG))\s*(?:Nr\.?\s*)?(?<num>\d{2,4}\/\d{2,4})\b/giu; const EU_TYPED_SUFFIX_PATTERN = /\b(?<type>Richtlinie|Directive|Verordnung|Regulation)\s*(?<num>\d{2,4}\/\d{2,4})\/(?<jur>EU|EG|EWG)\b/giu; const EU_GENERIC_PREFIX_PATTERN = /\b(?<jur>EU|EG|EWG)\s*(?:Nr\.?\s*)?(?<num>\d{2,4}\/\d{2,4})\b/giu; const EU_GENERIC_SUFFIX_PATTERN = /\b(?<num>\d{2,4}\/\d{2,4})\/(?<jur>EU|EG|EWG)\b/giu; const CELEX_PATTERN = /\b(?:CELEX[:\s]*)?(?<celex>3\d{4}[A-Z]\d{4})\b/giu; function collectCurrencyDocuments(request) { const documents = []; let dbUnavailable = false; const statuteId = request.statuteId?.trim(); if (statuteId) { const byStatute = getGermanLawDocumentsByStatuteId(statuteId, 120); if (byStatute === null) { dbUnavailable = true; } else { documents.push(...byStatute.documents); } } const citation = request.citation?.trim(); if (citation) { const byCitation = getGermanLawDocumentsByCitation(citation, 80); if (byCitation === null) { dbUnavailable = true; const fallback = searchDocumentsInMemory(GERMAN_LEGISLATION, { query: citation, limit: 20, }); documents.push(...fallback.documents); } else { documents.push(...byCitation.documents); } } return { documents: uniqueDocuments(documents), dbUnavailable, }; } function collectEuBasisDocuments(request, maxDocuments) { const documents = []; let dbUnavailable = false; const documentId = request.documentId?.trim(); if (documentId) { const byId = getGermanDocumentByAnyId(documentId); if (byId === undefined) { dbUnavailable = true; } else if (byId) { documents.push(byId); } const memoryDocument = getDocumentFromMemory(GERMAN_LEGISLATION, documentId); if (memoryDocument) { documents.push(memoryDocument); } } const statuteId = request.statuteId?.trim(); if (statuteId) { const byStatute = getGermanLawDocumentsByStatuteId(statuteId, maxDocuments); if (byStatute === null) { dbUnavailable = true; } else { documents.push(...byStatute.documents); } } const citation = request.citation?.trim(); if (citation) { const byCitation = getGermanLawDocumentsByCitation(citation, maxDocuments); if (byCitation === null) { dbUnavailable = true; const fallback = searchDocumentsInMemory(GERMAN_LEGISLATION, { query: citation, limit: Math.min(maxDocuments, 20), }); documents.push(...fallback.documents); } else { documents.push(...byCitation.documents); } } return { documents: uniqueDocuments(documents).slice(0, maxDocuments), dbUnavailable, }; } async function collectDocumentsForEuSearch(adapter, query, maxDocuments) { const statuteLimit = clampLimit(maxDocuments, 1, 200); const secondaryLimit = clampLimit(Math.ceil(maxDocuments / 2), 1, 200); const statutes = ((await adapter.searchDocuments?.({ query, limit: statuteLimit })) ?? { documents: [], }).documents; const caseLaw = ((await adapter.searchCaseLaw?.({ query, limit: secondaryLimit })) ?? { documents: [], }).documents; const preparatoryWorks = ((await adapter.getPreparatoryWorks?.({ query, limit: secondaryLimit })) ?? { documents: [], }).documents; return uniqueDocuments([...statutes, ...caseLaw, ...preparatoryWorks]).slice(0, maxDocuments); } function extractEuReferencesFromDocuments(documents, maxReferences) { const references = []; const seen = new Set(); const clampedLimit = clampLimit(maxReferences, 1, 5000); for (const document of documents) { const extracted = extractEuActsFromText(buildDocumentEuSearchText(document)); const sourceStatuteId = resolveSourceStatuteId(document); for (const euAct of extracted) { const key = `${normalizeEuIdentifier(euAct.euId)}|${document.id}`; if (seen.has(key)) { continue; } seen.add(key); references.push({ euId: euAct.euId, euType: euAct.euType, sourceKind: document.kind, sourceId: document.id, ...(sourceStatuteId ? { sourceStatuteId } : {}), ...(document.citation ? { sourceCitation: document.citation } : {}), ...(document.title ? { sourceTitle: document.title } : {}), ...(document.sourceUrl ? { sourceUrl: document.sourceUrl } : {}), ...(euAct.contextSnippet ? { contextSnippet: euAct.contextSnippet } : {}), confidence: euAct.confidence, }); if (references.length >= clampedLimit) { return references; } } } return references; } function summarizeEuImplementations(references) { const grouped = new Map(); for (const reference of references) { const normalizedId = normalizeEuIdentifier(reference.euId); const key = `${normalizedId}|${reference.euType.toLowerCase()}`; const current = grouped.get(key) ?? { euId: normalizedId, euType: reference.euType, sources: new Set(), statutes: new Set(), }; current.sources.add(reference.sourceId); if (reference.sourceStatuteId) { current.statutes.add(reference.sourceStatuteId); } grouped.set(key, current); } return [...grouped.values()] .map((entry) => ({ euId: entry.euId, euType: entry.euType, implementationCount: entry.sources.size, statutes: [...entry.statutes].sort((a, b) => a.localeCompare(b)), })) .sort((left, right) => { if (right.implementationCount !== left.implementationCount) { return right.implementationCount - left.implementationCount; } return left.euId.localeCompare(right.euId); }); } function extractEuActsFromText(content) { const trimmed = collapseWhitespace(content); if (!trimmed) { return []; } const found = new Map(); const addAct = (euId, euType, confidence, index, rawMatch) => { const normalizedId = normalizeEuIdentifier(euId); const key = `${normalizedId}|${euType.toLowerCase()}`; const current = found.get(key); if (current && current.confidence >= confidence) { return; } found.set(key, { euId: normalizedId, euType, confidence, contextSnippet: buildContextSnippet(trimmed, index, rawMatch.length), }); }; for (const match of trimmed.matchAll(CELEX_PATTERN)) { const celex = match.groups?.celex; if (!celex) { continue; } const parsed = parseCelex(celex); if (!parsed) { continue; } addAct(parsed.euId, parsed.euType, 0.99, match.index ?? 0, match[0]); } for (const match of trimmed.matchAll(EU_TYPED_PREFIX_PATTERN)) { const type = match.groups?.type; const num = match.groups?.num; const jurisdiction = match.groups?.jur1 ?? match.groups?.jur2; if (!type || !num || !jurisdiction) { continue; } addAct(normalizeEuIdParts(jurisdiction, num), normalizeEuType(type), 0.95, match.index ?? 0, match[0]); } for (const match of trimmed.matchAll(EU_TYPED_SUFFIX_PATTERN)) { const type = match.groups?.type; const num = match.groups?.num; const jurisdiction = match.groups?.jur; if (!type || !num || !jurisdiction) { continue; } addAct(normalizeEuIdParts(jurisdiction, num), normalizeEuType(type), 0.94, match.index ?? 0, match[0]); } for (const match of trimmed.matchAll(EU_GENERIC_PREFIX_PATTERN)) { const jurisdiction = match.groups?.jur; const num = match.groups?.num; if (!jurisdiction || !num) { continue; } addAct(normalizeEuIdParts(jurisdiction, num), "act", 0.9, match.index ?? 0, match[0]); } for (const match of trimmed.matchAll(EU_GENERIC_SUFFIX_PATTERN)) { const jurisdiction = match.groups?.jur; const num = match.groups?.num; if (!jurisdiction || !num) { continue; } addAct(normalizeEuIdParts(jurisdiction, num), "act", 0.89, match.index ?? 0, match[0]); } return [...found.values()].sort((left, right) => { if (right.confidence !== left.confidence) { return right.confidence - left.confidence; } return left.euId.localeCompare(right.euId); }); } function buildDocumentEuSearchText(document) { const metadataValues = document.metadata ? Object.values(document.metadata) .filter((value) => value !== null) .map((value) => String(value)) : []; return [ document.title, document.citation ?? "", document.textSnippet ?? "", metadataValues.join(" "), ] .filter(Boolean) .join(" "); } function resolveSourceStatuteId(document) { const metadataStatuteId = metadataStringValue(document, "statute_id"); if (metadataStatuteId) { return metadataStatuteId; } if (document.kind === "statute" || document.kind === "regulation") { const separator = document.id.indexOf(":"); if (separator > 0) { return document.id.slice(0, separator); } } return undefined; } function metadataStringValue(document, key) { const value = document.metadata?.[key]; if (typeof value === "string" && value.trim()) { return value.trim(); } return undefined; } function normalizeEuType(value) { const normalized = value.trim().toLowerCase(); if (normalized === "richtlinie" || normalized === "directive") { return "directive"; } if (normalized === "verordnung" || normalized === "regulation") { return "regulation"; } return "act"; } function normalizeEuIdParts(jurisdiction, numberExpression) { const [yearRaw, numberRaw] = numberExpression.split("/"); const year = yearRaw?.trim() ?? ""; const numberToken = numberRaw?.trim() ?? ""; const numericPart = Number.parseInt(numberToken, 10); const number = Number.isFinite(numericPart) && !Number.isNaN(numericPart) ? String(numericPart) : numberToken; return `${jurisdiction.trim().toUpperCase()} ${year}/${number}`.trim(); } function normalizeEuIdentifier(input) { const trimmed = collapseWhitespace(input).toUpperCase(); if (!trimmed) { return ""; } const celexInput = trimmed.replace(/^CELEX[:\s]*/i, ""); const celex = parseCelex(celexInput); if (celex) { return celex.euId; } const prefixMatch = trimmed.match(/^(EU|EG|EWG)\s*(?:NR\.?\s*)?(\d{2,4})\/(\d{2,4})$/); if (prefixMatch) { return normalizeEuIdParts(prefixMatch[1] ?? "EU", `${prefixMatch[2] ?? ""}/${prefixMatch[3] ?? ""}`); } const suffixMatch = trimmed.match(/^(\d{2,4})\/(\d{2,4})\/(EU|EG|EWG)$/); if (suffixMatch) { return normalizeEuIdParts(suffixMatch[3] ?? "EU", `${suffixMatch[1] ?? ""}/${suffixMatch[2] ?? ""}`); } return trimmed; } function euIdentifiersMatch(candidate, targetNormalized) { const candidateNormalized = normalizeEuIdentifier(candidate); if (candidateNormalized === targetNormalized) { return true; } const candidateWithoutJurisdiction = candidateNormalized.replace(/^(EU|EG|EWG)\s+/, ""); const targetWithoutJurisdiction = targetNormalized.replace(/^(EU|EG|EWG)\s+/, ""); return candidateWithoutJurisdiction === targetWithoutJurisdiction; } function parseCelex(input) { const normalized = input.trim().toUpperCase(); if (!/^3\d{4}[A-Z]\d{4}$/.test(normalized)) { return null; } const year = normalized.slice(1, 5); const typeMarker = normalized.slice(5, 6); const rawNumber = normalized.slice(6); const parsedNumber = Number.parseInt(rawNumber, 10); if (!Number.isFinite(parsedNumber) || Number.isNaN(parsedNumber)) { return null; } const euType = typeMarker === "R" ? "regulation" : typeMarker === "L" ? "directive" : typeMarker === "D" ? "decision" : "act"; return { euId: `EU ${year}/${parsedNumber}`, euType, }; } function buildContextSnippet(content, index, length) { const start = Math.max(0, index - 90); const end = Math.min(content.length, index + length + 90); return collapseWhitespace(content.slice(start, end)); } function formatGermanCitationByStyle(parsed, style) { if (style === "default" || style === "pinpoint") { return parsed.normalized; } if (parsed.type === "paragraph") { const section = parsed.parsed.section ?? ""; const code = parsed.parsed.code ?? ""; return collapseWhitespace(`§ ${section} ${code}`); } const article = parsed.parsed.article ?? ""; const code = parsed.parsed.code ?? ""; return collapseWhitespace(`Art. ${article} ${code}`); } function uniqueDocuments(documents) { const seen = new Set(); const deduped = []; for (const document of documents) { if (seen.has(document.id)) { continue; } seen.add(document.id); deduped.push(document); } return deduped; } function newestDate(values) { const validDates = values .map((value) => normalizeIsoDate(value)) .filter((value) => value !== undefined) .sort((left, right) => left.localeCompare(right)); return validDates.at(-1); } function normalizeIsoDate(value) { if (!value) { return undefined; } const trimmed = value.trim(); if (!trimmed) { return undefined; } if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) { return undefined; } return trimmed; } function dedupeStrings(values) { return [...new Set(values.map((value) => value.trim()).filter(Boolean))]; } function collapseWhitespace(value) { return value.trim().replace(/\s+/g, " "); } function clampLimit(value, min, max) { if (!Number.isFinite(value) || Number.isNaN(value)) { return min; } if (value < min) { return min; } if (value > max) { return max; } return Math.trunc(value); } function buildIngestionArgs(sourceId, dryRun) { const args = [ INGESTION_SCRIPTS[sourceId], "--db-path", resolveGermanLawDatabasePath(), ]; if (dryRun) { args.push("--dry-run"); } else { args.push("--only-missing", "--quiet"); } if (sourceId === "gesetze-im-internet") { const maxLaws = process.env.GERMAN_LAW_INGEST_MAX_LAWS?.trim(); if (maxLaws) { args.push("--max-laws", maxLaws); } } if (sourceId === "rechtsprechung-im-internet") { const maxCases = process.env.GERMAN_LAW_INGEST_MAX_CASES?.trim(); if (maxCases) { args.push("--max-cases", maxCases); } const stopAfterExisting = process.env.GERMAN_LAW_INGEST_CASES_STOP_AFTER_EXISTING?.trim(); if (stopAfterExisting) { args.push("--stop-after-existing", stopAfterExisting); } } if (sourceId === "dip-bundestag") { const maxDocuments = process.env.GERMAN_LAW_INGEST_MAX_PREP_WORKS?.trim(); if (maxDocuments) { args.push("--max-documents", maxDocuments); } const stopAfterExisting = process.env.GERMAN_LAW_INGEST_PREP_STOP_AFTER_EXISTING?.trim(); if (stopAfterExisting) { args.push("--stop-after-existing", stopAfterExisting); } const apiKey = process.env.GERMAN_LAW_DIP_API_KEY?.trim(); if (apiKey) { args.push("--api-key", apiKey); } const wahlperioden = process.env.GERMAN_LAW_PREP_WAHLPERIODEN ?.split(",") .map((value) => value.trim()) .filter(Boolean); for (const period of wahlperioden ?? []) { args.push("--wahlperiode", period); } } return args; } function normalizeSourceId(sourceId) { if (!sourceId) { return "gesetze-im-internet"; } const normalized = sourceId.trim().toLowerCase(); if (normalized === "all") { return "all"; } if (normalized === "gesetze-im-internet" || normalized === "rechtsprechung-im-internet" || normalized === "dip-bundestag") { return normalized; } return "gesetze-im-internet"; } function extractIngestedCount(report) { return Number(report.ingested_count ?? report.ingested_sections ?? report.ingested_cases ?? report.ingested_documents ?? 0); } function getCurrentSourceCount(sourceId) { if (sourceId === "gesetze-im-internet") { return getGermanLawDocumentCount() ?? 0; } if (sourceId === "rechtsprechung-im-internet") { return getGermanCaseLawDocumentCount() ?? 0; } return getGermanPreparatoryWorkCount() ?? 0; } function extractDryRunSelectedCount(report) { return Number(report.selected_laws ?? report.selected_cases ?? report.selected_documents ?? 0); } function extractSkippedCount(report) { return Number(report.skipped_count ?? report.skipped_sections ?? report.skipped_cases ?? report.skipped_documents ?? 0); } async function executeIngestionScript(args) { const output = await new Promise((resolve, reject) => { execFile("python3", args, { maxBuffer: 32 * 1024 * 1024 }, (error, stdout, stderr) => { if (error) { reject(new Error(`German ingestion script failed: ${stderr || stdout || error.message}`)); return; } resolve({ stdout, stderr }); }); }); const lines = output.stdout .trim() .split("\n") .map((line) => line.trim()) .filter(Boolean); const lastLine = lines.at(-1); if (!lastLine) { return {}; } try { return JSON.parse(lastLine); } catch { return {}; } }