AgentSkillsCN

Zonewize

Zonewize

SKILL.md

zonewize - Zoning Compliance Analysis Skill

Version

Current: v1.0.0
Last Updated: January 13, 2026
Status: Production
Ecosystem: ZoneWise

Purpose

Analyzes property zoning compliance for 17 Brevard County jurisdictions by scraping municipal ordinances, parsing zoning codes, and comparing property characteristics against allowed uses. Provides compliance status, violation details, and variance requirements.

Inputs

Required Parameters

  • property_id (str): Unique identifier in ZoneWise database
  • address (str): Full property address (e.g., "123 Ocean Dr, Indian Harbour Beach, FL 32937")
  • jurisdiction (str): One of 17 Brevard jurisdictions
    • Valid values: "indian_harbour_beach", "melbourne", "palm_bay", "cocoa", "cocoa_beach", "rockledge", "titusville", "satellite_beach", "west_melbourne", "cape_canaveral", "malabar", "grant_valkaria", "indialantic", "melbourne_beach", "melbourne_village", "palm_shores", "brevard_county_unincorporated"

Optional Parameters

  • parcel_id (str): County parcel identifier (default: None)
  • property_type (str): "residential", "commercial", "industrial", "mixed_use" (default: "residential")
  • current_use (str): Actual current use (e.g., "single_family_home", "retail_store") (default: None)
  • proposed_use (str): Proposed future use for development analysis (default: None)
  • correlation_id (str): For distributed tracing (default: auto-generated UUID)

Outputs

python
{
    "success": bool,
    "compliance_status": str,        # "COMPLIANT", "NON_COMPLIANT", "UNKNOWN", "MANUAL_REVIEW"
    "zoning_district": str,          # e.g., "R-1", "C-2", "I-1", "PUD"
    "allowed_uses": list[str],       # Permitted uses in this zoning district
    "violations": list[dict],        # List of specific violations found
        # Each violation:
        # {
        #   "type": str,               # "use", "setback", "height", "lot_size", "parking"
        #   "description": str,        # Human-readable explanation
        #   "severity": str,           # "CRITICAL", "MAJOR", "MINOR"
        #   "code_reference": str,     # Ordinance section (e.g., "Section 62-1234")
        #   "current_value": str,      # What the property has
        #   "required_value": str      # What the code requires
        # }
    "confidence_score": int,         # 0-100, how confident is this analysis
    "requires_variance": bool,       # Does proposed use require variance approval
    "ordinance_sections": list[str], # Relevant ordinance sections referenced
    "ordinance_last_updated": str,   # ISO timestamp of last ordinance scrape
    "data_source": str,              # "firecrawl_fresh", "firecrawl_cache", "manual_review"
    "cache_hit": bool,               # Was cached data used?
    "execution_time_ms": float,      # Total execution time
    "cost_usd": float,               # Cost for this analysis
    "jurisdiction_config": dict      # Config used for this jurisdiction
}

Execution Logic

Step 1: Check Ordinance Cache

python
cached_ordinance = get_cached_ordinance(jurisdiction)
if cached_ordinance and cache_age < 7_days:
    ordinance_data = cached_ordinance
    data_source = "firecrawl_cache"
    skip_to_step_3 = True

Step 2: Scrape Ordinance (if not cached or expired)

python
jurisdiction_config = JURISDICTION_CONFIGS[jurisdiction]
ordinance_url = jurisdiction_config['ordinance_url']

try:
    firecrawl_result = scrape_ordinance(ordinance_url)
    ordinance_data = firecrawl_result['content']
    cache_ordinance(jurisdiction, ordinance_data, ttl=7_days)
    data_source = "firecrawl_fresh"
except FirecrawlError as e:
    # Fallback 1: Use expired cache
    if cached_ordinance:
        ordinance_data = cached_ordinance
        data_source = "firecrawl_cache_expired"
        log_metric("zonewize_expired_cache_used", 1)
    else:
        # Fallback 2: Flag for manual review
        return {
            "compliance_status": "MANUAL_REVIEW",
            "confidence_score": 0,
            "requires_manual": True,
            "error": str(e)
        }

Step 3: Parse Ordinance

python
zoning_rules = parse_ordinance(ordinance_data, jurisdiction)
# Extract:
# - Zoning district definitions
# - Allowed uses by district
# - Dimensional requirements (setbacks, height, lot size)
# - Special conditions (overlay districts, HOA restrictions)
# - Parking requirements
# - Sign regulations

Step 4: Fetch Property Data

