APM

>Agent Skill

@datadrivenconstruction/as-built-documentation

skilldevelopment

Automate as-built documentation and digital handover for construction. Compile project records, generate O&M manuals, create asset databases, and ensure complete project closeout.

apm::install
$apm install @datadrivenconstruction/as-built-documentation
apm::skill.md
---
name: "as-built-documentation"
description: "Automate as-built documentation and digital handover for construction. Compile project records, generate O&M manuals, create asset databases, and ensure complete project closeout."
homepage: "https://datadrivenconstruction.io"
metadata: {"openclaw": {"emoji": "🚀", "os": ["win32"], "homepage": "https://datadrivenconstruction.io", "requires": {"bins": ["python3"]}}}
---
# As-Built Documentation

## Overview

This skill implements automated as-built documentation and digital handover for construction projects. Compile accurate records, generate operation manuals, and ensure complete documentation for facility management.

**Capabilities:**
- As-built drawing management
- O&M manual generation
- Asset data compilation
- Warranty tracking
- Document organization
- BIM-to-FM handover

## Quick Start

```python
from dataclasses import dataclass, field
from datetime import date, datetime
from typing import List, Dict, Optional
from enum import Enum

class DocumentType(Enum):
    DRAWING = "drawing"
    SPECIFICATION = "specification"
    SUBMITTAL = "submittal"
    WARRANTY = "warranty"
    CERTIFICATE = "certificate"
    MANUAL = "manual"
    TEST_REPORT = "test_report"
    COMMISSIONING = "commissioning"
    PHOTO = "photo"

class DocumentStatus(Enum):
    DRAFT = "draft"
    REVIEWED = "reviewed"
    APPROVED = "approved"
    AS_BUILT = "as_built"
    FINAL = "final"

@dataclass
class HandoverDocument:
    doc_id: str
    doc_type: DocumentType
    title: str
    file_path: str
    version: str
    status: DocumentStatus
    system: str  # Building system (HVAC, Electrical, etc.)
    uploaded_date: date
    approved_by: str = ""

@dataclass
class AssetRecord:
    asset_id: str
    asset_name: str
    asset_type: str
    manufacturer: str
    model: str
    serial_number: str
    location: str
    install_date: date
    warranty_end: date
    documents: List[str] = field(default_factory=list)

def check_handover_completeness(documents: List[HandoverDocument],
                                required_types: List[DocumentType]) -> Dict:
    """Check if all required documents are present"""
    present_types = {doc.doc_type for doc in documents if doc.status == DocumentStatus.FINAL}
    missing = set(required_types) - present_types

    return {
        'complete': len(missing) == 0,
        'total_required': len(required_types),
        'total_present': len(present_types),
        'missing_types': [t.value for t in missing]
    }

# Example
documents = [
    HandoverDocument("DOC-001", DocumentType.DRAWING, "Floor Plans As-Built",
                    "/docs/floorplans.pdf", "3.0", DocumentStatus.FINAL, "Architecture", date.today()),
    HandoverDocument("DOC-002", DocumentType.MANUAL, "HVAC O&M Manual",
                    "/docs/hvac_om.pdf", "1.0", DocumentStatus.FINAL, "HVAC", date.today()),
]

required = [DocumentType.DRAWING, DocumentType.MANUAL, DocumentType.WARRANTY]
status = check_handover_completeness(documents, required)
print(f"Complete: {status['complete']}, Missing: {status['missing_types']}")
```

## Comprehensive Handover System

### Document Management

