← All white papers

Energy & Industrial

ADL v0.3 for Energy & Industrial

Asset-integrity, inspection, management-of-change, and work-order ADL domains for energy and industrial operations, built to the ADL v0.3 specification.

Industrial operations carry their risk in state transitions: a pressure vessel inspected past its due date, a temporary management-of-change that quietly became permanent, a work order started without a valid permit. This paper works the core energy and industrial domains in the Application Definition Language (ADL) as complete, runnable definitions: asset integrity and inspection, management of change, and an edge/IoT gateway domain.

The emphasis throughout is on making safety-critical rules blocking rather than advisory, and on making time-based conditions (an inspection coming due, a temporary change expiring) fire on their own through scheduled triggers instead of waiting for someone to notice. Each domain below shows how those guarantees are declared in ADL and enforced by the Kalos runtime.

Domain 1: Asset Integrity (assets)

Asset, Inspection, and Workorder share one domain: the workflows that close the inspection→asset loop and the cross-resource guards require co-residence.

domain:
  name: assets
  version: "1.0.0"
  description: >
    Asset register, inspection management, and work orders. Inspection results
    flow back to the asset via workflow — corrosion rate and wall thickness on
    the asset are maintained by the system, not by manual transcription.
    Safety-critical checks (permits, LOTO) BLOCK work, they do not warn.

  channels:
    email:
      provider: smtp
      host: ${SMTP_HOST}
      port: ${SMTP_PORT:587}
      from: "integrity-noreply@example.com"

  objects:
    Timestamp:
      createdAt: { type: datetime, autoNow: create }
      updatedAt: { type: datetime, autoNow: update }

  enums:
    AssetStatus:
      values:
        commissioning:  { label: "Commissioning",     color: "#3b82f6" }
        operational:    { label: "Operational",        color: "#10b981" }
        degraded:       { label: "Degraded",           color: "#f59e0b" }
        out_of_service: { label: "Out of Service",     color: "#ef4444" }
        maintenance:    { label: "Under Maintenance",  color: "#8b5cf6" }
        decommissioned: { label: "Decommissioned",     color: "#64748b", final: true }
    AssetCriticality:
      values:
        critical: { label: "Critical", description: "Failure causes safety hazard or system shutdown", color: "#ef4444" }
        high:     { label: "High",     description: "Failure causes significant production loss",      color: "#f97316" }
        medium:   { label: "Medium",   description: "Failure causes moderate operational impact",      color: "#f59e0b" }
        low:      { label: "Low",      description: "Failure causes minimal impact",                   color: "#94a3b8" }
    InspectionType:
      values:
        api_510:       { label: "API 510 - Pressure Vessel" }
        api_570:       { label: "API 570 - Piping" }
        api_653:       { label: "API 653 - Tank" }
        visual:        { label: "Visual" }
        ut_thickness:  { label: "UT Thickness" }
        vibration:     { label: "Vibration Analysis" }
        thermographic: { label: "Thermographic" }
        emergency:     { label: "Emergency" }
    InspectionStatus:
      values:
        scheduled:   { label: "Scheduled",   color: "#94a3b8" }
        in_progress: { label: "In Progress", color: "#3b82f6" }
        completed:   { label: "Completed",   color: "#8b5cf6" }
        reviewed:    { label: "Reviewed",    color: "#06b6d4" }
        approved:    { label: "Approved",    color: "#10b981", final: true }
        cancelled:   { label: "Cancelled",   color: "#64748b", final: true }
    FindingSeverity:
      values:
        none:             { label: "No Findings",        color: "#10b981" }
        minor:            { label: "Minor",              color: "#94a3b8" }
        moderate:         { label: "Moderate",           color: "#f59e0b" }
        major:            { label: "Major",              color: "#f97316" }
        critical:         { label: "Critical",           color: "#ef4444" }
        immediate_action: { label: "Immediate Action",   color: "#7f1d1d" }
    FitnessResult:
      values:
        fit:                 { label: "Fit for Service" }
        fit_with_monitoring: { label: "Fit with Enhanced Monitoring" }
        repair_required:     { label: "Repair Required" }
        replace:             { label: "Replace" }
        unfit:               { label: "Not Fit for Service" }
    WorkType:
      values: [preventive, corrective, predictive, emergency, modification]
    WorkPriority:
      values:
        low:       { label: "Low",       color: "#94a3b8" }
        normal:    { label: "Normal",    color: "#3b82f6" }
        high:      { label: "High",      color: "#f59e0b" }
        emergency: { label: "Emergency", color: "#ef4444" }
    WorkorderStatus:
      values:
        requested:   { label: "Requested",   color: "#94a3b8" }
        planned:     { label: "Planned",     color: "#3b82f6" }
        approved:    { label: "Approved",    color: "#6366f1" }
        in_progress: { label: "In Progress", color: "#f59e0b" }
        on_hold:     { label: "On Hold",     color: "#8b5cf6" }
        completed:   { label: "Completed",   color: "#10b981" }
        closed:      { label: "Closed",      color: "#059669", final: true }
        rejected:    { label: "Rejected",    color: "#ef4444", final: true }

  # ── Time-based obligations: schedules, not fieldChange events ───────────────
  schedules:
    inspectionDue:
      description: "Warn 30 days before an asset's inspection due date; escalate when overdue"
      type: relative
      source:
        resource: Asset
        filter: "(status == 'operational' || status == 'degraded') && nextInspectionDue != null"
      relativeTo: nextInspectionDue
      tiers:
        - daysUntil: 30
          action: notify
          tag: 30day
          channels: [email]
          recipients: { roles: [maintenancePlanner, inspector] }
          template:
            subject: "Inspection due in 30 days — {assetTag}"
            body: "{name} at {facility}. Due: {nextInspectionDue}. Criticality: {criticality}."
        - daysUntil: 0
          action: notify
          tag: overdue
          channels: [email]
          recipients: { roles: [integrityEngineer, operationsManager] }
          template:
            subject: "INSPECTION OVERDUE — {assetTag}"
            body: "{name} at {facility} was due {nextInspectionDue}. Criticality: {criticality}."
    endOfLife:
      description: "End-of-design-life planning horizon (2 years out)"
      type: relative
      source:
        resource: Asset
        filter: "status != 'decommissioned' && expectedEndOfLife != null"
      relativeTo: expectedEndOfLife
      tiers:
        - daysUntil: 730
          action: notify
          tag: 2year
          channels: [email]
          recipients: { roles: [integrityEngineer, operationsManager] }
          template:
            subject: "Asset {assetTag} reaches design end-of-life in 2 years"
            body: "{name}. Expected end of life: {expectedEndOfLife}. Begin replacement/life-extension planning."

  # ── Inspection results maintain the asset record ────────────────────────────
  workflows:
    prefillInspectionBaseline:
      description: >
        On inspection creation, copy the asset's current condition data onto the
        inspection as the comparison baseline — the manual transcription step
        the white paper claimed to eliminate, actually eliminated.
      trigger:
        resource: Inspection
        event: create
      steps:
        - name: copyBaseline
          action: update
          resource: Inspection
          target: $trigger.record.id
          input:
            previousThickness: "findOne(Asset, id == $trigger.record.assetId).wallThickness"
            previousInspectionDate: "findOne(Asset, id == $trigger.record.assetId).lastInspectionDate"
            minRequiredThickness: "findOne(Asset, id == $trigger.record.assetId).minWallThickness"
          then: end

    applyInspectionResults:
      description: >
        On inspection approval, write the measured condition back to the asset.
        The asset's corrosion rate, wall thickness, and inspection clock are
        maintained by the system from approved inspections.
      trigger:
        resource: Inspection
        event: transition
        transition: approve
      steps:
        - name: updateAsset
          action: update
          resource: Asset
          target: $trigger.record.assetId
          input:
            wallThickness: $trigger.record.measuredThickness
            corrosionRate: $trigger.record.calculatedCorrosionRate
            lastInspectionDate: $trigger.record.performedDate
            nextInspectionDue: $trigger.record.nextInspectionDate
          then: end
      onError:
        action: transition
        target: $trigger.record.id
        transition: requestReinspection

  resources:
    # ─────────────────────────────────────────────────────────────────────────
    Asset:
      object:
        id:                { type: uuid, primaryKey: true }
        assetTag:          { type: string, required: true, unique: true, immutable: true, maxLength: 40 }
        name:              { type: string, required: true, maxLength: 200 }
        assetType:         { type: string, required: true, maxLength: 80 }
        location:          { type: string, required: true, maxLength: 200 }
        gpsLatitude:       { type: float, min: -90, max: 90 }
        gpsLongitude:      { type: float, min: -180, max: 180 }
        facility:          { type: string, required: true, maxLength: 120 }
        unit:              { type: string, maxLength: 60 }
        status:            { $ref: '#/enums/AssetStatus', default: commissioning }
        criticality:       { $ref: '#/enums/AssetCriticality' }
        manufacturer:      { type: string, maxLength: 120 }
        modelNumber:       { type: string, maxLength: 80 }
        serialNumber:      { type: string, maxLength: 80 }
        installDate:       { type: date }
        commissionDate:    { type: date }
        designLifeYears:   { type: integer, min: 1 }
        lastInspectionDate: { type: date }
        nextInspectionDue: { type: date }
        operatingPressure: { type: float }
        operatingTemp:     { type: float }
        designPressure:    { type: float }
        designTemp:        { type: float }
        wallThickness:     { type: float, min: 0 }
        minWallThickness:  { type: float, min: 0 }
        corrosionRate:     { type: float, min: 0 }
        parentAssetId:     { type: uuid }
        notes:             { type: text }
        $merge: { $ref: '#/objects/Timestamp' }

      relationships:
        parent:      { type: belongsTo, target: Asset, foreignKey: parentAssetId, onDelete: restrict }
        children:    { type: hasMany,   target: Asset, foreignKey: parentAssetId }
        inspections: { type: hasMany,   target: Inspection, foreignKey: assetId }
        workorders:  { type: hasMany,   target: Workorder,  foreignKey: assetId }

      indexes:
        - { fields: [status] }
        - { fields: [facility] }
        - { fields: [criticality] }
        - { fields: [parentAssetId] }
        - { fields: [nextInspectionDue] }

      computed:
        expectedEndOfLife:
          type: date
          formula: "dateAdd(installDate, designLifeYears, 'years')"
          stored: true
          updateOn: [installDate, designLifeYears]
        # A stored field computed from now() is stale the moment it's written —
        # time-based action lives in the endOfLife schedule; this field answers
        # the API question only.
        remainingWallLifeYears:
          type: float
          formula: "if(corrosionRate > 0 && wallThickness != null && minWallThickness != null, (wallThickness - minWallThickness) / corrosionRate, null)"
          stored: true
          updateOn: [wallThickness, minWallThickness, corrosionRate]

      stateMachine:
        field: status
        initial: commissioning
        states:
          commissioning:
            transitions:
              commission:
                to: operational
                guards: [inspector, engineer, admin]
                requiredFields: [commissionDate]
          operational:
            transitions:
              degrade:
                to: degraded
                guards: [inspector, engineer, admin]
              takeOutOfService:
                to: out_of_service
                guards: [engineer, operationsManager, admin]
              startMaintenance:
                to: maintenance
                guards: [maintenancePlanner, admin]
          degraded:
            transitions:
              restore:
                to: operational
                guards: [inspector, engineer, admin]
              takeOutOfService:
                to: out_of_service
                guards: [engineer, operationsManager, admin]
              startMaintenance:
                to: maintenance
                guards: [maintenancePlanner, admin]
          out_of_service:
            transitions:
              returnToService:
                to: operational
                guards: [inspector, engineer, admin]
              startMaintenance:
                to: maintenance
                guards: [maintenancePlanner, admin]
              decommission:
                to: decommissioned
                guards: [operationsManager, admin]
          maintenance:
            transitions:
              completeMaintenance:
                to: operational
                guards: [maintenanceTech, engineer, admin]
              failMaintenance:
                to: out_of_service
                guards: [maintenanceTech, engineer, admin]
          decommissioned:
            final: true

      validation:
        wallAboveMinimum:
          rule: "wallThickness == null || minWallThickness == null || wallThickness >= 0"
          message: "Wall thickness cannot be negative"
        designEnvelope:
          rule: "operatingPressure == null || designPressure == null || operatingPressure <= designPressure"
          message: "Operating pressure cannot exceed design pressure"

      features:
        audit: true
        search: true
      audit:
        exclude: [updatedAt]

      history:
        events:
          commissioned:
            on: transition
            transition: commission
            capture: [assetTag, name, assetType, location, facility]
            label: "Asset {assetTag} commissioned at {facility}"
            detail: "{name} ({assetType}), Location: {location}"
            icon: lucide:check-circle
            severity: success
          degraded:
            on: transition
            transition: degrade
            capture: [assetTag, name]
            label: "{assetTag} — condition degraded"
            icon: lucide:alert-triangle
            severity: warning
          outOfService:
            on: transition
            transition: takeOutOfService
            capture: [assetTag, name]
            label: "{assetTag} taken out of service"
            icon: lucide:power-off
            severity: error
          maintenanceStarted:
            on: transition
            transition: startMaintenance
            capture: [assetTag, name]
            label: "{assetTag} — maintenance started"
            icon: lucide:wrench
            severity: info
          maintenanceComplete:
            on: transition
            transition: completeMaintenance
            capture: [assetTag, name]
            label: "{assetTag} — maintenance complete, returned to service"
            icon: lucide:check
            severity: success
          returnedToService:
            on: transition
            transition: returnToService
            capture: [assetTag, name]
            label: "{assetTag} returned to service"
            icon: lucide:power
            severity: success
          decommissioned:
            on: transition
            transition: decommission
            capture: [assetTag, name, location]
            label: "{assetTag} DECOMMISSIONED"
            icon: lucide:archive
            severity: info
          conditionDataUpdated:
            # Fired by the applyInspectionResults workflow write.
            on: update
            fields: [wallThickness, corrosionRate]
            capture: [assetTag, wallThickness, corrosionRate]
            label: "{assetTag} condition data updated from approved inspection"
            detail: "Wall: {wallThickness}. Corrosion rate: {corrosionRate}"
            icon: lucide:ruler
            severity: info
          wallThicknessCritical:
            # Legitimate fieldChange: wallThickness genuinely changes on the
            # workflow write-back.
            on: fieldChange
            field: wallThickness
            condition: "wallThickness != null && minWallThickness != null && wallThickness <= minWallThickness * 1.1"
            capture: [assetTag, wallThickness, minWallThickness, remainingWallLifeYears]
            label: "WALL THICKNESS CRITICAL — {assetTag}"
            detail: "Current: {wallThickness}, Minimum: {minWallThickness}. Remaining life: {remainingWallLifeYears} years"
            icon: lucide:alert-octagon
            severity: error
          criticalityChanged:
            on: fieldChange
            field: criticality
            capture: [assetTag, criticality]
            label: "{assetTag} criticality changed to {criticality}"
            icon: lucide:flag
            severity: warning

      permissions:
        list:   [authenticated]
        get:    [authenticated]
        create: [engineer, operationsManager, admin]
        update: [inspector, engineer, maintenanceTech, maintenancePlanner, operationsManager, admin]
        delete: [admin]
        fields:
          criticality:      { write: [engineer, operationsManager, admin] }
          corrosionRate:    { write: [inspector, engineer, admin] }
          wallThickness:    { write: [inspector, engineer, admin] }
          minWallThickness: { write: [engineer, admin] }
          designPressure:   { write: [engineer, admin] }
          designTemp:       { write: [engineer, admin] }

    # ─────────────────────────────────────────────────────────────────────────
    Inspection:
      mode: sheet
      description: >
        An inspection record. The baseline (previous thickness/date, minimum
        required thickness) is prefilled from the asset by workflow, and the
        approved results flow back to the asset by workflow — no transcription.
      object:
        id:                     { type: uuid, primaryKey: true }
        inspectionNumber:       { type: string, required: true, unique: true, immutable: true, maxLength: 40 }
        assetId:                { type: uuid, required: true, immutable: true }
        inspectionType:         { $ref: '#/enums/InspectionType', required: true }
        inspectionCode:         { type: string, maxLength: 40 }
        status:                 { $ref: '#/enums/InspectionStatus', default: scheduled }
        scheduledDate:          { type: date, required: true }
        performedDate:          { type: date }
        inspectorId:            { type: string, maxLength: 100 }
        inspectorName:          { type: string, maxLength: 120 }
        inspectorCertification: { type: string, maxLength: 80 }
        conditions:             { type: text }
        weatherTemp:            { type: float }
        weatherConditions:      { type: string, maxLength: 120 }
        finding:                { $ref: '#/enums/FindingSeverity' }
        findingDescription:     { type: text }
        recommendation:         { type: text }
        nextInspectionDate:     { type: date }
        measuredThickness:      { type: float, min: 0 }
        previousThickness:      { type: float, min: 0 }       # prefilled by workflow
        previousInspectionDate: { type: date }                 # prefilled by workflow
        minRequiredThickness:   { type: float, min: 0 }       # prefilled by workflow
        fitnessForService:      { $ref: '#/enums/FitnessResult' }
        completedAt:            { type: datetime }
        photos:                 { type: json }
        attachments:            { type: json }
        $merge: { $ref: '#/objects/Timestamp' }

      relationships:
        asset: { type: belongsTo, target: Asset, foreignKey: assetId, onDelete: restrict }

      indexes:
        - { fields: [assetId] }
        - { fields: [status] }
        - { fields: [scheduledDate] }
        - { fields: [inspectionType] }

      computed:
        calculatedCorrosionRate:
          type: float
          formula: >
            if(previousThickness != null && measuredThickness != null
               && previousThickness > measuredThickness
               && previousInspectionDate != null && performedDate != null,
               (previousThickness - measuredThickness)
                 / max(yearsBetween(previousInspectionDate, performedDate), 0.1),
               0)
          stored: true
          updateOn: [previousThickness, measuredThickness, previousInspectionDate, performedDate]

      dynamicFields:
        enabled: true
        allowTypes: [float, integer, string, text, boolean, date]
        maxColumns: 50
        permissions:
          addColumn:    [inspector, engineer, admin]
          deleteColumn: [admin]

      stateMachine:
        field: status
        initial: scheduled
        states:
          scheduled:
            transitions:
              begin:
                to: in_progress
                guards: [inspector, admin]
                sets:
                  inspectorId: "$user.id"
                  inspectorName: "$user.username"
          in_progress:
            transitions:
              complete:
                to: completed
                guards: [inspector, admin]
                requiredFields: [performedDate, measuredThickness]
                sets:
                  completedAt: "now()"
              cancel:
                to: cancelled
                guards: [engineer, operationsManager, admin]
          completed:
            transitions:
              review:
                to: reviewed
                guards: [engineer, admin]
                requiredFields: [finding, fitnessForService]
              requestReinspection:
                to: scheduled
                guards: [engineer, admin]
          reviewed:
            transitions:
              approve:
                to: approved
                # SoD: the approving integrity engineer is not the performing
                # inspector. No admin bypass on the approval itself.
                guard: "hasRole('integrityEngineer') && field('inspectorId') != userId()"
                requiredFields: [recommendation, nextInspectionDate]
              requestReinspection:
                to: scheduled
                guards: [integrityEngineer, admin]
          approved:
            final: true
          cancelled:
            final: true

      validation:
        performedNotFuture:
          rule: "performedDate == null || performedDate <= now()"
          message: "Performed date cannot be in the future"
        unfitCannotApproveNext:
          rule: "status != 'approved' || fitnessForService != 'unfit'"
          message: "An asset assessed 'not fit for service' cannot have an approved routine inspection — raise a work order and re-inspect"

      features:
        audit: true
        versioning: true    # full measurement snapshots

      history:
        events:
          scheduled:
            on: create
            capture: [inspectionNumber, inspectionType, scheduledDate]
            label: "Inspection {inspectionNumber} scheduled"
            detail: "Type: {inspectionType}. Date: {scheduledDate}"
            icon: lucide:calendar
            severity: info
          started:
            on: transition
            transition: begin
            capture: [inspectorName, inspectorCertification]
            label: "Inspection started by {inspectorName}"
            detail: "Certification: {inspectorCertification}"
            icon: lucide:play
            severity: info
          completed:
            on: transition
            transition: complete
            capture: [inspectorName, performedDate, measuredThickness, conditions]
            label: "Inspection completed by {inspectorName}"
            detail: "Performed: {performedDate}. Thickness: {measuredThickness}. Conditions: {conditions}"
            icon: lucide:check
            severity: info
          reviewed:
            on: transition
            transition: review
            capture: [finding, fitnessForService, findingDescription]
            label: "Reviewed — {finding}, fitness: {fitnessForService}"
            detail: "{findingDescription}"
            icon: lucide:eye
            severity: info
          approved:
            on: transition
            transition: approve
            capture: [recommendation, nextInspectionDate]
            label: "Inspection approved"
            detail: "Recommendation: {recommendation}. Next inspection: {nextInspectionDate}"
            icon: lucide:check-circle
            severity: success
          criticalFinding:
            on: fieldChange
            field: finding
            condition: "finding == 'critical' || finding == 'immediate_action'"
            capture: [inspectionNumber, finding, findingDescription]
            label: "CRITICAL FINDING — {inspectionNumber}"
            detail: "{findingDescription}"
            icon: lucide:alert-octagon
            severity: error
          thicknessBelowMinimum:
            # Compares against the required minimum (prefilled from the asset)
            # with a 10% approach band.
            on: fieldChange
            field: measuredThickness
            condition: "measuredThickness != null && minRequiredThickness != null && measuredThickness <= minRequiredThickness * 1.1"
            capture: [measuredThickness, minRequiredThickness]
            label: "THICKNESS AT OR APPROACHING MINIMUM"
            detail: "Measured: {measuredThickness}. Required minimum: {minRequiredThickness}"
            icon: lucide:alert-octagon
            severity: error
          highCorrosionRate:
            on: fieldChange
            field: calculatedCorrosionRate
            condition: "calculatedCorrosionRate > 0.5"
            capture: [calculatedCorrosionRate, measuredThickness]
            label: "HIGH CORROSION RATE — {calculatedCorrosionRate} mm/yr"
            icon: lucide:alert-triangle
            severity: error

      permissions:
        list:   [authenticated]
        get:    [authenticated]
        create: [inspector, engineer, maintenancePlanner, admin]
        update: [inspector, engineer, integrityEngineer, admin]
        delete: [admin]
        fields:
          finding:              { write: [engineer, integrityEngineer, admin] }
          fitnessForService:    { write: [integrityEngineer, admin] }
          recommendation:       { write: [engineer, integrityEngineer, admin] }
          minRequiredThickness: { write: [admin] }   # workflow-set from the asset

    # ─────────────────────────────────────────────────────────────────────────
    Workorder:
      description: >
        Maintenance work order. Safety documentation is BLOCKING: work cannot
        start with a required permit or LOTO undocumented.
      object:
        id:                    { type: uuid, primaryKey: true }
        woNumber:              { type: string, required: true, unique: true, immutable: true, maxLength: 40 }
        assetId:               { type: uuid, required: true, immutable: true }
        title:                 { type: string, required: true, maxLength: 200 }
        description:           { type: text, required: true }
        workType:              { $ref: '#/enums/WorkType' }
        priority:              { $ref: '#/enums/WorkPriority', default: normal }
        status:                { $ref: '#/enums/WorkorderStatus', default: requested }
        requestedById:         { type: string, required: true, immutable: true, maxLength: 100 }
        requestedByName:       { type: string, required: true, maxLength: 120 }
        assignedToId:          { type: string, maxLength: 100 }
        assignedToName:        { type: string, maxLength: 120 }
        plannedStartDate:      { type: date }
        plannedEndDate:        { type: date }
        actualStartDate:       { type: datetime }
        actualEndDate:         { type: datetime }
        estimatedHours:        { type: float, min: 0 }
        actualHours:           { type: float, min: 0 }
        partsUsed:             { type: json }
        estimatedCost:         { type: money }
        actualCost:            { type: money }
        completionNotes:       { type: text }
        failureCode:           { type: string, maxLength: 40 }
        failureCause:          { type: string, maxLength: 200 }
        rejectionReason:       { type: text }
        closedAt:              { type: datetime }
        safetyPermitRequired:  { type: boolean, default: false }
        safetyPermitNumber:    { type: string, maxLength: 40 }
        lockoutTagoutRequired: { type: boolean, default: false }
        lotoTagNumber:         { type: string, maxLength: 40 }
        $merge: { $ref: '#/objects/Timestamp' }

      relationships:
        asset: { type: belongsTo, target: Asset, foreignKey: assetId, onDelete: restrict }

      indexes:
        - { fields: [assetId] }
        - { fields: [status] }
        - { fields: [priority] }
        - { fields: [assignedToId] }

      stateMachine:
        field: status
        initial: requested
        states:
          requested:
            transitions:
              plan:
                to: planned
                guards: [maintenancePlanner, admin]
                requiredFields: [assignedToId, plannedStartDate]
              reject:
                to: rejected
                guards: [maintenancePlanner, operationsManager, admin]
                requiredFields: [rejectionReason]
          planned:
            transitions:
              approve:
                to: approved
                guards: [operationsManager, admin]
              hold:
                to: on_hold
                guards: [maintenancePlanner, operationsManager, admin]
          on_hold:
            transitions:
              release:
                to: planned
                guards: [maintenancePlanner, admin]
          approved:
            transitions:
              start:
                to: in_progress
                # The assignee starts the work: requestedById isn't an
                # owner-convention FK, and the requester isn't who executes
                # anyway, so the guard checks the assignee directly.
                guard: "field('assignedToId') == userId() || hasRole('maintenanceTech') || hasRole('admin')"
                sets:
                  actualStartDate: "now()"
          in_progress:
            transitions:
              complete:
                to: completed
                guard: "field('assignedToId') == userId() || hasRole('maintenanceTech') || hasRole('admin')"
                requiredFields: [completionNotes, actualHours]
                sets:
                  actualEndDate: "now()"
              hold:
                to: on_hold
                guards: [maintenanceTech, admin]
          completed:
            transitions:
              close:
                to: closed
                guards: [maintenancePlanner, operationsManager, admin]
                sets:
                  closedAt: "now()"
          closed:
            final: true
          rejected:
            final: true

      validation:
        # BLOCKING safety controls.
        permitBeforeStart:
          rule: "status != 'in_progress' || safetyPermitRequired == false || safetyPermitNumber != null"
          message: "A required safety permit must be documented before work starts"
        lotoBeforeStart:
          rule: "status != 'in_progress' || lockoutTagoutRequired == false || lotoTagNumber != null"
          message: "A required Lockout/Tagout must be documented (tag number) before work starts"
        plannedDatesSane:
          rule: "plannedEndDate == null || plannedStartDate == null || plannedEndDate >= plannedStartDate"
          message: "Planned end date must not precede the planned start date"

      features:
        audit: true

      history:
        events:
          requested:
            on: create
            capture: [woNumber, title, workType, priority, requestedByName]
            label: "WO {woNumber} — {title}"
            detail: "Type: {workType}. Priority: {priority}. Requested by: {requestedByName}"
            icon: lucide:clipboard-list
            severity: info
          planned:
            on: transition
            transition: plan
            capture: [assignedToName, plannedStartDate, estimatedHours]
            label: "Planned — assigned to {assignedToName}"
            detail: "Start: {plannedStartDate}. Est. hours: {estimatedHours}"
            icon: lucide:calendar
            severity: info
          started:
            on: transition
            transition: start
            capture: [assignedToName, safetyPermitNumber, lotoTagNumber]
            label: "Work started by {assignedToName}"
            detail: "Permit: {safetyPermitNumber}. LOTO: {lotoTagNumber}"
            icon: lucide:play
            severity: info
          completed:
            on: transition
            transition: complete
            capture: [completionNotes, actualHours, partsUsed]
            label: "Work completed — {actualHours} hours"
            detail: "{completionNotes}"
            icon: lucide:check
            severity: success
          closed:
            on: transition
            transition: close
            label: "Work order closed"
            icon: lucide:check-circle
            severity: success
          rejected:
            on: transition
            transition: reject
            capture: [rejectionReason]
            label: "Work order rejected"
            detail: "{rejectionReason}"
            icon: lucide:x-circle
            severity: warning
          emergencyWork:
            on: create
            condition: "priority == 'emergency'"
            capture: [woNumber, title]
            label: "EMERGENCY WORK ORDER — {woNumber}: {title}"
            icon: lucide:alert-octagon
            severity: error

      # Without an explicit permissions block, every operation defaults to
      # admin-only, so no technician could touch a work order.
      permissions:
        list:   [authenticated]
        get:    [authenticated]
        create: [maintenanceTech, maintenancePlanner, engineer, operationsManager, admin]
        update: [maintenanceTech, maintenancePlanner, engineer, operationsManager, admin]
        delete: [admin]
        fields:
          safetyPermitNumber: { write: [safetyEngineer, maintenancePlanner, admin] }
          lotoTagNumber:      { write: [safetyEngineer, maintenanceTech, admin] }
          actualCost:         { write: [maintenancePlanner, operationsManager, admin] }