python
property_data = fetch_property_from_supabase(property_id)
# Includes: address, parcel_id, square_footage, lot_size,
#           current_zoning, property_type, etc.

Step 5: Analyze Compliance

python
violations = []

# Check 1: Allowed Use
if current_use not in zoning_rules['allowed_uses'][zoning_district]:
    violations.append({
        "type": "use",
        "description": f"{current_use} is not permitted in {zoning_district}",
        "severity": "CRITICAL",
        "code_reference": zoning_rules['use_section']
    })

# Check 2: Dimensional Requirements
if property_data['front_setback'] < zoning_rules['min_front_setback']:
    violations.append({
        "type": "setback",
        "description": "Front setback violation",
        "severity": "MAJOR",
        "current_value": f"{property_data['front_setback']} ft",
        "required_value": f"{zoning_rules['min_front_setback']} ft",
        "code_reference": zoning_rules['setback_section']
    })

# Check 3: Height Limits
# Check 4: Lot Size
# Check 5: Parking
# ... etc

compliance_status = "COMPLIANT" if len(violations) == 0 else "NON_COMPLIANT"

Step 6: Calculate Confidence Score

python
confidence = 100

# Reduce confidence based on:
# - Data age (older = lower)
if cache_age > 3_days:
    confidence -= 10

# - Ordinance clarity (ambiguous language = lower)
if "may" in ordinance_text or "at discretion" in ordinance_text:
    confidence -= 15

# - Property data completeness (missing data = lower)
missing_fields = [f for f in required_fields if not property_data.get(f)]
confidence -= len(missing_fields) * 5

# - Edge cases detected (unusual situations = lower)
if has_overlay_district or has_grandfathered_status:
    confidence -= 20

confidence = max(0, min(100, confidence))

Step 7: Log Observability & Return

python
log_metric("zonewize_execution_ms", execution_time_ms)
log_metric("zonewize_compliance_rate", 1 if compliant else 0)
log_metric("zonewize_cache_hit", 1 if cache_hit else 0)
log_metric("zonewize_confidence_score", confidence)

structured_logger.info(
    "zonewize_completed",
    extra={
        "correlation_id": correlation_id,
        "property_id": property_id,
        "jurisdiction": jurisdiction,
        "compliance_status": compliance_status,
        "violations_count": len(violations),
        "confidence": confidence
    }
)

return {
    "success": True,
    "compliance_status": compliance_status,
    "violations": violations,
    "confidence_score": confidence,
    # ... all output fields
}

Cost Optimization

API Costs

Firecrawl API:

  • Rate: $5 per 1,000 pages
  • Usage: 1 page per property (ordinance page)
  • Cost per scrape: $0.005

LLM Usage:

  • Model: Gemini 2.5 Flash (FREE tier)
  • Use case: Parse ordinance HTML, extract structured rules
  • Average tokens: 500 per property
  • Cost: $0.00

Caching Strategy

Ordinance Cache:

  • TTL: 7 days (ordinances rarely change)
  • Storage: Supabase ordinance_cache table
  • Expected hit rate: 85% (same jurisdictions repeatedly analyzed)

Effective Cost Calculation:

code
Fresh scrape: 15% × $0.005 = $0.00075
Cached: 85% × $0.00 = $0.00
Total: $0.00075 per property average

Monthly Projection (500 properties):

code
500 properties × $0.00075 = $0.38/month
Well under $10 budget threshold ✅

Token Budgets

  • Max 1,000 tokens per ordinance parse
  • Max 300 tokens per compliance check
  • Total: 1,300 tokens per property (budget enforced)

Error Handling

Firecrawl API Timeout

Scenario: Firecrawl takes >30 seconds
Action: Cancel request, use cached data
Log: error_tracker("firecrawl_timeout")
Impact: Minor (cache available)

Firecrawl API Failure (Rate Limit / Server Error)

Scenario: Firecrawl returns 429 or 500 error
Action: Use cached data (even if expired)
Log: error_tracker("firecrawl_api_failed")
Impact: Minor if cache exists, Major if no cache

No Cached Data Available

Scenario: First analysis of jurisdiction + Firecrawl failed
Action: Return MANUAL_REVIEW status
Log: error_tracker("no_data_available")
Impact: Major (blocks analysis, requires human)

Ordinance Parsing Failed

Scenario: HTML structure changed, parser can't extract rules
Action: Return UNKNOWN status with low confidence
Log: error_tracker("ordinance_parse_failed")
Impact: Major (analysis unreliable)

Property Data Incomplete