```python
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
from typing import List, Dict, Optional, Tuple
from enum import Enum
import uuid
import os

class BuildingSystem(Enum):
    ARCHITECTURAL = "architectural"
    STRUCTURAL = "structural"
    MECHANICAL = "mechanical"
    ELECTRICAL = "electrical"
    PLUMBING = "plumbing"
    FIRE_PROTECTION = "fire_protection"
    CONTROLS = "controls"
    ELEVATOR = "elevator"
    CIVIL = "civil"
    LANDSCAPE = "landscape"

@dataclass
class DocumentRequirement:
    requirement_id: str
    doc_type: DocumentType
    system: BuildingSystem
    description: str
    is_mandatory: bool = True
    quantity: int = 1  # How many documents of this type needed
    format_requirements: str = "PDF"

@dataclass
class ProjectDocument:
    doc_id: str
    requirement_id: Optional[str]
    doc_type: DocumentType
    system: BuildingSystem
    title: str
    description: str
    file_path: str
    file_size_mb: float
    format: str
    version: str
    revision_date: date
    author: str
    reviewer: str
    status: DocumentStatus
    metadata: Dict = field(default_factory=dict)
    related_assets: List[str] = field(default_factory=list)
    supersedes: Optional[str] = None  # Previous version doc_id

class DocumentManager:
    """Manage project handover documents"""

    STANDARD_REQUIREMENTS = {
        BuildingSystem.MECHANICAL: [
            DocumentRequirement("REQ-M01", DocumentType.DRAWING, BuildingSystem.MECHANICAL,
                              "HVAC As-Built Drawings"),
            DocumentRequirement("REQ-M02", DocumentType.MANUAL, BuildingSystem.MECHANICAL,
                              "HVAC Operation & Maintenance Manual"),
            DocumentRequirement("REQ-M03", DocumentType.WARRANTY, BuildingSystem.MECHANICAL,
                              "HVAC Equipment Warranties"),
            DocumentRequirement("REQ-M04", DocumentType.TEST_REPORT, BuildingSystem.MECHANICAL,
                              "TAB Report - Testing, Adjusting, Balancing"),
            DocumentRequirement("REQ-M05", DocumentType.COMMISSIONING, BuildingSystem.MECHANICAL,
                              "Commissioning Report"),
        ],
        BuildingSystem.ELECTRICAL: [
            DocumentRequirement("REQ-E01", DocumentType.DRAWING, BuildingSystem.ELECTRICAL,
                              "Electrical As-Built Drawings"),
            DocumentRequirement("REQ-E02", DocumentType.MANUAL, BuildingSystem.ELECTRICAL,
                              "Electrical O&M Manual"),
            DocumentRequirement("REQ-E03", DocumentType.WARRANTY, BuildingSystem.ELECTRICAL,
                              "Electrical Equipment Warranties"),
            DocumentRequirement("REQ-E04", DocumentType.TEST_REPORT, BuildingSystem.ELECTRICAL,
                              "Electrical Test Reports"),
            DocumentRequirement("REQ-E05", DocumentType.CERTIFICATE, BuildingSystem.ELECTRICAL,
                              "Electrical Inspection Certificate"),
        ],
        BuildingSystem.PLUMBING: [
            DocumentRequirement("REQ-P01", DocumentType.DRAWING, BuildingSystem.PLUMBING,
                              "Plumbing As-Built Drawings"),
            DocumentRequirement("REQ-P02", DocumentType.MANUAL, BuildingSystem.PLUMBING,
                              "Plumbing O&M Manual"),
            DocumentRequirement("REQ-P03", DocumentType.TEST_REPORT, BuildingSystem.PLUMBING,
                              "Pressure Test Reports"),
        ],
        BuildingSystem.FIRE_PROTECTION: [
            DocumentRequirement("REQ-F01", DocumentType.DRAWING, BuildingSystem.FIRE_PROTECTION,
                              "Fire Protection As-Built Drawings"),
            DocumentRequirement("REQ-F02", DocumentType.MANUAL, BuildingSystem.FIRE_PROTECTION,
                              "Fire Systems O&M Manual"),
            DocumentRequirement("REQ-F03", DocumentType.CERTIFICATE, BuildingSystem.FIRE_PROTECTION,
                              "Fire Marshal Approval"),
            DocumentRequirement("REQ-F04", DocumentType.TEST_REPORT, BuildingSystem.FIRE_PROTECTION,
                              "Fire Alarm Acceptance Test"),
        ],
        BuildingSystem.ARCHITECTURAL: [
            DocumentRequirement("REQ-A01", DocumentType.DRAWING, BuildingSystem.ARCHITECTURAL,
                              "Architectural As-Built Drawings"),
            DocumentRequirement("REQ-A02", DocumentType.SPECIFICATION, BuildingSystem.ARCHITECTURAL,
                              "Finish Schedule"),
            DocumentRequirement("REQ-A03", DocumentType.WARRANTY, BuildingSystem.ARCHITECTURAL,
                              "Roofing Warranty"),
        ]
    }

    def __init__(self, project_id: str, project_name: str):
        self.project_id = project_id
        self.project_name = project_name
        self.requirements: Dict[str, DocumentRequirement] = {}
        self.documents: Dict[str, ProjectDocument] = {}
        self._load_standard_requirements()

    def _load_standard_requirements(self):
        """Load standard document requirements"""
        for system, reqs in self.STANDARD_REQUIREMENTS.items():
            for req in reqs:
                self.requirements[req.requirement_id] = req

    def add_requirement(self, requirement: DocumentRequirement):
        """Add custom requirement"""
        self.requirements[requirement.requirement_id] = requirement

    def upload_document(self, doc_type: DocumentType, system: BuildingSystem,
                       title: str, file_path: str, author: str,
                       requirement_id: str = None,
                       related_assets: List[str] = None) -> ProjectDocument:
        """Upload new document"""
        doc_id = f"DOC-{uuid.uuid4().hex[:8].upper()}"

        # Get file info
        file_size = os.path.getsize(file_path) / (1024 * 1024) if os.path.exists(file_path) else 0
        file_format = os.path.splitext(file_path)[1].upper().replace('.', '')

        doc = ProjectDocument(
            doc_id=doc_id,
            requirement_id=requirement_id,
            doc_type=doc_type,
            system=system,
            title=title,
            description="",
            file_path=file_path,
            file_size_mb=file_size,
            format=file_format,
            version="1.0",
            revision_date=date.today(),
            author=author,
            reviewer="",
            status=DocumentStatus.DRAFT,
            related_assets=related_assets or []
        )

        self.documents[doc_id] = doc
        return doc

    def approve_document(self, doc_id: str, reviewer: str) -> bool:
        """Approve document"""
        doc = self.documents.get(doc_id)
        if doc:
            doc.status = DocumentStatus.APPROVED
            doc.reviewer = reviewer
            return True
        return False

    def finalize_document(self, doc_id: str) -> bool:
        """Finalize document for handover"""
        doc = self.documents.get(doc_id)
        if doc and doc.status == DocumentStatus.APPROVED:
            doc.status = DocumentStatus.FINAL
            return True
        return False

    def get_status_summary(self) -> Dict:
        """Get document status summary"""
        summary = {
            'total_requirements': len(self.requirements),
            'total_documents': len(self.documents),
            'by_status': {},
            'by_system': {},
            'completion': {}
        }

        # Count by status
        for doc in self.documents.values():
            status = doc.status.value
            summary['by_status'][status] = summary['by_status'].get(status, 0) + 1

            system = doc.system.value
            if system not in summary['by_system']:
                summary['by_system'][system] = {'uploaded': 0, 'final': 0}
            summary['by_system'][system]['uploaded'] += 1
            if doc.status == DocumentStatus.FINAL:
                summary['by_system'][system]['final'] += 1

        # Check completion against requirements
        for req_id, req in self.requirements.items():
            matching_docs = [
                d for d in self.documents.values()
                if d.requirement_id == req_id and d.status == DocumentStatus.FINAL
            ]

            summary['completion'][req_id] = {
                'description': req.description,
                'system': req.system.value,
                'required': req.quantity,
                'submitted': len(matching_docs),
                'complete': len(matching_docs) >= req.quantity
            }

        return summary

    def get_missing_documents(self) -> List[DocumentRequirement]:
        """Get list of missing required documents"""
        missing = []

        for req_id, req in self.requirements.items():
            if not req.is_mandatory:
                continue

            matching_docs = [
                d for d in self.documents.values()
                if d.requirement_id == req_id and d.status == DocumentStatus.FINAL
            ]

            if len(matching_docs) < req.quantity:
                missing.append(req)

        return missing
```