Domain 2: Management of Change (moc)

domain:
  name: moc
  version: "1.0.0"
  description: >
    OSHA PSM 29 CFR 1910.119(l) Management of Change. Training completion and
    PSSR are BLOCKING gates, not warnings; the originator cannot approve their
    own change; temporary MOCs are reverted by schedule — the Complete
    Reference's own §24 example, applied.

  channels:
    email:
      provider: smtp
      host: ${SMTP_HOST}
      port: ${SMTP_PORT:587}
      from: "moc-noreply@example.com"

  objects:
    Timestamp:
      createdAt: { type: datetime, autoNow: create }
      updatedAt: { type: datetime, autoNow: update }

  enums:
    ChangeType:
      values:
        equipment:          { label: "Equipment" }
        process_technology: { label: "Process Technology" }
        procedure:          { label: "Procedure" }
        facility:           { label: "Facility" }
        config_change:      { label: "Configuration Change" }
    MocStatus:
      values:
        initiated:    { label: "Initiated",     color: "#94a3b8" }
        submitted:    { label: "Submitted",     color: "#3b82f6" }
        under_review: { label: "Under Review",  color: "#8b5cf6" }
        approved:     { label: "Approved",      color: "#10b981" }
        implementing: { label: "Implementing",  color: "#f97316" }
        pssr_pending: { label: "PSSR Pending",  color: "#06b6d4" }
        closed:       { label: "Closed",        color: "#059669" }
        reverting:    { label: "Reverting",     color: "#f59e0b" }
        reverted:     { label: "Reverted",      color: "#64748b", final: true }
        rejected:     { label: "Rejected",      color: "#ef4444", final: true }
    RiskLevel:
      values:
        low:      { label: "Low",      color: "#94a3b8" }
        medium:   { label: "Medium",   color: "#f59e0b" }
        high:     { label: "High",     color: "#f97316" }
        critical: { label: "Critical", color: "#ef4444" }

  schedules:
    # The spec's own §24 example, finally used in the domain it was written for.
    temporaryMocExpiry:
      description: "Warn 7 days before a temporary MOC's expiry; execute the revert on the date"
      type: relative
      source:
        resource: Change
        filter: "isTemporary == true && status == 'closed'"
      relativeTo: temporaryExpiry
      tiers:
        - daysUntil: 7
          action: notify
          tag: 7day
          channels: [email]
          recipients: { roles: [operationsManager, safetyEngineer] }
          template:
            subject: "TEMPORARY MOC EXPIRING — {mocNumber}"
            body: "{title}. Expires {temporaryExpiry}. Revert plan must be executed."
        - daysUntil: 0
          action: transition
          transition: revert
          tag: revert

  resources:
    Change:
      object:
        id:                        { type: uuid, primaryKey: true }
        mocNumber:                 { type: string, required: true, unique: true, immutable: true, maxLength: 40 }
        title:                     { type: string, required: true, maxLength: 200 }
        changeType:                { $ref: '#/enums/ChangeType', required: true }
        facility:                  { type: string, required: true, maxLength: 120 }
        unit:                      { type: string, maxLength: 60 }
        status:                    { $ref: '#/enums/MocStatus', default: initiated }
        description:               { type: markdown, required: true }
        justification:             { type: markdown, required: true }
        safetyImpact:              { type: text }
        environmentalImpact:       { type: text }
        operationalImpact:         { type: text }
        hazardAssessment:          { type: text }
        riskLevel:                 { $ref: '#/enums/RiskLevel' }
        rejectionReason:           { type: text }
        affectedAssets:            { type: json }
        affectedProcedures:        { type: json }
        affectedPnids:             { type: json }
        createdById:               { type: string, required: true, immutable: true, maxLength: 100 }
        originatorName:            { type: string, required: true, maxLength: 120 }
        reviewerId:                { type: string, maxLength: 100 }
        reviewerName:              { type: string, maxLength: 120 }
        approvedById:              { type: string, maxLength: 100 }
        approvedByName:            { type: string, maxLength: 120 }
        isTemporary:               { type: boolean, default: false }
        temporaryDuration:         { type: integer, min: 1 }
        temporaryExpiry:           { type: date }
        revertPlan:                { type: text }
        trainingRequired:          { type: boolean, default: false }
        trainingComplete:          { type: boolean, default: false }
        preStartupReviewComplete:  { type: boolean, default: false }
        closedAt:                  { type: datetime }
        revertedAt:                { type: datetime }
        $merge: { $ref: '#/objects/Timestamp' }

      indexes:
        - { fields: [status] }
        - { fields: [facility] }
        - { fields: [riskLevel] }
        - { fields: [createdById] }

      stateMachine:
        field: status
        initial: initiated
        states:
          initiated:
            transitions:
              submit:
                to: submitted
                guards: [owner, engineer, admin]
                requiredFields: [description, justification, changeType]
          submitted:
            transitions:
              review:
                to: under_review
                guards: [safetyEngineer, processEngineer, admin]
                sets:
                  reviewerId: "$user.id"
                  reviewerName: "$user.username"
          under_review:
            transitions:
              approve:
                to: approved
                # SoD: the approver is not the originator and not the safety
                # reviewer. No admin bypass on approval.
                guard: >
                  (hasRole('operationsManager') || hasRole('plantManager'))
                  && field('createdById') != userId()
                  && field('reviewerId') != userId()
                requiredFields: [hazardAssessment, riskLevel, safetyImpact]
                sets:
                  approvedById: "$user.id"
                  approvedByName: "$user.username"
              reject:
                to: rejected
                guards: [operationsManager, plantManager, admin]
                requiredFields: [rejectionReason]
              requestRevision:
                to: initiated
                guards: [safetyEngineer, admin]
          approved:
            transitions:
              implement:
                to: implementing
                guards: [engineer, operationsManager, admin]
          implementing:
            transitions:
              completeImplementation:
                to: pssr_pending
                guards: [engineer, admin]
          pssr_pending:
            transitions:
              completePssr:
                to: closed
                guards: [safetyEngineer, operationsManager, admin]
                sets:
                  closedAt: "now()"
              failPssr:
                to: implementing
                guards: [safetyEngineer, admin]
          closed:
            transitions:
              revert:
                to: reverting
                guards: [operationsManager, admin]
          reverting:
            transitions:
              completeRevert:
                to: reverted
                guards: [engineer, admin]
                sets:
                  revertedAt: "now()"
          reverted:
            final: true
          rejected:
            final: true

      validation:
        # BLOCKING gates. A requiredFields check on a boolean is satisfied by
        # `false` (non-null); these rules check the VALUE.
        trainingBeforeImplementation:
          rule: "status != 'implementing' || trainingRequired == false || trainingComplete == true"
          message: "Required training must be complete before implementation (29 CFR 1910.119(l)(3))"
        pssrMustActuallyPass:
          rule: "status != 'closed' || preStartupReviewComplete == true"
          message: "The Pre-Startup Safety Review must be complete before the MOC closes"
        temporaryNeedsExpiryAndPlan:
          rule: "isTemporary == false || (temporaryExpiry != null && revertPlan != null)"
          message: "A temporary MOC must carry an expiry date and a revert plan"
        revertNeedsPlan:
          rule: "status != 'reverting' || revertPlan != null"
          message: "Reverting requires a documented revert plan"

      features:
        audit: true
        versioning: true

      history:
        events:
          initiated:
            on: create
            capture: [mocNumber, title, changeType, facility, originatorName]
            label: "MOC {mocNumber} initiated — {title}"
            detail: "Type: {changeType}. Facility: {facility}. Originator: {originatorName}"
            icon: lucide:file-plus
            severity: info
          submitted:
            on: transition
            transition: submit
            label: "MOC submitted for safety review"
            icon: lucide:send
            severity: info
          safetyReviewStarted:
            on: transition
            transition: review
            capture: [reviewerName]
            label: "Safety review started by {reviewerName}"
            icon: lucide:shield
            severity: info
          approved:
            on: transition
            transition: approve
            capture: [approvedByName, hazardAssessment, riskLevel, safetyImpact]
            label: "MOC APPROVED by {approvedByName} — risk level {riskLevel}"
            detail: "Safety impact: {safetyImpact}. Hazard assessment: {hazardAssessment}"
            icon: lucide:check-circle
            severity: success
          rejected:
            on: transition
            transition: reject
            capture: [rejectionReason]
            label: "MOC REJECTED"
            detail: "{rejectionReason}"
            icon: lucide:x-circle
            severity: error
          implementationStarted:
            on: transition
            transition: implement
            label: "Implementation started"
            icon: lucide:hammer
            severity: info
          pssrCompleted:
            on: transition
            transition: completePssr
            label: "Pre-Startup Safety Review PASSED — MOC closed"
            icon: lucide:shield-check
            severity: success
          pssrFailed:
            on: transition
            transition: failPssr
            label: "Pre-Startup Safety Review FAILED — returned for rework"
            icon: lucide:shield-x
            severity: error
          reverted:
            on: transition
            transition: completeRevert
            capture: [mocNumber, revertedAt]
            label: "Temporary MOC {mocNumber} reverted"
            icon: lucide:undo
            severity: info
          highRiskMoc:
            on: fieldChange
            field: riskLevel
            condition: "riskLevel == 'high' || riskLevel == 'critical'"
            capture: [mocNumber, title, riskLevel, facility]
            label: "HIGH RISK MOC — {mocNumber}: {title}"
            detail: "Facility: {facility}. Risk: {riskLevel}. Enhanced review required."
            icon: lucide:alert-octagon
            severity: error
            roles: [safetyEngineer, operationsManager, plantManager, admin]

      permissions:
        list:   [authenticated]
        get:    [authenticated]
        create: [engineer, processEngineer, operationsManager, admin]
        update: [engineer, processEngineer, safetyEngineer, operationsManager, plantManager, admin]
        delete: [admin]
        fields:
          hazardAssessment:         { write: [safetyEngineer, processEngineer, admin] }
          riskLevel:                { write: [safetyEngineer, operationsManager, admin] }
          preStartupReviewComplete: { write: [safetyEngineer, operationsManager, admin] }
          trainingComplete:         { write: [safetyEngineer, operationsManager, admin] }
          approvedById:             { write: [admin] }   # normally set only via the approve transition
          reviewerId:               { write: [admin] }