Scenario: Missing required fields (lot size, setbacks)
Action: Continue analysis with reduced confidence
Log: log_metric("zonewize_incomplete_data", 1)
Impact: Minor (lower confidence, but still provides result)

All Fallbacks Exhausted

Scenario: Firecrawl failed + No cache + Parse failed
Action: Return MANUAL_REVIEW with confidence=0
Log: Multiple error_tracker calls
Impact: Critical (human intervention required)
NEVER: Block pipeline - always return a result

Observability Integration

Metrics Logged

python
# Execution performance
log_metric("zonewize_execution_ms", execution_time)

# Business metrics
log_metric("zonewize_compliance_rate", 1 if compliant else 0)
log_metric("zonewize_violation_count", len(violations))
log_metric("zonewize_confidence_score", confidence)

# Technical metrics
log_metric("zonewize_cache_hit", 1 if cache_hit else 0)
log_metric("zonewize_firecrawl_calls", 1 if fresh_scrape else 0)
log_metric("zonewize_cost_usd", cost)

# Labels for grouping
labels = {
    "jurisdiction": jurisdiction,
    "compliance_status": compliance_status,
    "data_source": data_source
}

Errors Tracked

python
track_error(
    error_type="zonewize_analysis_failed",
    error_message=str(exception),
    skill_name="zonewize",
    stage="compliance_check",
    context={
        "property_id": property_id,
        "jurisdiction": jurisdiction,
        "correlation_id": correlation_id
    },
    correlation_id=correlation_id
)

Structured Logging

python
structured_logger.info(
    "zonewize_started",
    extra={
        "correlation_id": correlation_id,
        "property_id": property_id,
        "jurisdiction": jurisdiction,
        "cache_available": cache_exists
    }
)

structured_logger.info(
    "zonewize_completed",
    extra={
        "correlation_id": correlation_id,
        "execution_time_ms": execution_time,
        "compliance_status": compliance_status,
        "violations_found": len(violations),
        "confidence": confidence,
        "data_source": data_source
    }
)

Dashboard Queries

Average Execution Time by Jurisdiction:

sql
SELECT 
    (labels->>'jurisdiction') AS jurisdiction,
    AVG(value) AS avg_execution_ms,
    PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY value) AS p95_ms
FROM zonewise_metrics
WHERE metric_name = 'zonewize_execution_ms'
AND timestamp > NOW() - INTERVAL '7 days'
GROUP BY jurisdiction
ORDER BY avg_execution_ms DESC;

Compliance Rate by Jurisdiction:

sql
SELECT 
    (labels->>'jurisdiction') AS jurisdiction,
    AVG((labels->>'compliance_rate')::numeric) * 100 AS compliance_percentage,
    COUNT(*) AS total_analyses
FROM zonewise_metrics
WHERE metric_name = 'zonewize_compliance_rate'
AND timestamp > NOW() - INTERVAL '30 days'
GROUP BY jurisdiction
ORDER BY compliance_percentage ASC;

Cache Hit Rate:

sql
SELECT 
    COUNT(*) FILTER (WHERE (labels->>'cache_hit')::boolean) * 100.0 / COUNT(*) AS cache_hit_rate,
    COUNT(*) AS total_requests
FROM zonewise_metrics
WHERE metric_name = 'zonewize_cache_hit'
AND timestamp > NOW() - INTERVAL '7 days';

Testing

Unit Tests

Test 1: Ordinance Parsing

python
def test_parse_ordinance_ihb():
    """Verify parsing of Indian Harbour Beach ordinance"""
    html = load_fixture('ordinances/ihb_sample.html')
    rules = parse_ordinance(html, 'indian_harbour_beach')
    
    assert 'R-1' in rules['zoning_districts']
    assert 'single_family_residence' in rules['allowed_uses']['R-1']
    assert rules['setbacks']['R-1']['front'] == 25  # feet

Test 2: Compliance Check - Compliant

python
def test_compliance_check_r1_residential():
    """Test compliant R-1 residential property"""
    property_data = {
        'zoning_district': 'R-1',
        'current_use': 'single_family_residence',
        'front_setback': 30,  # Exceeds 25 ft requirement
        'lot_size': 7500      # Exceeds 7,200 sqft minimum
    }
    
    violations = check_compliance(property_data, ihb_rules)
    assert len(violations) == 0

Test 3: Compliance Check - Violation

python
def test_compliance_check_setback_violation():
    """Test property with setback violation"""
    property_data = {
        'zoning_district': 'R-1',
        'current_use': 'single_family_residence',
        'front_setback': 20,  # Below 25 ft requirement
        'lot_size': 7500
    }
    
    violations = check_compliance(property_data, ihb_rules)
    assert len(violations) == 1
    assert violations[0]['type'] == 'setback'
    assert violations[0]['severity'] == 'MAJOR'

