UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

1,238 lines (1,091 loc) • 36.6 kB
/** * Legal Contracts Plugin - MCP Tools * * Implements 5 MCP tools for legal contract analysis: * 1. legal/clause-extract - Extract and classify clauses * 2. legal/risk-assess - Identify and score contractual risks * 3. legal/contract-compare - Compare contracts with attention-based alignment * 4. legal/obligation-track - Extract obligations with DAG analysis * 5. legal/playbook-match - Match clauses against negotiation playbook * * Based on ADR-034: Legal Contract Analysis Plugin * * @module v3/plugins/legal-contracts/mcp-tools */ import { z } from 'zod'; import type { ClauseExtractionResult, RiskAssessmentResult, ContractComparisonResult, ObligationTrackingResult, PlaybookMatchResult, ExtractedClause, RiskFinding, Obligation, PlaybookMatch, DocumentMetadata, IAttentionBridge, IDAGBridge, } from './types.js'; import { ClauseExtractInputSchema, RiskAssessInputSchema, ContractCompareInputSchema, ObligationTrackInputSchema, PlaybookMatchInputSchema, ClauseType, RiskCategory, RiskSeverity, } from './types.js'; import { createAttentionBridge } from './bridges/attention-bridge.js'; import { createDAGBridge } from './bridges/dag-bridge.js'; // ============================================================================ // MCP Tool Types // ============================================================================ /** * MCP Tool definition */ export interface MCPTool<TInput = unknown, TOutput = unknown> { name: string; description: string; category: string; version: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any inputSchema: z.ZodType<TInput, z.ZodTypeDef, any>; handler: (input: TInput, context: ToolContext) => Promise<MCPToolResult<TOutput>>; } /** * Tool execution context */ export interface ToolContext { get<T>(key: string): T | undefined; set<T>(key: string, value: T): void; bridges: { attention: IAttentionBridge; dag: IDAGBridge; }; } /** * MCP Tool result format */ export interface MCPToolResult<T = unknown> { content: Array<{ type: 'text'; text: string }>; data?: T; } // ============================================================================ // Clause Extract Tool // ============================================================================ /** * MCP Tool: legal/clause-extract * * Extract and classify clauses from legal documents */ export const clauseExtractTool: MCPTool< z.infer<typeof ClauseExtractInputSchema>, ClauseExtractionResult > = { name: 'legal/clause-extract', description: 'Extract and classify clauses from legal documents', category: 'legal', version: '3.0.0-alpha.1', inputSchema: ClauseExtractInputSchema, handler: async (input, context) => { const startTime = Date.now(); try { // Validate input const validated = ClauseExtractInputSchema.parse(input); // Parse document and extract clauses const metadata = parseDocumentMetadata(validated.document); const clauses = await extractClauses( validated.document, validated.clauseTypes, validated.jurisdiction, context ); // Separate classified and unclassified const classifiedClauses = clauses.filter(c => c.confidence >= 0.7); const unclassified = clauses .filter(c => c.confidence < 0.7) .map(c => ({ text: c.text, startOffset: c.startOffset, endOffset: c.endOffset, reason: `Low confidence: ${(c.confidence * 100).toFixed(1)}%`, })); const result: ClauseExtractionResult = { success: true, clauses: classifiedClauses, metadata, unclassified, durationMs: Date.now() - startTime, }; return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], data: result, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: errorMessage, durationMs: Date.now() - startTime, }, null, 2), }], }; } }, }; // ============================================================================ // Risk Assess Tool // ============================================================================ /** * MCP Tool: legal/risk-assess * * Assess contractual risks with severity scoring */ export const riskAssessTool: MCPTool< z.infer<typeof RiskAssessInputSchema>, RiskAssessmentResult > = { name: 'legal/risk-assess', description: 'Assess contractual risks with severity scoring', category: 'legal', version: '3.0.0-alpha.1', inputSchema: RiskAssessInputSchema, handler: async (input, context) => { const startTime = Date.now(); try { const validated = RiskAssessInputSchema.parse(input); // Extract clauses first const clauses = await extractClauses(validated.document, undefined, 'US', context); // Assess risks const risks = await assessRisks( clauses, validated.partyRole, validated.riskCategories, validated.industryContext ); // Filter by threshold if specified const filteredRisks = validated.threshold ? risks.filter(r => getSeverityLevel(r.severity) >= getSeverityLevel(validated.threshold!)) : risks; // Build category summary const categorySummary = buildCategorySummary(filteredRisks); // Calculate overall score const overallScore = calculateOverallRiskScore(filteredRisks); const grade = scoreToGrade(overallScore); const result: RiskAssessmentResult = { success: true, partyRole: validated.partyRole, risks: filteredRisks, categorySummary, overallScore, grade, criticalRisks: filteredRisks .filter(r => r.severity === 'critical' || r.severity === 'high') .slice(0, 5), durationMs: Date.now() - startTime, }; return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], data: result, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: errorMessage, durationMs: Date.now() - startTime, }, null, 2), }], }; } }, }; // ============================================================================ // Contract Compare Tool // ============================================================================ /** * MCP Tool: legal/contract-compare * * Compare two contracts with detailed diff and semantic alignment */ export const contractCompareTool: MCPTool< z.infer<typeof ContractCompareInputSchema>, ContractComparisonResult > = { name: 'legal/contract-compare', description: 'Compare two contracts with detailed diff and semantic alignment', category: 'legal', version: '3.0.0-alpha.1', inputSchema: ContractCompareInputSchema, handler: async (input, context) => { const startTime = Date.now(); try { const validated = ContractCompareInputSchema.parse(input); // Extract clauses from both documents const baseClauses = await extractClauses(validated.baseDocument, undefined, 'US', context); const compareClauses = await extractClauses(validated.compareDocument, undefined, 'US', context); // Initialize attention bridge const attention = context.bridges.attention; if (!attention.isInitialized()) { await attention.initialize(); } // Align clauses using attention const alignments = await attention.alignClauses(baseClauses, compareClauses); // Detect changes const changes = detectChanges(baseClauses, compareClauses, alignments); // Calculate similarity score const similarityScore = alignments.length > 0 ? alignments.reduce((sum, a) => sum + a.similarity, 0) / alignments.length : 0; // Build summary const summary = { totalChanges: changes.length, added: changes.filter(c => c.type === 'added').length, removed: changes.filter(c => c.type === 'removed').length, modified: changes.filter(c => c.type === 'modified').length, favorable: changes.filter(c => c.impact === 'favorable').length, unfavorable: changes.filter(c => c.impact === 'unfavorable').length, }; // Generate redline if requested const redlineMarkup = validated.generateRedline ? generateRedlineMarkup(validated.baseDocument, changes) : undefined; const result: ContractComparisonResult = { success: true, mode: validated.comparisonMode, changes, alignments, similarityScore, summary, redlineMarkup, durationMs: Date.now() - startTime, }; return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], data: result, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: errorMessage, durationMs: Date.now() - startTime, }, null, 2), }], }; } }, }; // ============================================================================ // Obligation Track Tool // ============================================================================ /** * MCP Tool: legal/obligation-track * * Extract obligations, deadlines, and dependencies using DAG analysis */ export const obligationTrackTool: MCPTool< z.infer<typeof ObligationTrackInputSchema>, ObligationTrackingResult > = { name: 'legal/obligation-track', description: 'Extract obligations, deadlines, and dependencies using DAG analysis', category: 'legal', version: '3.0.0-alpha.1', inputSchema: ObligationTrackInputSchema, handler: async (input, context) => { const startTime = Date.now(); try { const validated = ObligationTrackInputSchema.parse(input); // Extract obligations let obligations = await extractObligations( validated.document, validated.obligationTypes ); // Filter by party if specified if (validated.party) { obligations = obligations.filter(o => o.party.toLowerCase().includes(validated.party!.toLowerCase()) ); } // Filter by timeframe if specified if (validated.timeframe) { obligations = filterByTimeframe(obligations, validated.timeframe); } // Initialize DAG bridge const dag = context.bridges.dag; if (!dag.isInitialized()) { await dag.initialize(); } // Build dependency graph const graph = await dag.buildDependencyGraph(obligations); // Build timeline const timeline = buildTimeline(obligations); // Find upcoming deadlines (next 30 days) const now = new Date(); const thirtyDaysLater = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); const upcomingDeadlines = obligations.filter(o => o.dueDate && o.dueDate >= now && o.dueDate <= thirtyDaysLater ); // Find overdue const overdue = obligations.filter(o => o.dueDate && o.dueDate < now && o.status !== 'completed' && o.status !== 'waived' ); const result: ObligationTrackingResult = { success: true, obligations, graph, timeline, upcomingDeadlines, overdue, durationMs: Date.now() - startTime, }; return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], data: result, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: errorMessage, durationMs: Date.now() - startTime, }, null, 2), }], }; } }, }; // ============================================================================ // Playbook Match Tool // ============================================================================ /** * MCP Tool: legal/playbook-match * * Compare contract clauses against negotiation playbook */ export const playbookMatchTool: MCPTool< z.infer<typeof PlaybookMatchInputSchema>, PlaybookMatchResult > = { name: 'legal/playbook-match', description: 'Compare contract clauses against negotiation playbook', category: 'legal', version: '3.0.0-alpha.1', inputSchema: PlaybookMatchInputSchema, handler: async (input, context) => { const startTime = Date.now(); try { const validated = PlaybookMatchInputSchema.parse(input); // Parse playbook const playbook = parsePlaybook(validated.playbook); // Extract clauses from document const clauses = await extractClauses(validated.document, undefined, 'US', context); // Initialize attention bridge const attention = context.bridges.attention; if (!attention.isInitialized()) { await attention.initialize(); } // Match clauses against playbook const matches = await matchAgainstPlaybook( clauses, playbook, validated.strictness, validated.suggestAlternatives, attention ); // Build summary const summary = { totalClauses: matches.length, matchesPreferred: matches.filter(m => m.status === 'matches_preferred').length, matchesAcceptable: matches.filter(m => m.status === 'matches_acceptable').length, requiresFallback: matches.filter(m => m.status === 'requires_fallback').length, violatesRedline: matches.filter(m => m.status === 'violates_redline').length, noMatch: matches.filter(m => m.status === 'no_match').length, }; // Find red line violations const redLineViolations = matches.filter(m => m.status === 'violates_redline'); // Prioritize negotiations const negotiationPriorities = buildNegotiationPriorities(matches, validated.prioritizeClauses); const result: PlaybookMatchResult = { success: true, playbook: { id: playbook.id, name: playbook.name, version: playbook.version, }, matches, summary, redLineViolations, negotiationPriorities, durationMs: Date.now() - startTime, }; return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], data: result, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: errorMessage, durationMs: Date.now() - startTime, }, null, 2), }], }; } }, }; // ============================================================================ // Helper Functions // ============================================================================ /** * Parse document metadata */ function parseDocumentMetadata(document: string): DocumentMetadata { const hash = simpleHash(document); return { id: `doc-${hash.substring(0, 8)}`, format: 'txt', wordCount: document.split(/\s+/).length, charCount: document.length, language: 'en', parties: [], contentHash: hash, }; } /** * Simple hash function */ function simpleHash(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(16).padStart(16, '0'); } /** * Extract clauses from document */ async function extractClauses( document: string, clauseTypes: ClauseType[] | undefined, _jurisdiction: string, _context: ToolContext ): Promise<ExtractedClause[]> { const clauses: ExtractedClause[] = []; // Define clause patterns const clausePatterns: Record<ClauseType, RegExp[]> = { indemnification: [/indemnif/i, /hold\s+harmless/i, /defend\s+and\s+indemnify/i], limitation_of_liability: [/limitation\s+of\s+liability/i, /liability\s+shall\s+not\s+exceed/i], termination: [/termination/i, /right\s+to\s+terminate/i, /upon\s+termination/i], confidentiality: [/confidential/i, /non-disclosure/i, /proprietary\s+information/i], ip_assignment: [/intellectual\s+property/i, /assignment\s+of\s+(ip|rights)/i, /work\s+for\s+hire/i], governing_law: [/governing\s+law/i, /governed\s+by\s+the\s+laws/i, /jurisdiction/i], arbitration: [/arbitration/i, /arbitral\s+proceedings/i, /binding\s+arbitration/i], force_majeure: [/force\s+majeure/i, /act\s+of\s+god/i, /beyond\s+reasonable\s+control/i], warranty: [/warrant/i, /represents\s+and\s+warrants/i, /as-is/i], payment_terms: [/payment/i, /invoic/i, /net\s+\d+/i], non_compete: [/non-?compet/i, /not\s+compete/i], non_solicitation: [/non-?solicit/i, /not\s+solicit/i], assignment: [/assignment/i, /may\s+not\s+assign/i], insurance: [/insurance/i, /maintain\s+coverage/i], representations: [/represent/i, /represent\s+and\s+warrant/i], covenants: [/covenant/i, /agrees\s+to/i], data_protection: [/data\s+protection/i, /personal\s+data/i, /gdpr/i, /privacy/i], audit_rights: [/audit/i, /right\s+to\s+inspect/i, /access\s+to\s+records/i], }; // Split document into sections/paragraphs const sections = document.split(/\n\n+/); let offset = 0; for (const section of sections) { const sectionStart = document.indexOf(section, offset); const sectionEnd = sectionStart + section.length; offset = sectionEnd; // Try to classify section for (const [type, patterns] of Object.entries(clausePatterns)) { const clauseType = type as ClauseType; // Skip if not in requested types if (clauseTypes && clauseTypes.length > 0 && !clauseTypes.includes(clauseType)) { continue; } // Check patterns let matchCount = 0; for (const pattern of patterns) { if (pattern.test(section)) { matchCount++; } } if (matchCount > 0) { const confidence = Math.min(0.5 + matchCount * 0.2, 0.99); clauses.push({ id: `clause-${clauses.length + 1}`, type: clauseType, text: section.trim(), startOffset: sectionStart, endOffset: sectionEnd, confidence, keyTerms: extractKeyTerms(section), }); break; // Only classify as one type } } } return clauses; } /** * Extract key terms from text */ function extractKeyTerms(text: string): string[] { const terms: string[] = []; const termPatterns = [ /\$[\d,]+/g, // Dollar amounts /\d+\s*(days?|months?|years?)/gi, // Time periods /\d+%/g, // Percentages /"[^"]+"/g, // Quoted terms ]; for (const pattern of termPatterns) { const matches = text.match(pattern); if (matches) { terms.push(...matches); } } return [...new Set(terms)].slice(0, 10); } /** * Assess risks in clauses */ async function assessRisks( clauses: ExtractedClause[], _partyRole: string, categories: RiskCategory[] | undefined, _industryContext: string | undefined ): Promise<RiskFinding[]> { const risks: RiskFinding[] = []; // Risk patterns by clause type and party role const riskPatterns: Record<string, Array<{ pattern: RegExp; severity: RiskSeverity; category: RiskCategory; title: string; description: string; mitigation: string; }>> = { indemnification: [ { pattern: /unlimited\s+indemnification/i, severity: 'critical', category: 'financial', title: 'Unlimited Indemnification', description: 'Contract requires unlimited indemnification which could expose party to significant financial risk', mitigation: 'Negotiate cap on indemnification liability', }, ], limitation_of_liability: [ { pattern: /no\s+limitation/i, severity: 'high', category: 'financial', title: 'No Liability Cap', description: 'Contract contains no limitation on liability', mitigation: 'Add liability cap based on contract value or insurance coverage', }, ], termination: [ { pattern: /immediate\s+termination/i, severity: 'medium', category: 'operational', title: 'Immediate Termination Right', description: 'Counterparty can terminate immediately without notice', mitigation: 'Negotiate notice period for termination', }, ], warranty: [ { pattern: /as-?is/i, severity: 'medium', category: 'legal', title: 'As-Is Warranty Disclaimer', description: 'Product/service provided without warranty', mitigation: 'Negotiate minimum performance warranties', }, ], }; for (const clause of clauses) { const patterns = riskPatterns[clause.type] ?? []; for (const riskPattern of patterns) { if (riskPattern.pattern.test(clause.text)) { // Filter by category if specified if (categories && !categories.includes(riskPattern.category)) { continue; } risks.push({ id: `risk-${risks.length + 1}`, category: riskPattern.category, severity: riskPattern.severity, title: riskPattern.title, description: riskPattern.description, clauseIds: [clause.id], mitigations: [riskPattern.mitigation], deviatesFromStandard: true, confidence: clause.confidence, }); } } } return risks; } /** * Get severity level as number */ function getSeverityLevel(severity: RiskSeverity): number { const levels: Record<RiskSeverity, number> = { low: 1, medium: 2, high: 3, critical: 4, }; return levels[severity]; } /** * Build category summary */ function buildCategorySummary( risks: RiskFinding[] ): Record<RiskCategory, { count: number; highestSeverity: RiskSeverity; averageScore: number }> { const summary: Record<string, { count: number; highestSeverity: RiskSeverity; totalScore: number }> = {}; for (const category of Object.values(RiskCategory.options)) { summary[category] = { count: 0, highestSeverity: 'low', totalScore: 0 }; } for (const risk of risks) { const cat = summary[risk.category]; if (cat) { cat.count++; cat.totalScore += getSeverityLevel(risk.severity); if (getSeverityLevel(risk.severity) > getSeverityLevel(cat.highestSeverity)) { cat.highestSeverity = risk.severity; } } } const result: Record<RiskCategory, { count: number; highestSeverity: RiskSeverity; averageScore: number }> = {} as any; for (const [category, data] of Object.entries(summary)) { result[category as RiskCategory] = { count: data.count, highestSeverity: data.highestSeverity, averageScore: data.count > 0 ? data.totalScore / data.count : 0, }; } return result; } /** * Calculate overall risk score */ function calculateOverallRiskScore(risks: RiskFinding[]): number { if (risks.length === 0) return 100; const maxScore = 100; let penalty = 0; for (const risk of risks) { const severityPenalty: Record<RiskSeverity, number> = { low: 2, medium: 5, high: 15, critical: 30, }; penalty += severityPenalty[risk.severity]; } return Math.max(0, maxScore - penalty); } /** * Convert score to grade */ function scoreToGrade(score: number): 'A' | 'B' | 'C' | 'D' | 'F' { if (score >= 90) return 'A'; if (score >= 80) return 'B'; if (score >= 70) return 'C'; if (score >= 60) return 'D'; return 'F'; } /** * Detect changes between documents */ function detectChanges( baseClauses: ExtractedClause[], compareClauses: ExtractedClause[], alignments: import('./types.js').ClauseAlignment[] ): import('./types.js').ContractChange[] { const changes: import('./types.js').ContractChange[] = []; const alignedCompare = new Set(alignments.map(a => a.compareClauseId)); for (const alignment of alignments) { const baseClause = baseClauses.find(c => c.id === alignment.baseClauseId); const compareClause = compareClauses.find(c => c.id === alignment.compareClauseId); if (alignment.alignmentType === 'no_match') { changes.push({ type: 'removed', clauseType: baseClause?.type, baseSection: baseClause?.section, baseText: baseClause?.text, significance: 0.8, impact: 'requires_review', explanation: 'Clause exists in base but not in comparison document', }); } else if (alignment.alignmentType !== 'exact') { changes.push({ type: 'modified', clauseType: baseClause?.type, baseSection: baseClause?.section, compareSection: compareClause?.section, baseText: baseClause?.text, compareText: compareClause?.text, significance: 1 - alignment.similarity, impact: 'requires_review', explanation: `Clause modified (${(alignment.similarity * 100).toFixed(1)}% similarity)`, }); } } // Find added clauses for (const clause of compareClauses) { if (!alignedCompare.has(clause.id)) { changes.push({ type: 'added', clauseType: clause.type, compareSection: clause.section, compareText: clause.text, significance: 0.7, impact: 'requires_review', explanation: 'New clause in comparison document', }); } } return changes; } /** * Generate redline markup */ function generateRedlineMarkup( baseDocument: string, changes: import('./types.js').ContractChange[] ): string { // Simplified redline generation let markup = baseDocument; for (const change of changes) { if (change.type === 'removed' && change.baseText) { markup = markup.replace( change.baseText, `<del style="color:red">${change.baseText}</del>` ); } else if (change.type === 'added' && change.compareText) { markup += `\n<ins style="color:green">${change.compareText}</ins>`; } } return markup; } /** * Extract obligations from document */ async function extractObligations( document: string, types: import('./types.js').ObligationType[] | undefined ): Promise<Obligation[]> { const obligations: Obligation[] = []; // Obligation patterns const obligationPatterns: Record<string, { pattern: RegExp; type: import('./types.js').ObligationType }[]> = { payment: [ { pattern: /shall\s+pay/i, type: 'payment' }, { pattern: /payment\s+due/i, type: 'payment' }, ], delivery: [ { pattern: /shall\s+deliver/i, type: 'delivery' }, { pattern: /delivery\s+date/i, type: 'delivery' }, ], notification: [ { pattern: /shall\s+notify/i, type: 'notification' }, { pattern: /provide\s+notice/i, type: 'notification' }, ], approval: [ { pattern: /shall\s+approve/i, type: 'approval' }, { pattern: /written\s+approval/i, type: 'approval' }, ], compliance: [ { pattern: /shall\s+comply/i, type: 'compliance' }, { pattern: /in\s+compliance\s+with/i, type: 'compliance' }, ], }; const sentences = document.split(/[.!?]+/); for (let i = 0; i < sentences.length; i++) { const sentence = sentences[i]?.trim() ?? ''; if (!sentence) continue; for (const [, patterns] of Object.entries(obligationPatterns)) { for (const { pattern, type } of patterns) { if (types && !types.includes(type)) continue; if (pattern.test(sentence)) { obligations.push({ id: `obl-${obligations.length + 1}`, type, party: extractParty(sentence), description: sentence, dependsOn: [], blocks: [], clauseIds: [], status: 'pending', priority: 'medium', }); break; } } } } return obligations; } /** * Extract party from sentence */ function extractParty(sentence: string): string { const partyPatterns = [ /the\s+(buyer|seller|licensor|licensee|employer|employee)/i, /(party\s+a|party\s+b)/i, /the\s+company/i, ]; for (const pattern of partyPatterns) { const match = sentence.match(pattern); if (match?.[1]) { return match[1]; } } return 'Unknown Party'; } /** * Filter obligations by timeframe */ function filterByTimeframe( obligations: Obligation[], _timeframe: string ): Obligation[] { // Parse ISO duration or date range // Simplified implementation return obligations; } /** * Build timeline from obligations */ function buildTimeline( obligations: Obligation[] ): Array<{ date: Date; obligations: string[]; isMilestone: boolean }> { const timeline: Map<string, { date: Date; obligations: string[]; isMilestone: boolean }> = new Map(); for (const obligation of obligations) { if (obligation.dueDate) { const dateKey = obligation.dueDate.toISOString().split('T')[0] ?? ''; const existing = timeline.get(dateKey); if (existing) { existing.obligations.push(obligation.id); } else { timeline.set(dateKey, { date: obligation.dueDate, obligations: [obligation.id], isMilestone: obligation.priority === 'critical', }); } } } return Array.from(timeline.values()).sort((a, b) => a.date.getTime() - b.date.getTime()); } /** * Parse playbook from string (JSON or ID) */ function parsePlaybook(playbookInput: string): import('./types.js').Playbook { try { const parsed = JSON.parse(playbookInput); return parsed as import('./types.js').Playbook; } catch { // Return a default playbook return { id: playbookInput, name: 'Default Playbook', contractType: 'General', jurisdiction: 'US', partyRole: 'buyer', updatedAt: new Date(), version: '1.0.0', positions: [], }; } } /** * Match clauses against playbook */ async function matchAgainstPlaybook( clauses: ExtractedClause[], playbook: import('./types.js').Playbook, strictness: import('./types.js').PlaybookStrictness, suggestAlternatives: boolean, _attention: IAttentionBridge ): Promise<PlaybookMatch[]> { const matches: PlaybookMatch[] = []; for (const clause of clauses) { const position = playbook.positions.find(p => p.clauseType === clause.type); if (!position) { matches.push({ clauseId: clause.id, position: { clauseType: clause.type, preferredLanguage: '', acceptableVariations: [], redLines: [], fallbackPositions: [], negotiationNotes: '', businessJustification: '', }, status: 'no_match', preferredSimilarity: 0, recommendation: 'No playbook position defined for this clause type', }); continue; } // Check against preferred language const preferredSimilarity = calculateTextSimilarity(clause.text, position.preferredLanguage); // Determine status based on similarity and strictness let status: PlaybookMatch['status']; const thresholds = { strict: { preferred: 0.95, acceptable: 0.9, fallback: 0.8 }, moderate: { preferred: 0.85, acceptable: 0.75, fallback: 0.6 }, flexible: { preferred: 0.7, acceptable: 0.6, fallback: 0.4 }, }; const threshold = thresholds[strictness]; // Check red lines first const violatesRedLine = position.redLines.some(rl => clause.text.toLowerCase().includes(rl.toLowerCase()) ); if (violatesRedLine) { status = 'violates_redline'; } else if (preferredSimilarity >= threshold.preferred) { status = 'matches_preferred'; } else if (position.acceptableVariations.some(v => calculateTextSimilarity(clause.text, v) >= threshold.acceptable )) { status = 'matches_acceptable'; } else if (position.fallbackPositions.length > 0) { status = 'requires_fallback'; } else { status = 'no_match'; } matches.push({ clauseId: clause.id, position, status, preferredSimilarity, suggestedAlternative: suggestAlternatives ? position.preferredLanguage : undefined, recommendation: generateRecommendation(status, clause.type), }); } return matches; } /** * Calculate text similarity (simplified) */ function calculateTextSimilarity(text1: string, text2: string): number { if (!text1 || !text2) return 0; const words1 = new Set(text1.toLowerCase().split(/\s+/)); const words2 = new Set(text2.toLowerCase().split(/\s+/)); const intersection = new Set([...words1].filter(w => words2.has(w))); const union = new Set([...words1, ...words2]); return intersection.size / union.size; } /** * Generate recommendation based on match status */ function generateRecommendation(status: PlaybookMatch['status'], _clauseType: ClauseType): string { const recommendations: Record<PlaybookMatch['status'], string> = { matches_preferred: 'Clause matches preferred playbook position. No action required.', matches_acceptable: 'Clause is within acceptable variation. Consider negotiating closer to preferred position.', requires_fallback: 'Clause requires fallback position. Review fallback options and negotiate accordingly.', violates_redline: 'CRITICAL: Clause violates red line. This must be negotiated before signing.', no_match: 'No playbook position available. Conduct independent review of this clause.', }; return recommendations[status]; } /** * Build negotiation priorities */ function buildNegotiationPriorities( matches: PlaybookMatch[], prioritizedTypes: ClauseType[] | undefined ): Array<{ clauseId: string; priority: number; reason: string }> { const priorities: Array<{ clauseId: string; priority: number; reason: string }> = []; const statusPriority: Record<PlaybookMatch['status'], number> = { violates_redline: 100, requires_fallback: 70, no_match: 50, matches_acceptable: 30, matches_preferred: 10, }; for (const match of matches) { let priority = statusPriority[match.status]; // Boost priority for prioritized clause types if (prioritizedTypes?.includes(match.position.clauseType)) { priority += 20; } priorities.push({ clauseId: match.clauseId, priority, reason: match.recommendation, }); } return priorities.sort((a, b) => b.priority - a.priority); } // ============================================================================ // Tool Registry // ============================================================================ /** * All Legal Contracts MCP Tools */ export const legalContractsTools: MCPTool[] = [ clauseExtractTool as unknown as MCPTool, riskAssessTool as unknown as MCPTool, contractCompareTool as unknown as MCPTool, obligationTrackTool as unknown as MCPTool, playbookMatchTool as unknown as MCPTool, ]; /** * Tool name to handler map */ export const toolHandlers = new Map<string, MCPTool['handler']>([ ['legal/clause-extract', clauseExtractTool.handler as MCPTool['handler']], ['legal/risk-assess', riskAssessTool.handler as MCPTool['handler']], ['legal/contract-compare', contractCompareTool.handler as MCPTool['handler']], ['legal/obligation-track', obligationTrackTool.handler as MCPTool['handler']], ['legal/playbook-match', playbookMatchTool.handler as MCPTool['handler']], ]); /** * Create tool context with bridges */ export function createToolContext(): ToolContext { const store = new Map<string, unknown>(); return { get: <T>(key: string) => store.get(key) as T | undefined, set: <T>(key: string, value: T) => { store.set(key, value); }, bridges: { attention: createAttentionBridge(), dag: createDAGBridge(), }, }; } export default legalContractsTools;