Edge / IoT domain (edge)

# Runs on an embedded-LINUX edge gateway (ARM SBC-class hardware). The white
# paper claimed ESP32/STM32 targets: a C++20 microkernel with dlopen-based
# plugins, SQLite, and JWT does not run on a ~520KB-RAM microcontroller with no
# dynamic loader. Sensor MCUs publish raw readings (MQTT/serial) to the gateway;
# Kalos evaluates the domain there. Do not claim bare-metal MCU support.
domain:
  name: edge
  version: "1.0.0"
  resources:
    Reading:
      object:
        id:             { type: uuid, primaryKey: true }
        assetTag:       { type: string, required: true, maxLength: 40 }
        pressure:       { type: float, required: true }
        temp:           { type: float }
        # Device-configured limits, defined on the resource so the event
        # conditions below can reference them.
        limitPressure:  { type: float, required: true }
        limitTemp:      { type: float }
        recordedAt:     { type: datetime, autoNow: create }
      indexes:
        - { fields: [assetTag] }
      history:
        events:
          overpressure:
            on: create
            condition: "pressure > limitPressure * 1.1"
            capture: [assetTag, pressure, limitPressure]
            label: "OVERPRESSURE — {assetTag}: {pressure} psi (limit {limitPressure})"
            icon: lucide:alert-octagon
            severity: error
          temperatureAnomaly:
            on: create
            condition: "limitTemp != null && temp != null && temp > limitTemp * 1.05"
            capture: [assetTag, temp, limitTemp]
            label: "TEMPERATURE ANOMALY — {assetTag}: {temp}°F (limit {limitTemp})"
            icon: lucide:thermometer
            severity: warning
      permissions:
        list:   [authenticated]
        get:    [authenticated]
        create: [sensorGateway, admin]
        update: [admin]
        delete: [admin]