### Asset Registry

```python
from datetime import date, timedelta
from typing import List, Dict, Optional

@dataclass
class MaintenanceSchedule:
    schedule_id: str
    task_description: str
    frequency: str  # daily, weekly, monthly, quarterly, annual
    next_due: date
    responsible_party: str
    estimated_duration_hours: float

@dataclass
class AssetDetails:
    asset_id: str
    asset_tag: str
    asset_name: str
    asset_type: str
    category: str  # equipment, fixture, system

    # Identification
    manufacturer: str
    model_number: str
    serial_number: str
    part_number: str = ""

    # Location
    building: str = ""
    floor: str = ""
    room: str = ""
    coordinates: Tuple[float, float, float] = (0, 0, 0)

    # Installation
    install_date: date = None
    installed_by: str = ""
    cost: float = 0

    # Warranty
    warranty_start: date = None
    warranty_end: date = None
    warranty_provider: str = ""
    warranty_terms: str = ""

    # Specifications
    specifications: Dict = field(default_factory=dict)

    # Documents
    manuals: List[str] = field(default_factory=list)
    drawings: List[str] = field(default_factory=list)
    photos: List[str] = field(default_factory=list)

    # Maintenance
    maintenance_schedules: List[MaintenanceSchedule] = field(default_factory=list)
    service_contacts: List[Dict] = field(default_factory=list)

    # BIM reference
    ifc_guid: str = ""
    revit_element_id: str = ""

class AssetRegistry:
    """Manage building assets for handover"""

    def __init__(self, project_id: str):
        self.project_id = project_id
        self.assets: Dict[str, AssetDetails] = {}

    def register_asset(self, asset: AssetDetails):
        """Register new asset"""
        self.assets[asset.asset_id] = asset

    def import_from_bim(self, bim_data: List[Dict]):
        """Import assets from BIM model"""
        for item in bim_data:
            asset = AssetDetails(
                asset_id=f"AST-{uuid.uuid4().hex[:8].upper()}",
                asset_tag=item.get('tag', ''),
                asset_name=item.get('name', ''),
                asset_type=item.get('type', ''),
                category=item.get('category', 'equipment'),
                manufacturer=item.get('manufacturer', ''),
                model_number=item.get('model', ''),
                serial_number=item.get('serial', ''),
                building=item.get('building', ''),
                floor=item.get('floor', ''),
                room=item.get('room', ''),
                ifc_guid=item.get('ifc_guid', ''),
                revit_element_id=item.get('revit_id', '')
            )

            # Add specifications
            for key, value in item.get('parameters', {}).items():
                asset.specifications[key] = value

            self.assets[asset.asset_id] = asset

    def add_warranty(self, asset_id: str, start_date: date,
                    duration_years: int, provider: str, terms: str = ""):
        """Add warranty information"""
        asset = self.assets.get(asset_id)
        if asset:
            asset.warranty_start = start_date
            asset.warranty_end = start_date + timedelta(days=365 * duration_years)
            asset.warranty_provider = provider
            asset.warranty_terms = terms

    def add_maintenance_schedule(self, asset_id: str,
                                 schedule: MaintenanceSchedule):
        """Add maintenance schedule"""
        asset = self.assets.get(asset_id)
        if asset:
            asset.maintenance_schedules.append(schedule)

    def get_warranty_report(self) -> Dict:
        """Get warranty status report"""
        today = date.today()

        report = {
            'total_assets': len(self.assets),
            'with_warranty': 0,
            'active_warranties': 0,
            'expiring_soon': [],  # Within 90 days
            'expired': []
        }

        for asset in self.assets.values():
            if asset.warranty_end:
                report['with_warranty'] += 1

                if asset.warranty_end >= today:
                    report['active_warranties'] += 1

                    days_remaining = (asset.warranty_end - today).days
                    if days_remaining <= 90:
                        report['expiring_soon'].append({
                            'asset_id': asset.asset_id,
                            'asset_name': asset.asset_name,
                            'warranty_end': asset.warranty_end.isoformat(),
                            'days_remaining': days_remaining
                        })
                else:
                    report['expired'].append({
                        'asset_id': asset.asset_id,
                        'asset_name': asset.asset_name,
                        'warranty_end': asset.warranty_end.isoformat()
                    })

        return report

    def export_to_cmms(self) -> List[Dict]:
        """Export assets for CMMS import"""
        export_data = []

        for asset in self.assets.values():
            export_data.append({
                'asset_id': asset.asset_id,
                'asset_tag': asset.asset_tag,
                'name': asset.asset_name,
                'type': asset.asset_type,
                'category': asset.category,
                'manufacturer': asset.manufacturer,
                'model': asset.model_number,
                'serial': asset.serial_number,
                'location': f"{asset.building}/{asset.floor}/{asset.room}",
                'install_date': asset.install_date.isoformat() if asset.install_date else '',
                'warranty_end': asset.warranty_end.isoformat() if asset.warranty_end else '',
                'specifications': asset.specifications
            })

        return export_data
```