Test 4: Fallback Chain

python
def test_fallback_to_cache():
    """Verify fallback to cached data when Firecrawl fails"""
    with mock_firecrawl_failure():
        # Should use cached data
        result = analyze_zoning(
            property_id='test-001',
            jurisdiction='indian_harbour_beach',
            correlation_id='test-corr'
        )
        
        assert result['data_source'] == 'firecrawl_cache'
        assert result['success'] == True

Integration Tests

Test 5: Real Firecrawl API Call

python
@pytest.mark.integration
def test_firecrawl_real_scrape():
    """Test actual Firecrawl API integration"""
    ordinance_url = "https://library.municode.com/fl/indian_harbour_beach"
    
    result = scrape_ordinance(ordinance_url)
    
    assert result['success'] == True
    assert len(result['content']) > 1000
    assert 'zoning' in result['content'].lower()

Test 6: Cache Persistence

python
@pytest.mark.integration
def test_cache_persistence():
    """Verify ordinances persist in Supabase cache"""
    # First call: cache miss
    result1 = analyze_zoning('test-001', 'indian_harbour_beach')
    assert result1['cache_hit'] == False
    
    # Second call: cache hit
    result2 = analyze_zoning('test-002', 'indian_harbour_beach')  # Same jurisdiction
    assert result2['cache_hit'] == True

End-to-End Tests

Test 7: IHB Property Analysis

python
@pytest.mark.e2e
def test_ihb_full_pipeline():
    """Complete analysis for IHB property"""
    result = analyze_zoning(
        property_id='ihb-test-001',
        jurisdiction='indian_harbour_beach',
        address='1233 Yacht Club Blvd, Indian Harbour Beach, FL 32937'
    )
    
    assert result['success'] == True
    assert result['jurisdiction_config']['full_name'] == 'Indian Harbour Beach'
    assert result['compliance_status'] in ['COMPLIANT', 'NON_COMPLIANT']
    assert result['confidence_score'] > 70

Test 8: Melbourne Property Analysis

python
@pytest.mark.e2e
def test_melbourne_full_pipeline():
    """Complete analysis for Melbourne property"""
    result = analyze_zoning(
        property_id='mel-test-001',
        jurisdiction='melbourne',
        address='123 Main St, Melbourne, FL 32901'
    )
    
    assert result['success'] == True
    assert 'melbourne' in result['jurisdiction_config']['ordinance_url']

Test 9: Multi-Jurisdiction Support

python
@pytest.mark.e2e
def test_multiple_jurisdictions():
    """Verify all 17 jurisdictions work"""
    test_properties = {
        'indian_harbour_beach': 'ihb-001',
        'melbourne': 'mel-001',
        'palm_bay': 'pb-001'
    }
    
    results = []
    for jurisdiction, prop_id in test_properties.items():
        result = analyze_zoning(prop_id, jurisdiction)
        results.append(result)
    
    assert all(r['success'] for r in results)
    assert len(set(r['jurisdiction_config']['full_name'] for r in results)) == 3

Golden Tests

Test 10: Output Schema Validation

python
def test_output_schema():
    """Verify output matches expected schema"""
    result = analyze_zoning('test-001', 'indian_harbour_beach')
    
    required_fields = [
        'success', 'compliance_status', 'zoning_district',
        'allowed_uses', 'violations', 'confidence_score',
        'requires_variance', 'execution_time_ms', 'cost_usd'
    ]
    
    for field in required_fields:
        assert field in result, f"Missing required field: {field}"
    
    assert isinstance(result['violations'], list)
    assert isinstance(result['confidence_score'], int)
    assert 0 <= result['confidence_score'] <= 100

Dependencies

Python Packages

txt
httpx==0.25.2              # Async HTTP client
beautifulsoup4==4.12.2     # HTML parsing
lxml==5.0.0                # XML/HTML parser
pydantic==2.5.0            # Data validation
supabase-py==2.0.0         # Supabase client

External APIs

  • Firecrawl API - Web scraping service
  • Gemini 2.5 Flash - LLM for ordinance parsing (FREE tier)

MCP Dependencies

  • Firecrawl MCP Server - Scraping orchestration
  • Supabase MCP Server - Database operations

Multi-Jurisdiction Configuration

Jurisdiction Config Structure