Investigator queries

# PHMSA — complete asset history (resolve the tag, then the timeline)
curl -s "$BASE/api/v1/assets/assets?filter[assetTag]=PL-2019-0847" \
  -H "Authorization: Bearer $TOKEN" | jq -r '.data.items[0].id'
curl -s "$BASE/api/v1/assets/assets/{id}/history" -H "Authorization: Bearer $TOKEN"

# Related inspections and work orders via the relationship endpoints
curl -s "$BASE/api/v1/assets/assets/{id}/inspections" -H "Authorization: Bearer $TOKEN"
curl -s "$BASE/api/v1/assets/assets/{id}/workorders"  -H "Authorization: Bearer $TOKEN"

# OSHA PSM — MOCs for a facility (resource filter; facility is not a history param)
curl -s "$BASE/api/v1/moc/changes?filter[facility]=Unit-7&sort=-createdAt" \
  -H "Authorization: Bearer $TOKEN"
# Training/PSSR compliance is now structural: an implemented MOC with incomplete
# training CANNOT EXIST — the validation rule blocks it at 422. The audit answer
# is the YAML rule plus the absence of counterexamples, not a warning log.

# API 510 — critical findings and corrosion trend events across the fleet
curl -s "$BASE/api/v1/assets/inspections/history?event=criticalFinding&after=2025-01-01" \
  -H "Authorization: Bearer $TOKEN"
curl -s "$BASE/api/v1/assets/inspections/history?event=highCorrosionRate" \
  -H "Authorization: Bearer $TOKEN"

# Evidence the inspection-due engine ran (scheduler log)
curl -s "$BASE/api/v1/assets/schedules/inspectionDue/log?since=2026-01-01" \
  -H "Authorization: Bearer $TOKEN"

# Workflow evidence: every write-back of inspection results to an asset
curl -s "$BASE/api/v1/assets/workflows/applyInspectionResults/instances?status=completed" \
  -H "Authorization: Bearer $TOKEN"