### O&M Manual Generator

```python
class OMManualGenerator:
    """Generate O&M manuals from project data"""

    def __init__(self, doc_manager: DocumentManager, asset_registry: AssetRegistry):
        self.docs = doc_manager
        self.assets = asset_registry

    def generate_system_manual(self, system: BuildingSystem,
                              output_path: str) -> Dict:
        """Generate O&M manual for building system"""
        # Collect system documents
        system_docs = [
            d for d in self.docs.documents.values()
            if d.system == system and d.status == DocumentStatus.FINAL
        ]

        # Collect system assets
        system_assets = [
            a for a in self.assets.assets.values()
            if a.asset_type.lower() in system.value.lower() or
               system.value.lower() in a.category.lower()
        ]

        manual_content = {
            'system': system.value,
            'generated_date': date.today().isoformat(),
            'sections': []
        }

        # Section 1: System Overview
        manual_content['sections'].append({
            'title': 'System Overview',
            'content': f"Overview of {system.value} system",
            'subsections': []
        })

        # Section 2: Equipment List
        equipment_list = []
        for asset in system_assets:
            equipment_list.append({
                'tag': asset.asset_tag,
                'name': asset.asset_name,
                'manufacturer': asset.manufacturer,
                'model': asset.model_number,
                'location': f"{asset.building}/{asset.floor}/{asset.room}"
            })

        manual_content['sections'].append({
            'title': 'Equipment Schedule',
            'equipment': equipment_list
        })

        # Section 3: Operation Procedures
        manual_content['sections'].append({
            'title': 'Operation Procedures',
            'content': 'Standard operating procedures',
            'reference_docs': [d.doc_id for d in system_docs if d.doc_type == DocumentType.MANUAL]
        })

        # Section 4: Maintenance Requirements
        maintenance_tasks = []
        for asset in system_assets:
            for schedule in asset.maintenance_schedules:
                maintenance_tasks.append({
                    'asset': asset.asset_name,
                    'task': schedule.task_description,
                    'frequency': schedule.frequency,
                    'duration': schedule.estimated_duration_hours
                })

        manual_content['sections'].append({
            'title': 'Preventive Maintenance',
            'tasks': maintenance_tasks
        })

        # Section 5: Warranty Information
        warranties = []
        for asset in system_assets:
            if asset.warranty_end:
                warranties.append({
                    'asset': asset.asset_name,
                    'provider': asset.warranty_provider,
                    'expires': asset.warranty_end.isoformat(),
                    'terms': asset.warranty_terms
                })

        manual_content['sections'].append({
            'title': 'Warranty Information',
            'warranties': warranties
        })

        # Section 6: Service Contacts
        contacts = []
        for asset in system_assets:
            contacts.extend(asset.service_contacts)

        manual_content['sections'].append({
            'title': 'Service Contacts',
            'contacts': list({c['name']: c for c in contacts}.values())
        })

        # Section 7: Reference Documents
        manual_content['sections'].append({
            'title': 'Reference Documents',
            'documents': [
                {'id': d.doc_id, 'title': d.title, 'type': d.doc_type.value}
                for d in system_docs
            ]
        })

        return manual_content

    def generate_building_manual(self, output_path: str) -> Dict:
        """Generate complete building O&M manual"""
        building_manual = {
            'project': self.docs.project_name,
            'generated': date.today().isoformat(),
            'systems': {}
        }

        for system in BuildingSystem:
            building_manual['systems'][system.value] = self.generate_system_manual(
                system, output_path
            )

        return building_manual
```

