Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Analyze construction contract clauses. Identify risks, obligations, and key terms using NLP.
.claude/skills/datadrivenconstruction-contract-clause-analyzer/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-17 | ✗→✓ | ▲ Improved | 256% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 411% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 497% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 382% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 64% | 0% |
Contract review is time-consuming and error-prone:
AI-assisted contract clause analysis that identifies key provisions, flags risks, and extracts critical terms.
pythonimport pandas as pd from datetime import datetime, date from typing import Dict, Any, List, Optional from dataclasses import dataclass, field from enum import Enum import re class ClauseType(Enum): SCOPE = "scope" PAYMENT = "payment" SCHEDULE = "schedule" CHANGE_ORDER = "change_order" TERMINATION = "termination" INDEMNIFICATION = "indemnification" INSURANCE = "insurance" WARRANTY = "warranty" DISPUTE = "dispute" LIABILITY = "liability" FORCE_MAJEURE = "force_majeure" SAFETY = "safety" COMPLIANCE = "compliance" OTHER = "other" class RiskLevel(Enum): HIGH = "high" MEDIUM = "medium" LOW = "low" INFO = "info" @dataclass class ContractClause: clause_id: str section: str title: str text: str clause_type: ClauseType risk_level: RiskLevel key_terms: List[str] = field(default_factory=list) obligations: List[str] = field(default_factory=list) deadlines: List[str] = field(default_factory=list) amounts: List[str] = field(default_factory=list) notes: str = "" @dataclass class AnalysisResult: contract_name: str analyzed_date: datetime total_clauses: int clauses: List[ContractClause] risk_summary: Dict[str, int] key_dates: List[Dict[str, str]] key_amounts: List[Dict[str, str]] class ContractClauseAnalyzer: """Analyze construction contract clauses.""" RISK_KEYWORDS = { 'high': ['indemnify', 'sole discretion', 'waive', 'forfeit', 'liquidated damages', 'consequential', 'unlimited liability', 'hold harmless', 'no limit'], 'medium': ['shall', 'must', 'required', 'obligated', 'responsible', 'liable', 'penalty', 'default', 'breach'], 'low': ['may', 'should', 'reasonable', 'mutual', 'consent', 'approval'] } CLAUSE_PATTERNS = { ClauseType.PAYMENT: ['payment', 'invoice', 'retainage', 'progress payment'], ClauseType.SCHEDULE: ['schedule', 'completion date', 'milestone', 'time is of the essence'], ClauseType.CHANGE_ORDER: ['change order', 'modification', 'additional work', 'variation'], ClauseType.TERMINATION: ['termination', 'terminate', 'cancellation'], ClauseType.INDEMNIFICATION: ['indemnif', 'hold harmless', 'defend'], ClauseType.INSURANCE: ['insurance', 'coverage', 'policy', 'insured'], ClauseType.WARRANTY: ['warranty', 'guarantee', 'defect', 'workmanship'], ClauseType.DISPUTE: ['dispute', 'arbitration', 'mediation', 'litigation'], ClauseType.LIABILITY: ['liability', 'damages', 'limitation'], ClauseType.FORCE_MAJEURE: ['force majeure', 'act of god', 'unforeseen'], } def __init__(self): self.clauses: List[ContractClause] = [] def analyze_text(self, contract_name: str, text: str) -> AnalysisResult: """Analyze contract text.""" self.clauses = [] # Split into sections/clauses sections = self._split_into_sections(text) for i, section in enumerate(sections): clause = self._analyze_clause(f"CL-{i+1:03d}", section) self.clauses.append(clause) # Generate summary risk_summary = { 'high': sum(1 for c in self.clauses if c.risk_level == RiskLevel.HIGH), 'medium': sum(1 for c in self.clauses if c.risk_level == RiskLevel.MEDIUM), 'low': sum(1 for c in self.clauses if c.risk_level == RiskLevel.LOW) } key_dates = [] key_amounts = [] for clause in self.clauses: for d in clause.deadlines: key_dates.append({'clause': clause.clause_id, 'date': d}) for a in clause.amounts: key_amounts.append({'clause': clause.clause_id, 'amount': a}) return AnalysisResult( contract_name=contract_name, analyzed_date=datetime.now(), total_clauses=len(self.clauses), clauses=self.clauses, risk_summary=risk_summary, key_dates=key_dates, key_amounts=key_amounts ) def _split_into_sections(self, text: str) -> List[Dict[str, str]]: """Split contract into sections.""" sections = [] # Simple split by numbered sections pattern = r'(\d+\.[\d\.]*\s+[A-Z][^\.]+)' parts = re.split(pattern, text) current_title = "" for i, part in enumerate(parts): if re.match(r'\d+\.[\d\.]*\s+[A-Z]', part): current_title = part.strip() elif part.strip() and current_title: sections.append({ 'title': current_title, 'text': part.strip() }) current_title = "" # If no sections found, treat whole text as one if not sections and text.strip(): sections.append({'title': 'Contract Text', 'text': text.strip()}) return sections def _analyze_clause(self, clause_id: str, section: Dict[str, str]) -> ContractClause: """Analyze single clause.""" text = section.get('text', '') title = section.get('title', '') text_lower = text.lower() # Determine clause type clause_type = self._determine_type(text_lower) # Assess risk level risk_level = self._assess_risk(text_lower) # Extract key terms key_terms = self._extract_key_terms(text) # Extract obligations obligations = self._extract_obligations(text) # Extract dates deadlines = self._extract_dates(text) # Extract amounts amounts = self._extract_amounts(text) return ContractClause( clause_id=clause_id, section=clause_id, title=title, text=text[:500] + "..." if len(text) > 500 else text, clause_type=clause_type, risk_level=risk_level, key_terms=key_terms, obligations=obligations, deadlines=deadlines, amounts=amounts ) def _determine_type(self, text: str) -> ClauseType: """Determine clause type from content.""" for clause_type, keywords in self.CLAUSE_PATTERNS.items(): if any(kw in text for kw in keywords): return clause_type return ClauseType.OTHER def _assess_risk(self, text: str) -> RiskLevel: """Assess risk level of clause.""" high_count = sum(1 for kw in self.RISK_KEYWORDS['high'] if kw in text) medium_count = sum(1 for kw in self.RISK_KEYWORDS['medium'] if kw in text) if high_count >= 2: return RiskLevel.HIGH elif high_count >= 1 or medium_count >= 3: return RiskLevel.MEDIUM elif medium_count >= 1: return RiskLevel.LOW return RiskLevel.INFO def _extract_key_terms(self, text: str) -> List[str]: """Extract key defined terms.""" # Look for quoted terms or capitalized multi-word phrases patterns = [ r'"([^"]+)"', r"'([^']+)'", r'\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b' ] terms = [] for pattern in patterns: matches = re.findall(pattern, text) terms.extend(matches[:5]) return list(set(terms))[:10] def _extract_obligations(self, text: str) -> List[str]: """Extract obligation statements.""" patterns = [ r'(?:contractor|owner|party)\s+shall\s+([^\.]+)', r'(?:contractor|owner|party)\s+must\s+([^\.]+)', r'(?:contractor|owner|party)\s+is\s+(?:required|obligated)\s+to\s+([^\.]+)' ] obligations = [] for pattern in patterns: matches = re.findall(pattern, text, re.IGNORECASE) obligations.extend(matches[:3]) return obligations[:5] def _extract_dates(self, text: str) -> List[str]: """Extract date references.""" patterns = [ r'\b\d{1,2}/\d{1,2}/\d{2,4}\b', r'\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},?\s+\d{4}\b', r'\b\d+\s+(?:calendar|working|business)\s+days\b', r'\bwithin\s+\d+\s+days\b' ] dates = [] for pattern in patterns: matches = re.findall(pattern, text, re.IGNORECASE) dates.extend(matches) return dates[:5] def _extract_amounts(self, text: str) -> List[str]: """Extract monetary amounts.""" patterns = [ r'\$[\d,]+(?:\.\d{2})?', r'\b\d+(?:,\d{3})*(?:\.\d{2})?\s*(?:dollars|USD)\b', r'\b\d+(?:\.\d+)?%\b' ] amounts = [] for pattern in patterns: matches = re.findall(pattern, text, re.IGNORECASE) amounts.extend(matches) return amounts[:5] def get_high_risk_clauses(self) -> List[ContractClause]: """Get all high-risk clauses.""" return [c for c in self.clauses if c.risk_level == RiskLevel.HIGH] def export_analysis(self, result: AnalysisResult, output_path: str): """Export analysis to Excel.""" with pd.ExcelWriter(output_path, engine='openpyxl') as writer: # Summary summary_df = pd.DataFrame([{ 'Contract': result.contract_name, 'Analyzed': result.analyzed_date, 'Total Clauses': result.total_clauses, 'High Risk': result.risk_summary['high'], 'Medium Risk': result.risk_summary['medium'], 'Low Risk': result.risk_summary['low'] }]) summary_df.to_excel(writer, sheet_name='Summary', index=False) # Clauses clause_data = [{ 'ID': c.clause_id, 'Title': c.title[:50], 'Type': c.clause_type.value, 'Risk': c.risk_level.value, 'Key Terms': ', '.join(c.key_terms[:3]), 'Obligations': len(c.obligations), 'Dates': ', '.join(c.deadlines[:2]), 'Amounts': ', '.join(c.amounts[:2]) } for c in result.clauses] pd.DataFrame(clause_data).to_excel(writer, sheet_name='Clauses', index=False) return output_path
pythonanalyzer = ContractClauseAnalyzer() # Analyze contract text contract_text = open("contract.txt").read() result = analyzer.analyze_text("Construction Contract", contract_text) print(f"High risk clauses: {result.risk_summary['high']}") # Get risky clauses high_risk = analyzer.get_high_risk_clauses() for clause in high_risk: print(f"{clause.clause_id}: {clause.title}")
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-17 | fail→pass | 7,201 | 3,070 | -57% | 1 | 1 | 0% | 1,018 | 3,619 | +256% | 0 | 0 | — |
case-02 | pass→pass | 29,546 | 28,780 | -3% | 1 | 1 | 0% | 5,557 | 9,361 | +68% | 0 | 0 | — |
case-01 | fail→pass | 9,250 | 24,761 | +168% | 1 | 1 | 0% | 1,575 | 8,048 | +411% | 0 | 0 | — |
case-03 | fail→pass | 4,564 | 4,870 | +7% | 1 | 1 | 0% | 695 | 4,148 | +497% | 0 | 0 | — |
case-04 | pass→pass | 7,092 | 3,769 | -47% | 1 | 1 | 0% | 1,027 | 3,932 | +283% | 0 | 0 | — |
case-05 | pass→pass | 5,731 | 2,901 | -49% | 1 | 1 | 0% | 879 | 3,722 | +323% | 0 | 0 | — |
case-06 | fail→pass | 4,942 | 3,230 | -35% | 1 | 1 | 0% | 785 | 3,781 | +382% | 0 | 0 | — |
case-07 | pass→pass | 6,952 | 3,972 | -43% | 1 | 1 | 0% | 1,068 | 3,989 | +274% | 0 | 0 | — |
case-08 | pass→pass | 7,522 | 4,016 | -47% | 1 | 1 | 0% | 1,245 | 3,941 | +217% | 0 | 0 | — |
case-09 | pass→pass | 4,653 | 2,081 | -55% | 1 | 1 | 0% | 708 | 3,484 | +392% | 0 | 0 | — |
case-10 | pass→pass | 5,815 | 3,275 | -44% | 1 | 1 | 0% | 900 | 3,809 | +323% | 0 | 0 | — |
case-11 | pass→pass | 7,280 | 3,743 | -49% | 1 | 1 | 0% | 1,120 | 3,777 | +237% | 0 | 0 | — |
case-12 | fail→pass | 13,841 | 3,173 | -77% | 1 | 1 | 0% | 2,239 | 3,681 | +64% | 0 | 0 | — |
case-13 | fail→fail | 9,084 | 2,764 | -70% | 1 | 1 | 0% | 1,397 | 3,732 | +167% | 0 | 0 | — |
case-14 | pass→pass | 15,756 | 5,557 | -65% | 1 | 1 | 0% | 2,645 | 4,263 | +61% | 0 | 0 | — |
case-15 | pass→pass | 17,493 | 15,976 | -9% | 1 | 1 | 0% | 2,872 | 5,810 | +102% | 0 | 0 | — |
case-16 | pass→pass | 12,820 | 5,625 | -56% | 1 | 1 | 0% | 2,240 | 4,191 | +87% | 0 | 0 | — |
case-18 | pass→pass | 7,287 | 3,579 | -51% | 1 | 1 | 0% | 1,126 | 3,769 | +235% | 0 | 0 | — |
case-19 | fail→pass | 10,737 | 2,394 | -78% | 1 | 1 | 0% | 1,566 | 3,520 | +125% | 0 | 0 | — |
case-20 | pass→pass | 7,724 | 3,155 | -59% | 1 | 1 | 0% | 1,140 | 3,661 | +221% | 0 | 0 | — |
case-21 | fail→fail | 9,549 | 8,385 | -12% | 1 | 1 | 0% | 1,596 | 4,422 | +177% | 0 | 0 | — |
case-22 | fail→fail | 12,679 | 11,927 | -6% | 1 | 1 | 0% | 2,209 | 5,155 | +133% | 0 | 0 | — |
case-23 | fail→fail | 20,604 | 15,616 | -24% | 1 | 1 | 0% | 3,853 | 6,016 | +56% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +26 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.