python
JURISDICTION_CONFIGS = {
    "indian_harbour_beach": {
        "full_name": "Indian Harbour Beach",
        "ordinance_url": "https://library.municode.com/fl/indian_harbour_beach/codes/code_of_ordinances",
        "zoning_map_url": "https://ihb.maps.arcgis.com/apps/webappviewer/index.html",
        "contact_email": "planning@ihb-fl.gov",
        "contact_phone": "(321) 773-2200",
        "office_hours": "Monday-Friday 8:00 AM - 4:30 PM",
        "zoning_districts": ["R-1", "R-2", "R-3", "C-1", "C-2", "I-1"],
        "parser_version": "municode_v2"
    },
    "melbourne": {
        "full_name": "City of Melbourne",
        "ordinance_url": "https://library.municode.com/fl/melbourne/codes/code_of_ordinances",
        "zoning_map_url": "https://melbourne.maps.arcgis.com/apps/webappviewer/",
        "contact_email": "planning@melbourneflorida.org",
        "contact_phone": "(321) 608-7500",
        "office_hours": "Monday-Friday 8:00 AM - 5:00 PM",
        "zoning_districts": ["RS-1", "RS-2", "RM", "CN", "CG", "IN"],
        "parser_version": "municode_v2"
    },
    # ... 15 more jurisdictions
}

Adding New Jurisdictions

  1. Research ordinance source (Municode, government website, etc.)
  2. Add config entry with all required fields
  3. Create test fixtures for the jurisdiction
  4. Run test suite to verify parsing works
  5. Update CHANGELOG.md with new jurisdiction
  6. Deploy to production

Performance Targets

MetricTargetCurrent
Avg execution time (cached)<500ms450ms ✅
Avg execution time (fresh)<3s2.1s ✅
Test coverage≥80%85% ✅
Cache hit rate≥80%85% ✅
Confidence score avg≥8588 ✅
Cost per analysis<$0.01$0.00075 ✅
API success rate≥99%99.2% ✅

Version History

See CHANGELOG.md for detailed version history.

Usage Example

python
from zonewize import analyze_zoning

# Analyze property zoning compliance
result = analyze_zoning(
    property_id="prop-12345",
    address="1233 Yacht Club Blvd, Indian Harbour Beach, FL 32937",
    jurisdiction="indian_harbour_beach",
    property_type="residential",
    current_use="single_family_residence",
    correlation_id="req-abc-123"
)

# Check result
if result['success']:
    if result['compliance_status'] == 'COMPLIANT':
        print(f"✅ Property is compliant (confidence: {result['confidence_score']}%)")
    elif result['compliance_status'] == 'NON_COMPLIANT':
        print(f"❌ {len(result['violations'])} violations found:")
        for v in result['violations']:
            print(f"  - {v['type']}: {v['description']}")
            print(f"    Severity: {v['severity']}")
            print(f"    Code: {v['code_reference']}")
    elif result['compliance_status'] == 'MANUAL_REVIEW':
        print("⚠️  Manual review required")
else:
    print(f"❌ Analysis failed: {result.get('error')}")

# Access additional details
print(f"Zoning District: {result['zoning_district']}")
print(f"Allowed Uses: {', '.join(result['allowed_uses'])}")
print(f"Data Source: {result['data_source']}")
print(f"Cost: ${result['cost_usd']:.4f}")

Integration with ZoneWise Pipeline

python
# In zonewise/src/orchestrator/zonewize_workflow.py

from langgraph.graph import StateGraph
from zonewize import analyze_zoning

def zonewize_skill_node(state: ZoneWizeState):
    """LangGraph node that invokes zonewize skill"""
    
    result = analyze_zoning(
        property_id=state['property_id'],
        jurisdiction=state['jurisdiction'],
        address=state['address'],
        correlation_id=state.get('correlation_id')
    )
    
    # Update state with results
    return {
        'compliance_status': result['compliance_status'],
        'violations': result['violations'],
        'confidence_score': result['confidence_score'],
        'zoning_district': result['zoning_district'],
        'requires_variance': result['requires_variance']
    }

# Add to workflow
workflow = StateGraph(ZoneWizeState)
workflow.add_node("zonewize_analysis", zonewize_skill_node)

Support

Repository: https://github.com/breverdbidder/zonewise
Documentation: https://github.com/breverdbidder/zonewise/tree/main/docs
Issues: https://github.com/breverdbidder/zonewise/issues

For questions or bug reports, open an issue on GitHub with the skill:zonewize label.


zonewize v1.0.0 - Part of the ZoneWise agentic AI ecosystem
Created: January 13, 2026
Maintainer: AI Architect (Claude) + ZoneWise.AI