### Handover Checklist

```python
def generate_handover_checklist(doc_manager: DocumentManager,
                               asset_registry: AssetRegistry) -> Dict:
    """Generate comprehensive handover checklist"""
    checklist = {
        'generated': date.today().isoformat(),
        'project': doc_manager.project_name,
        'overall_status': 'incomplete',
        'categories': []
    }

    # Documents category
    doc_status = doc_manager.get_status_summary()
    missing_docs = doc_manager.get_missing_documents()

    checklist['categories'].append({
        'name': 'Documents',
        'complete': len(missing_docs) == 0,
        'items': [
            {
                'item': req.description,
                'system': req.system.value,
                'status': 'complete' if req.requirement_id not in [m.requirement_id for m in missing_docs] else 'missing'
            }
            for req in doc_manager.requirements.values()
        ]
    })

    # Assets category
    warranty_report = asset_registry.get_warranty_report()

    checklist['categories'].append({
        'name': 'Asset Registration',
        'complete': warranty_report['total_assets'] > 0,
        'items': [
            {'item': 'All major equipment registered', 'status': 'complete' if warranty_report['total_assets'] > 0 else 'incomplete'},
            {'item': 'Warranty information entered', 'status': 'complete' if warranty_report['with_warranty'] > 0 else 'incomplete'},
            {'item': 'Maintenance schedules defined', 'status': 'complete' if any(a.maintenance_schedules for a in asset_registry.assets.values()) else 'incomplete'}
        ]
    })

    # Training category
    checklist['categories'].append({
        'name': 'Training',
        'complete': False,
        'items': [
            {'item': 'Operations staff training completed', 'status': 'pending'},
            {'item': 'Maintenance staff training completed', 'status': 'pending'},
            {'item': 'Safety systems training completed', 'status': 'pending'}
        ]
    })

    # Determine overall status
    all_complete = all(cat['complete'] for cat in checklist['categories'])
    checklist['overall_status'] = 'complete' if all_complete else 'incomplete'

    return checklist
```

## Quick Reference

| Document Type | Typical Source | When Required |
|---------------|---------------|---------------|
| As-Built Drawings | Contractor | Substantial completion |
| O&M Manuals | Manufacturer/Contractor | Before training |
| Warranties | Manufacturers | At installation |
| Test Reports | Testing agency | After testing |
| Certificates | Authorities | Final inspection |
| Training Records | Contractor | Before handover |

## Resources

- **COBie Standard**: Construction Operations Building Information Exchange
- **ASHRAE Guideline 0**: Commissioning process
- **IFMA**: Facility Management resources
- **DDC Website**: https://datadrivenconstruction.io

## Next Steps

- See `bim-validation-pipeline` for BIM handover
- See `document-classification-nlp` for document processing
- See `digital-twin-sync` for FM integration