← All seeds
polis ADL v0.3.1

Municipal Permitting & Licensing

The flagship "kitchen-sink" seed: a city permitting authority — permits, inspections, payments, budgets, and public notices — designed to exercise the full ADL v0.3 feature surface.

Resources
SiteConfig, Department, PermitType, Applicant, Inspector, Permit, Inspection, Payment, PermitTag, PermitReviewer, PublicNotice, BudgetWorksheet, BudgetLine
Roles
admin, clerk, reviewer, inspector, inspectorLead, financeOfficer, privacyOfficer, applicant, authenticated, owner, *
Features (57)
CRUDrelationshipsbelongsToManycompositePrimaryKeyselfReferentialrichEnumsstateMachinecomputedFieldsaggregationFieldsreactiveFieldscrossFieldValidationcrossResourceValidationsoftDeleteauditversioningsearchexportimportcachingscopesconditionalPermissionsfieldLevelPermissionsaelPermissionshistoryrulesEnginenotificationsdigestNotificationsescalationChainsscheduledTaskstieredSchedulesworkflowsviewsreportsdashboardstemplatesintegrationswebhooksprivacygdprencryptionconsentretentionlocalizationi18nblocksiconsfontscomponentsrepeatersmediasvgsheetModedynamicFieldsindexessingletoncustomOperationsallContentTypes

A “kitchen-sink” ADL seed package that models a municipal permitting & licensing authority, designed to exercise the full ADL v0.3 feature surface.

It serves two purposes:

  1. Conformance test. If a Kalos runtime can parse, bootstrap, migrate, and serve this domain end-to-end, it implements the full v0.3 surface area.
  2. Worked documentation. Every construct appears in a realistic context, so the file doubles as a reference for how the features compose.

The domain story: a city agency accepts permit/license applications from residents and businesses, routes them through review and inspection, charges fees, issues official documents, publishes public notices, files reports to a state registry, and tracks departmental budgets, all under GDPR/CCPA-style privacy controls and multilingual public access.

Files

FilePurpose
domain.yamlThe complete ADL v0.3 domain definition
seed.yamlRoles, users, assignments, and sample records
manifest.jsonPackage metadata and feature list
README.mdThis document, including the feature-coverage matrix
VERSIONS.mdVersion history
LICENSE.mdMIT license

Resources

ResourceRole in the modelNotable features it carries
SiteConfigAgency-wide configurationSingleton, component, svg, font, color, json
DepartmentOrg unit owning permits and inspectorstimezone, reactive cross-resource computed field, export/search
PermitTypeCatalog of permit/license typesSelf-referential tree, icon, markdown, money, caching
ApplicantResident/business/contractor applying for permitsHeavy PII + privacy, password/token/bytes/set/language, soft delete, versioning, import, owner role
InspectorField inspectorbelongsTo Department, certification expiry (feeds a relative schedule)
PermitThe core lifecycle resourceState machine, history, rules, workflow, templates, integrations, computed aggregations, conditional + field-level RBAC, scopes, nearly every scalar type
InspectionField inspection on a permitChild state machine, repeater checklist, webhook update target
PaymentFee paymentState machine, aggregation source, financial PII
PermitTagFree-form tagsbelongsToMany (junction PermitTagLink, with timestamps)
PermitReviewerReviewer-to-permit assignmentComposite primary key resource
PublicNoticePublic-facing bulletinsblocks (incl. custom blocks), media, svg, icon, custom operations
BudgetWorksheetDepartmental budgetSheet mode: dynamic columns, reactive computed grandTotal
BudgetLineBudget rowsReactive same-record formula chain + cross-record SUMIF, schemaChange history

Roles

admin, clerk, reviewer, inspector, inspectorLead, financeOfficer, privacyOfficer, applicant, plus the built-in identifiers authenticated, owner, and *.

Running

Point the seeder at this package and start the server:

{
  "seeder": { "seed_file": "seeds/polis/seed.yaml", "run_on_start": true }
}

The seed is idempotent (§32.8). Demo passwords: admin = Admin123!, all others = Demo123!.

A handful of environment variables are referenced by integrations/privacy and can be left unset in development (the runtime falls back or disables the relevant subsystem): ENCRYPTION_KEY, STATE_CLIENT_ID, STATE_CLIENT_SECRET, INSPECTION_WEBHOOK_SECRET, S3_BUCKET, AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, SMTP_HOST, SMTP_USER, SMTP_PASS, AGENCY_NAME, AGENCY_ADDRESS, AGENCY_LOGO_PATH, JURISDICTION_CODE, ARCHIVE_PATH.

Feature-Coverage Matrix

Each spec chapter mapped to where it is exercised in domain.yaml.

§ChapterWhere it appears
6Document Structuredomain block with config, properties, objects, enums, resources, blocks, icons, fonts
7Type System (all 37 types)Every type appears at least once (see “Type coverage” below)
7.7Field constraintsrequired, unique, default, immutable, nullable, pattern, trim, lowercase, uppercase, min/max, precision/scale, writeOnly/internal/hidden/requestable, mask
7.8Auto-generated fieldsautoNow: create/always (Timestamp), autoincrement (Applicant.applicantNumber, Permit.caseSequence), token, slug
8RelationshipsbelongsTo (all four onDelete modes), hasMany (with orderBy), belongsToMany (Permit↔PermitTag via PermitTagLink), self-referential (PermitType.parent/children), composite PK (PermitReviewer), immutable FKs
9EnumerationsRich enums (PermitStatus, InspectionStatus, …) with label/description/color/icon/final; inline simple enums (Inspection.inspectionType, Payment.method, BudgetLine.category)
11Permissions & RBACOperation-level role arrays + AEL expressions (Permit.get), field-level read/write/mask, conditional-by-state (Permit.permissions.conditional), transition permissions, owner role
12State MachinesPermit, Inspection, Payment: guards, requiredFields, sets (now()/null), onEnter/onExit hooks, final states
13Computed FieldsVirtual (Permit.isExpired, displayLabel), materialized chain (balanceDuefeesCollected), aggregation (feesCollected, inspectionCount, permitCount), reactive (Department.totalAssessedFees, BudgetLine.categoryTotal)
14ValidationCross-field rules (Permit.geoPaired, decisionNeedsReason), when: create, cross-resource query()/exists() (Applicant.uniqueSsn, Permit.applicantActive)
15Resource FeaturessoftDelete, audit, versioning, search, export, import, caching+cacheTtlSeconds, transactions, validationMode
17API Configurationconfig.api.prefix + aliases, singleton (SiteConfig), customOperations (PublicNotice.publish/archive)
18Content & MediaCustom blocks (Callout, NoticeBanner), icons sets (lucide CDN + local seal), fonts (google + system), component, repeater, media, svg
19IndexesSingle, composite, named, and unique: true indexes across most resources
20ScopesPermit.scopes (default open, ownership scope :currentUserId, all), PublicNotice.scopes
21History of ChangePermit, Inspection, Payment, BudgetLine: on: create/update/transition/fieldChange/schemaChange, capture, label/detail, icon, severity, role-scoped events, conditional events, display, retention
22Rules Enginereject/warn/log actions, aggregation/lookup/temporal conditions, messageData, regulation, priority, dependsOn, group + _groups
23NotificationsStandalone (on: history), conditional, type: digest, escalation chains, throttle, priority, recipients by field/roles
24Scheduled TasksRecurring (expirePermits, auditRetentionCleanup), relative tiered (renewalReminders), action: report/transition/update/archive, skipIf
25WorkflowspermitReviewProcess: action/wait/decide/parallel steps, timeout, onError, $trigger/$event/$steps data flow
26Views / Reports / DashboardsactivePermits view (computed columns + highlight), complianceQuarterly report (parameters, metrics + table), operationsOverview dashboard (gauge/counter/scorecard/breakdown/timeSeries/timeline)
27Templates / DocumentspermitCertificate (trigger, PDF, data, layout, {#each} body, attachTo, notification), denialLetter
28Sheet ModeBudgetWorksheet/BudgetLine: dynamicFields, computedFields.reactive/overridable/crossRecord, on: schemaChange history
29 / 32Auth + Seederseed.yaml roles/users/assignments/data, deterministic IDs (allowUserProvided), state bypass (Permit #2 created approved), m2m link action
33Privacy / GDPRDomain privacy block (classifications, subjectIdentifier, anonymization, encryption, consent, retention) + field-level pii/sensitive/encrypted/subjectIdentifier on Applicant and Payment
34Localization / i18nlocalization block: supported locales, fallback chain, per-locale date/number formats, inline translations
35External IntegrationsREST outbound (statePermitRegistry, OAuth2 + retry + circuit breaker + rate limit + dead-letter), webhook inbound (sponsorWebhook, HMAC + replay protection), S3, SMTP
36AELUsed throughout: permission expressions, guards, computed formulas, validation rules, rule conditions, scope/notification conditions
37OpenAPI GenerationEmergent: generated from the model; rich-enum x-enum-* extensions come from the enums above

Type coverage (Chapter 7, all 37)

Tier 0: string, text, integer, decimal, boolean, date, datetime, uuid, email, url · Tier 1: float, timestamp, slug, json, markdown, autoincrement · Tier 2: phone, time, password, token, array · Tier 3: money, percentage, currency, country, language, timezone, color, bytes, set · Content: media, svg, blocks, icon, font, component, repeater

A coverage script (run during authoring) confirms all 37 types and 90+ feature constructs are present.

Notes & Assumptions

  • Lua hooks. The onEnter/onExit hook names (acknowledgeSubmission, pushToSearchIndex) reference external Lua scripts (§31). The domain declares the hooks; the scripts themselves are deployment-specific and not included here.
  • Media files. seed.yaml references file://assets/.... Provide those assets, switch to inline base64, or upload via the media endpoint after boot if your seeder does not resolve file://.
  • Version markers. Everything targets ADL 0.3.1, built specifically against the v0.3 Intelligence-Layer chapters.

Non-Goals

This seed is breadth-first by design. It favors touching every feature over deep realism in any one area. It is not a production permitting system: there is no payment-gateway implementation behind the Payment resource, no real PDF rendering engine, no GIS, and the integration endpoints point at example hosts. Those are intentionally left as the boundary between the declarative model and the runtime/plugins that implement it.

Domain definition

View raw (1946 lines)
Show domain.yaml
# ============================================================================
#  polis/domain.yaml
#
#  POLIS — Municipal Permitting & Licensing Authority
#  ADL version: 0.3.0
#
#  A "kitchen-sink" conformance seed. This single domain is intentionally
#  designed to exercise EVERY feature of the ADL v0.3 specification at least
#  once. It is meant as a living conformance test and as worked documentation:
#  if the runtime can parse, bootstrap, and serve this domain end-to-end, it
#  implements the full v0.3 surface area.
#
#  The README.md ships a feature-coverage matrix mapping each spec chapter to
#  the exact construct(s) below that exercise it.
#
#  Domain story: a city agency that accepts permit/license applications from
#  residents and businesses, routes them through review and inspection, charges
#  fees, issues official documents, publishes public notices, files reports to
#  state systems, and tracks departmental budgets — under GDPR/CCPA-style
#  privacy controls and multilingual public access.
# ============================================================================

domain:
  name: polis
  version: "1.0.0"
  description: >
    Municipal permitting and licensing authority. Models the full lifecycle of
    a permit from application through review, inspection, payment, issuance,
    expiry, and renewal — with departmental budgets, public notices, external
    system integrations, and privacy/retention controls. Authored as a complete
    ADL v0.3 conformance exemplar.

  # --- §17.8 API prefix & aliases ------------------------------------------
  config:
    api:
      prefix: /api/v1/polis
      aliases:
        - /api/v1/permits

  # --- §26.5 Domain locale (formatting defaults) ---------------------------
  locale:
    currency: USD
    dateFormat: "MMM d, yyyy"
    numberFormat:
      thousandsSeparator: ","
      decimalSeparator: "."

  # --- §34 Localization / i18n ---------------------------------------------
  localization:
    defaultLocale: en
    supportedLocales: [en, es, fr, zh]
    fallbackChain: [requestedLocale, en]
    dateFormat:
      en: "MMM d, yyyy"
      es: "d 'de' MMM 'de' yyyy"
      fr: "d MMM yyyy"
      zh: "yyyy年M月d日"
    numberFormat:
      en: { thousands: ",", decimal: ".", currencySymbol: "$", currencyPosition: before }
      es: { thousands: ".", decimal: ",", currencySymbol: "$", currencyPosition: before }
      fr: { thousands: " ", decimal: ",", currencySymbol: "$", currencyPosition: after }
      zh: { thousands: ",", decimal: ".", currencySymbol: "$", currencyPosition: before }
    translations:
      es:
        resources:
          Permit:
            label: "Permiso"
            plural: "Permisos"
            fields:
              permitNumber: "Número de Permiso"
              status: "Estado"
        enums:
          PermitStatus:
            approved: "Aprobado"
            rejected: "Rechazado"
            under_review: "En Revisión"
        validation:
          messages:
            required: "es obligatorio"
      fr:
        resources:
          Permit:
            label: "Permis"
            plural: "Permis"

  # --- §33 Privacy / GDPR --------------------------------------------------
  privacy:
    enabled: true
    classifications:
      personalData:
        description: "Standard personal data (GDPR Art. 6)"
        retention: 7 years
      sensitiveData:
        description: "Special category data (GDPR Art. 9)"
        retention: 10 years
      financialData:
        description: "Payment and financial records"
        retention: 10 years
    subjectIdentifier:
      primary: email
      alternates: [ssn, applicantNumber]
    anonymization:
      method: anonymize
      rules:
        default: "Redacted"
        firstName: "Redacted"
        lastName: "Redacted"
        email: "{$pseudonym}@redacted.local"
        dateOfBirth: "$fuzzyDate(5 years)"
        ssn: "$hash6"
        applicantNumber: "$preserve"
    encryption:
      provider: aes256gcm
      keySource: environment
      keyVariable: "${ENCRYPTION_KEY}"
      keyRotation: 365 days
    consent:
      - purpose: permit_processing
        description: "Process the application and issue permits"
        required: true
        withdrawable: false
      - purpose: marketing_updates
        description: "Send optional service announcements"
        required: false
        withdrawable: true
        resources: [Applicant]
    retention:
      - category: personalData
        duration: 7 years
        action: anonymize
        from: lastActivity
      - category: sensitiveData
        duration: 10 years
        action: archive
        from: recordCreation
      - category: financialData
        duration: 10 years
        action: delete
        from: lastActivity

  # =========================================================================
  #  §6.7 Reusable properties (atomic type aliases)
  # =========================================================================
  properties:
    emailAddr:
      type: email
      lowercase: true
      maxLength: 255
    phoneNum:
      type: phone
      pattern: "^\\+?[1-9]\\d{1,14}$"
    slugField:
      type: slug
      unique: true
    postalCode:
      type: string
      pattern: "^[0-9]{5}(-[0-9]{4})?$"
      maxLength: 10

  # =========================================================================
  #  §6.7 / §10 Reusable objects (mixins / composition)
  # =========================================================================
  objects:
    Timestamp:
      createdAt: { type: datetime, autoNow: create }
      updatedAt: { type: datetime, autoNow: always }
    SoftDelete:
      deletedAt: { type: datetime }
    AuditMeta:
      createdById: { type: uuid, internal: true }
      createdByName: { type: string, maxLength: 200 }
    Address:
      line1: { type: string, maxLength: 255, required: true }
      line2: { type: string, maxLength: 255 }
      city: { type: string, maxLength: 120, required: true }
      state: { type: string, maxLength: 2, uppercase: true }
      postalCode: { $ref: '#/properties/postalCode' }
      country: { type: country, default: "US" }

  # =========================================================================
  #  §9 Enumerations (rich — label/description/color/icon/final)
  # =========================================================================
  enums:
    PermitStatus:
      values:
        draft:               { label: "Draft",               description: "Applicant is preparing the application", color: "#94a3b8", icon: "lucide:pencil" }
        submitted:           { label: "Submitted",           description: "Submitted for intake review",            color: "#3b82f6", icon: "lucide:send" }
        under_review:        { label: "Under Review",         description: "Plan examiners are reviewing",           color: "#f59e0b", icon: "lucide:search" }
        revision_requested:  { label: "Revision Requested",   description: "Changes requested from applicant",       color: "#f97316", icon: "lucide:rotate-ccw" }
        inspection_scheduled: { label: "Inspection Scheduled", description: "Awaiting field inspection",              color: "#8b5cf6", icon: "lucide:calendar-clock" }
        approved:            { label: "Approved",             description: "Permit issued and active",               color: "#10b981", icon: "lucide:badge-check" }
        rejected:            { label: "Rejected",             description: "Application denied",                     color: "#ef4444", icon: "lucide:x-circle", final: true }
        expired:             { label: "Expired",              description: "Permit lapsed past expiry",              color: "#6b7280", icon: "lucide:hourglass", final: true }
        withdrawn:           { label: "Withdrawn",            description: "Withdrawn by applicant",                 color: "#64748b", icon: "lucide:undo-2", final: true }
    InspectionStatus:
      values:
        scheduled:   { label: "Scheduled",   color: "#3b82f6", icon: "lucide:calendar" }
        in_progress: { label: "In Progress", color: "#f59e0b", icon: "lucide:loader" }
        passed:      { label: "Passed",      color: "#10b981", icon: "lucide:check", final: true }
        failed:      { label: "Failed",      color: "#ef4444", icon: "lucide:x", final: true }
        cancelled:   { label: "Cancelled",   color: "#6b7280", icon: "lucide:ban", final: true }
    PaymentStatus:
      values:
        pending:  { label: "Pending",  color: "#f59e0b" }
        paid:     { label: "Paid",     color: "#10b981", final: true }
        refunded: { label: "Refunded", color: "#8b5cf6", final: true }
        failed:   { label: "Failed",   color: "#ef4444" }
    ApplicantType:
      values:
        resident:   { label: "Resident",   color: "#3b82f6" }
        business:   { label: "Business",   color: "#8b5cf6" }
        contractor: { label: "Contractor", color: "#f59e0b" }
        government: { label: "Government",  color: "#64748b" }
    RiskLevel:
      values:
        low:      { label: "Low",      color: "#10b981" }
        moderate: { label: "Moderate", color: "#f59e0b" }
        high:     { label: "High",     color: "#f97316" }
        critical: { label: "Critical", color: "#ef4444" }
    NoticeSeverity:
      values:
        info:    { label: "Info",    color: "#3b82f6" }
        notice:  { label: "Notice",  color: "#f59e0b" }
        warning: { label: "Warning", color: "#f97316" }
        urgent:  { label: "Urgent",  color: "#ef4444" }

  # =========================================================================
  #  §18.4 Custom block definitions (for `blocks` content fields)
  # =========================================================================
  blocks:
    Callout:
      label: "Callout"
      hasContent: true
      props:
        style: { type: enum, values: [info, warning, tip, danger], default: info }
        title: { type: string, maxLength: 120, required: false }
    NoticeBanner:
      label: "Notice Banner"
      hasContent: false
      props:
        heading: { type: string, required: true }
        severity: { type: enum, values: [info, notice, warning, urgent], default: info }
        ctaText: { type: string, default: "Read more" }
        ctaUrl: { type: url, required: false }

  # =========================================================================
  #  §18.5 Icon sets
  # =========================================================================
  icons:
    lucide:
      source: cdn
      url: "https://cdn.jsdelivr.net/npm/lucide-static@latest/icons/"
      prefix: lucide
      format: svg
    seal:
      source: local
      path: ./icons/
      prefix: seal
      format: svg

  # =========================================================================
  #  §18.6 Font sources
  # =========================================================================
  fonts:
    google:
      source: google-fonts
      preload: [Inter, "Source Serif 4", "JetBrains Mono"]
    system:
      source: system
      families: [Arial, Helvetica, Georgia, Menlo, monospace]

  # =========================================================================
  #  §22 Domain-level RULES (cross-resource business policies)
  # =========================================================================
  rulesConfig:
    logAllEvaluations: false
    logRetentionDays: 730

  rules:
    _groups:
      buildingCode:
        description: "Local building-code enforcement rules"
        rules: [inspectionRequiredBeforeApproval, highRiskNeedsSeniorReviewer]
        enabled: true
      finance:
        description: "Fee and payment controls"
        rules: [feesMustBePaidBeforeIssuance, largeRefundWarning]
        enabled: true

    feesMustBePaidBeforeIssuance:
      description: "A permit cannot be approved while a balance is owed."
      group: finance
      priority: 10
      trigger:
        resource: Permit
        on: [transition]
        transition: approve
      condition: >
        sum(Payment, 'amount', permitId == $record.id && status == 'paid')
        < $record.totalFeeAssessed
      action: reject
      severity: error
      message: "Permit {permitNumber} has an outstanding balance of {balanceOwed} and cannot be issued."
      messageData:
        balanceOwed: >
          $record.totalFeeAssessed
          - sum(Payment, 'amount', permitId == $record.id && status == 'paid')
      regulation: "Municipal Code §14.2 — Fee Collection"

    inspectionRequiredBeforeApproval:
      description: "At least one passed inspection is required before issuance."
      group: buildingCode
      priority: 20
      dependsOn: feesMustBePaidBeforeIssuance
      trigger:
        resource: Permit
        on: [transition]
        transition: approve
      condition: >
        count(Inspection, permitId == $record.id && status == 'passed') == 0
      action: reject
      severity: error
      message: "Permit {permitNumber} requires at least one passed inspection before approval."
      regulation: "IBC §110 — Inspections"
      notification:
        channels: [email]
        recipients:
          field: createdById
        template:
          subject: "Approval blocked — {permitNumber}"
          body: "An inspection must pass before {permitNumber} can be approved."

    highRiskNeedsSeniorReviewer:
      description: "High/critical risk permits must have a senior reviewer assigned."
      group: buildingCode
      priority: 30
      trigger:
        resource: Permit
        on: [transition]
        transition: approve
        condition: "$record.riskLevel == 'high' || $record.riskLevel == 'critical'"
      condition: >
        count(PermitReviewer, permitId == $record.id && senior == true) == 0
      action: reject
      severity: error
      message: "High-risk permit {permitNumber} requires a senior reviewer."
      regulation: "Municipal Code §14.9 — Review Standards"

    largeRefundWarning:
      description: "Flag refunds over $5,000 for finance review (non-blocking)."
      group: finance
      priority: 100
      trigger:
        resource: Payment
        on: [create, update]
        fields: [amount, status]
        condition: "$record.status == 'refunded'"
      condition: "$record.amount > 5000"
      action: warn
      severity: warning
      message: "Refund of {amount} on payment {id} exceeds the $5,000 review threshold."
      regulation: "Municipal Code §14.4 — Refunds"
      history:
        event: "largeRefundFlagged"
        label: "Large refund flagged for review"

    duplicateApplicantSsn:
      description: "Log when an SSN matches an existing applicant (fraud signal)."
      priority: 100
      trigger:
        resource: Applicant
        on: [create]
      condition: >
        count(Applicant, ssn == $record.ssn && id != $record.id) > 0
      action: log
      severity: info
      message: "Applicant {applicantNumber} shares an SSN with an existing record."

  # =========================================================================
  #  §23 NOTIFICATIONS (standalone, conditional, digest, escalation)
  # =========================================================================
  notifications:
    permitApprovedApplicant:
      on: history
      event: approved
      channels: [email]
      recipients:
        field: createdById
      template:
        subject: "Your permit {permitNumber} has been approved"
        body: "Good news — permit {permitNumber} is now active. It expires on {expiresAt}."

    highValuePermitAlert:
      on: history
      event: submitted
      condition: "$record.totalFeeAssessed > 25000"
      channels: [email, slack]
      recipients:
        roles: [financeOfficer]
      template:
        subject: "High-value permit submitted — ${totalFeeAssessed}"
        body: "Permit {permitNumber} was submitted with assessed fees of ${totalFeeAssessed}."
      priority: high
      throttle: 1h

    dailyIntakeDigest:
      type: digest
      schedule: "daily at 07:00"
      channels: [email]
      recipients:
        roles: [clerk, admin]
      query:
        resource: "*"
        history: true
        filter: "severity == 'warning' || severity == 'error'"
        since: 24h
      template:
        subject: "Daily permitting summary — {$date}"
        body: |
          ## Last 24 hours

          **Errors:** {errorCount}
          **Warnings:** {warningCount}

          {#each errors}
          - [{event}] {label} — {createdAt}
          {/each}
      emptyBehavior: skip

    inspectionFailureEscalation:
      on: history
      event: inspectionFailed
      channels: [email]
      recipients:
        field: createdById
      template:
        subject: "Inspection failed — {permitNumber}"
        body: "Inspection for {permitNumber} did not pass. Please review the report."
      priority: high
      escalation:
        - after: 48h
          channels: [email, slack]
          recipients:
            roles: [inspectorLead]
          template:
            subject: "ESCALATION — failed inspection unresolved — {permitNumber}"
            body: "Inspection failure on {permitNumber} has not been resolved in 48h."
          priority: urgent
        - after: 96h
          channels: [email, sms]
          recipients:
            roles: [admin]
          template:
            subject: "FINAL ESCALATION — {permitNumber}"
            body: "Inspection failure on {permitNumber} remains unresolved after 96h."
          priority: critical

  # =========================================================================
  #  §24 SCHEDULED TASKS (recurring, relative-tiered, batched, report)
  # =========================================================================
  schedules:
    expirePermits:
      description: "Expire active permits past their expiry date."
      recurrence: daily at 02:00
      source:
        resource: Permit
        filter: "status == 'approved' && expiresAt < now()"
      action: transition
      transition: expire

    flagStalePermits:
      description: "Flag applications stuck in review for 30+ days."
      recurrence: daily at 03:00
      source:
        resource: Permit
        filter: "status == 'under_review' && daysOld(updatedAt) > 30 && stale == false"
      action: update
      input:
        stale: true

    renewalReminders:
      description: "Tiered reminders before a permit expires."
      type: relative
      source:
        resource: Permit
        filter: "status == 'approved'"
      relativeTo: expiresAt
      tiers:
        - daysUntil: 60
          action: notify
          tag: renew60
          channels: [email]
          recipients:
            field: createdById
          template:
            subject: "Permit {permitNumber} expires in 60 days"
            body: "Renew {permitNumber} before {expiresAt} to avoid a lapse."
        - daysUntil: 14
          action: notify
          tag: renew14
          channels: [email, sms]
          recipients:
            field: createdById
            roles: [clerk]
          template:
            subject: "Permit {permitNumber} expires in 14 days"
            body: "Action required: renew {permitNumber} before {expiresAt}."
        - daysUntil: 0
          action: transition
          transition: expire
          notification:
            channels: [email]
            recipients:
              field: createdById
            template:
              subject: "Permit {permitNumber} has expired"
              body: "{permitNumber} expired on {expiresAt}."

    quarterlyComplianceReport:
      description: "Generate and email the quarterly compliance report."
      recurrence: quarterly on 1st at 06:00
      action: report
      report: complianceQuarterly
      recipients:
        roles: [admin, financeOfficer]
      channels: [email]
      format: pdf

    auditRetentionCleanup:
      description: "Archive audit rows past retention."
      recurrence: monthly on 1st at 02:30
      source:
        resource: "*"
        table: "_audit"
        filter: "daysOld(created_at) > 730"
      action: archive
      destination: "${ARCHIVE_PATH}/audit/{year}/{month}/"
      format: csv
      deleteAfterArchive: true

  # =========================================================================
  #  §25 WORKFLOWS (cross-resource orchestration)
  # =========================================================================
  workflows:
    permitReviewProcess:
      description: "End-to-end review: assign reviewer, await review, schedule inspection, await result."
      trigger:
        resource: Permit
        event: transition
        transition: submit
        condition: "$trigger.record.totalFeeAssessed >= 0"
      steps:
        - name: openReview
          action: transition
          resource: Permit
          target: $trigger.record.id
          transition: beginReview
          then: routeByRisk

        - name: routeByRisk
          decide:
            - condition: "$trigger.record.riskLevel == 'critical' || $trigger.record.riskLevel == 'high'"
              then: parallelChecks
            - default: true
              then: scheduleInspection

        - name: parallelChecks
          parallel:
            all: true
            steps:
              - name: awaitSeniorReview
                wait:
                  resource: Permit
                  event: transition
                  transition: seniorSignoff
                  filter:
                    id: $trigger.record.id
              - name: awaitFinanceClear
                wait:
                  resource: Payment
                  event: transition
                  transition: markPaid
                  filter:
                    permitId: $trigger.record.id
          then: scheduleInspection

        - name: scheduleInspection
          action: create
          resource: Inspection
          input:
            permitId: $trigger.record.id
            inspectionType: routine
          then: awaitInspection

        - name: awaitInspection
          wait:
            resource: Inspection
            event: transition
            transition: pass
            filter:
              permitId: $trigger.record.id
          timeout:
            duration: 30d
            then: escalateInspection
          condition: "$event.record.status == 'passed'"
          then: finalizeApproval
          else: requestRevision

        - name: finalizeApproval
          action: transition
          resource: Permit
          target: $trigger.record.id
          transition: approve
          then: end

        - name: requestRevision
          action: transition
          resource: Permit
          target: $trigger.record.id
          transition: requestRevision
          then: end

        - name: escalateInspection
          action: notify
          resource: Permit
          target: $trigger.record.id
          notification:
            channels: [email]
            recipients:
              roles: [inspectorLead]
            template:
              subject: "Inspection overdue — {permitNumber}"
              body: "No inspection result for {permitNumber} within 30 days."
          then: end
      timeout:
        duration: 120d
        action: transition
        resource: Permit
        target: $trigger.record.id
        transition: expire
      onError:
        action: notify
        resource: Permit
        target: $trigger.record.id
        notification:
          channels: [email]
          recipients:
            roles: [admin]
          template:
            subject: "Workflow error — permitReviewProcess"
            body: "Workflow instance {$workflow.id} failed at step {$workflow.failedStep}."

  # =========================================================================
  #  §27 TEMPLATES / DOCUMENTS
  # =========================================================================
  templates:
    permitCertificate:
      description: "Official permit certificate generated on approval."
      trigger:
        resource: Permit
        event: transition
        transition: approve
      format: pdf
      filename: "Permit-{permitNumber}-{$date}.pdf"
      permissions:
        generate: [clerk, admin]
        view: [authenticated]
      data:
        permit: $record
        applicant:
          source: Applicant
          filter: "id == $record.applicantId"
          single: true
        inspections:
          source: Inspection
          filter: "permitId == $record.id && status == 'passed'"
          sort: completedAt asc
        agency:
          static:
            name: "${AGENCY_NAME}"
            address: "${AGENCY_ADDRESS}"
      layout:
        pageSize: letter
        margins: { top: 1in, right: 1in, bottom: 1in, left: 1in }
        header:
          logo: "${AGENCY_LOGO_PATH}"
          text: "Department of Permits & Licensing"
        footer:
          left: "Generated {$datetime}"
          center: "Page {$page} of {$pages}"
          right: "OFFICIAL"
      body: |
        # Certificate of Permit

        **Permit Number:** {permit.permitNumber}
        **Issued:** {$date}

        ## Permit Holder
        **Name:** {applicant.firstName} {applicant.lastName}
        **Email:** {applicant.email}

        ## Details
        | Field | Value |
        |-------|-------|
        | Type | {permit.permitTypeName} |
        | Risk Level | {permit.riskLevel} |
        | Assessed Fees | {permit.totalFeeAssessed | money} |
        | Expires | {permit.expiresAt | date} |

        ## Passed Inspections
        {#each inspections}
        - {inspectionType} — passed {completedAt | date}
        {/each}
      attachTo:
        resource: Permit
        recordId: $record.id
        field: certificateDocId
      notification:
        channels: [email]
        recipients:
          field: createdById
        template:
          subject: "Permit certificate — {permit.permitNumber}"
          body: "Your official permit certificate is attached."

    denialLetter:
      description: "Denial letter generated when a permit is rejected."
      trigger:
        resource: Permit
        event: transition
        transition: reject
      format: pdf
      filename: "Denial-{permitNumber}.pdf"
      permissions:
        generate: [reviewer, admin]
        view: [authenticated]
      data:
        permit: $record
      layout:
        pageSize: letter
      body: |
        # Notice of Denial

        Permit **{permit.permitNumber}** has been denied.

        **Reason:** {permit.decisionReason}

        You may appeal within 30 days.

  # =========================================================================
  #  §35 EXTERNAL INTEGRATIONS (REST out, webhook in, S3, SMTP)
  # =========================================================================
  integrations:
    statePermitRegistry:
      description: "Report issued permits to the state registry."
      type: rest
      baseUrl: "https://api.state.example.gov/permits/v1"
      auth:
        type: oauth2_client_credentials
        tokenUrl: "https://auth.state.example.gov/token"
        clientId: "${STATE_CLIENT_ID}"
        clientSecret: "${STATE_CLIENT_SECRET}"
        scopes: ["permits.write"]
        tokenCache: true
      retry:
        maxAttempts: 3
        backoff: exponential
        baseDelay: 1s
        maxDelay: 30s
        retryOn: [500, 502, 503, 504, 429]
      circuitBreaker:
        enabled: true
        failureThreshold: 5
        successThreshold: 2
        timeout: 60s
      rateLimit:
        requestsPerSecond: 10
        burstSize: 20
        onExceeded: queue
      deadLetter:
        enabled: true
        maxRetries: 3
        notifyOn: [integration_failure]
      operations:
        reportIssuance:
          trigger:
            resource: Permit
            event: transition
            transition: approve
          request:
            method: POST
            path: "/issuances"
            headers:
              Content-Type: "application/json"
            body:
              permit_number: $record.permitNumber
              jurisdiction: "${JURISDICTION_CODE}"
              issued_at: "$formatDate($now(), 'yyyy-MM-dd')"
              fee_total: $record.totalFeeAssessed
          onSuccess:
            action: update
            resource: Permit
            target: $record.id
            input:
              stateRegistryId: $response.registry_id
            history:
              event: "reportedToState"
              label: "Reported to state registry — {stateRegistryId}"
          onError:
            action: notify
            resource: Permit
            target: $record.id

    sponsorWebhook:
      description: "Receive inspection results from the field-inspection vendor."
      type: webhook
      path: /webhooks/field-results
      auth:
        type: hmac_signature
        secret: "${INSPECTION_WEBHOOK_SECRET}"
        header: X-Webhook-Signature
        algorithm: sha256
        encoding: hex
        prefix: "sha256="
      replayProtection:
        enabled: true
        timestampHeader: X-Webhook-Timestamp
        maxAge: 5 minutes
        nonceHeader: X-Webhook-Nonce
      events:
        resultPosted:
          filter:
            field: "event_type"
            value: "inspection_result"
          mapping:
            inspectionId: $payload.inspection_id
            result: $payload.result
            completedAt: "$parseDate($payload.completed_at, 'yyyy-MM-dd')"
          action: update
          resource: Inspection
          target: "$findOne(Inspection, id == $mapped.inspectionId).id"
          input:
            rawResult: $mapped.result
            completedAt: $mapped.completedAt

    documentStorage:
      description: "Store generated permit documents in object storage."
      type: s3
      bucket: "${S3_BUCKET}"
      region: "${AWS_REGION}"
      auth:
        type: aws_sigv4
        accessKeyId: "${AWS_ACCESS_KEY_ID}"
        secretAccessKey: "${AWS_SECRET_ACCESS_KEY}"

    mailServer:
      description: "Outbound email for notifications and documents."
      type: smtp
      host: "${SMTP_HOST}"
      port: 587
      auth:
        type: basic
        username: "${SMTP_USER}"
        password: "${SMTP_PASS}"

  # =========================================================================
  #  §26 VIEWS
  # =========================================================================
  views:
    activePermits:
      description: "Approved permits with fee utilization and days remaining."
      source: Permit
      filter: "status == 'approved'"
      permissions: [clerk, reviewer, admin]
      columns:
        - { field: permitNumber, label: "Permit #" }
        - { field: permitTypeName, label: "Type" }
        - { field: totalFeeAssessed, label: "Assessed", format: money }
        - { name: collected, label: "Collected", computed: "sum(Payment, 'amount', permitId == $record.id && status == 'paid')", format: money }
        - { name: balance, label: "Balance", computed: "totalFeeAssessed - collected", format: money }
        - { field: expiresAt, label: "Expires", format: date }
        - name: daysLeft
          label: "Days Left"
          computed: "daysUntil(expiresAt)"
          highlight:
            red: "< 14"
            yellow: "< 60"
            green: ">= 60"
      sort: daysLeft asc
      defaultLimit: 50

  # =========================================================================
  #  §26 REPORTS
  # =========================================================================
  reports:
    complianceQuarterly:
      description: "Quarterly permitting and fee-collection summary."
      permissions: [admin, financeOfficer]
      header:
        title: "Quarterly Compliance Report"
        subtitle: "Quarter {$params.quarter} — {$params.year}"
      parameters:
        - { name: year, type: integer, default: "$currentYear", label: "Year" }
        - { name: quarter, type: string, default: "$currentQuarter", label: "Quarter" }
        - { name: department, type: string, optional: true, label: "Department", enum: "distinct(Permit, 'departmentName')" }
      sections:
        - name: summary
          type: metrics
          metrics:
            - { label: "Permits Issued", value: "count(Permit, status == 'approved')", format: integer }
            - { label: "Fees Assessed", value: "sum(Permit, 'totalFeeAssessed', status == 'approved')", format: money }
            - { label: "Fees Collected", value: "sum(Payment, 'amount', status == 'paid')", format: money }
            - { label: "Collection Rate", value: "sum(Payment, 'amount', status == 'paid') / sum(Permit, 'totalFeeAssessed', status == 'approved') * 100", format: percentage }
        - name: detail
          type: table
          view: activePermits
          groupBy: permitTypeName
          totals: [totalFeeAssessed]
          emptyMessage: "No active permits for the selected period."
      footer:
        text: "Generated {$now} by {$actor.name}"
        confidentiality: "INTERNAL — Not for public distribution"

  # =========================================================================
  #  §26 DASHBOARDS
  # =========================================================================
  dashboards:
    operationsOverview:
      description: "Live permitting operations dashboard."
      permissions: [clerk, admin, financeOfficer]
      refresh: 5m
      layout: { columns: 3 }
      widgets:
        - name: approvalRate
          type: gauge
          position: { row: 1, col: 1 }
          query:
            value: "count(Permit, status == 'approved') / count(Permit, status != 'draft' && status != 'withdrawn') * 100"
          label: "Approval Rate"
          format: percentage
          thresholds: { green: ">= 80", yellow: ">= 60", red: "< 60" }
          target: 85
        - name: overduePermits
          type: counter
          position: { row: 1, col: 2 }
          query:
            value: "count(Permit, status == 'under_review' && daysOld(updatedAt) > 30)"
          label: "Stale Reviews"
          icon: lucide:alert-triangle
          thresholds: { green: "== 0", yellow: "<= 3", red: "> 3" }
        - name: feesCollected
          type: scorecard
          position: { row: 1, col: 3 }
          query:
            value: "sum(Payment, 'amount', status == 'paid')"
          label: "Fees Collected"
          format: money
        - name: permitsByStatus
          type: breakdown
          position: { row: 2, col: 1 }
          query:
            resource: Permit
            groupBy: status
            metric: count
          label: "Permits by Status"
        - name: intakeTrend
          type: timeSeries
          position: { row: 2, col: 2, span: 2 }
          query:
            resource: Permit
            metric: count
            groupBy: "$dateGroup(createdAt, 'week')"
            filter: "createdAt > dateAdd(now(), -90, 'day')"
          label: "Submissions — Last 90 Days"
        - name: recentAlerts
          type: timeline
          position: { row: 3, col: 1, span: 3 }
          query:
            resource: "*"
            history: true
            filter: "severity == 'error' || severity == 'warning'"
            limit: 10
          label: "Recent Alerts"

  # =========================================================================
  #  §7–§20 RESOURCES
  # =========================================================================
  resources:

    # ----------------------------------------------------------------------
    #  SiteConfig — §17.4 singleton; demonstrates component/svg/font/json/color
    # ----------------------------------------------------------------------
    SiteConfig:
      description: "Agency-wide configuration. Exactly one record (singleton)."
      api:
        disable_list: true
        disable_create: true
        disable_delete: true
      object:
        id: { type: uuid, primaryKey: true, allowUserProvided: true }
        agencyName: { type: string, required: true, maxLength: 200 }
        logoSvg: { type: svg, maxSize: 50kb }
        headingFont: { type: font, sources: [google, system], default: "Inter" }
        bodyFont: { type: font, sources: [google, system], default: "Source Serif 4" }
        primaryColor: { type: color, default: "#1e3a8a" }
        branding:
          type: component
          collapsed: false
          object:
            tagline: { type: string, maxLength: 200 }
            accentColor: { type: color, default: "#f59e0b" }
            supportEmail: { $ref: '#/properties/emailAddr' }
        featureFlags: { type: json, description: "Arbitrary runtime flags" }
        $merge:
          - { $ref: '#/objects/Timestamp' }
      permissions:
        get: ["*"]
        update: [admin]

    # ----------------------------------------------------------------------
    #  Department — org unit; timezone; reactive cross-resource computed field
    # ----------------------------------------------------------------------
    Department:
      description: "An agency department that owns permits and employs inspectors."
      features:
        audit: true
        search: true
        export: true
      object:
        id: { type: uuid, primaryKey: true }
        name: { type: string, required: true, unique: true, maxLength: 150 }
        slug: { $ref: '#/properties/slugField', from: name }
        code: { type: string, required: true, unique: true, uppercase: true, pattern: "^[A-Z]{2,5}$" }
        timezone: { type: timezone, default: "America/New_York" }
        indirectRate: { type: percentage, min: 0, max: 100, default: 12.5 }
        contactEmail: { $ref: '#/properties/emailAddr' }
        $merge:
          - { $ref: '#/objects/Timestamp' }
      computed:
        # §13.7 reactive: recomputes across ALL permits in this department
        totalAssessedFees:
          type: money
          formula: "sum(Permit, 'totalFeeAssessed', departmentId == $this.id)"
          stored: true
          reactive: true
          recomputeOn: [Permit.create, Permit.update, Permit.delete]
      relationships:
        inspectors: { type: hasMany, target: Inspector, foreignKey: departmentId }
        permits: { type: hasMany, target: Permit, foreignKey: departmentId }
        budgets: { type: hasMany, target: BudgetWorksheet, foreignKey: departmentId }
      indexes:
        - { fields: [code], unique: true }
        - { fields: [name] }
      permissions:
        list: ["*"]
        get: ["*"]
        create: [admin]
        update: [admin]
        delete: [admin]

    # ----------------------------------------------------------------------
    #  PermitType — config catalog; self-referential tree; icon/markdown/money
    # ----------------------------------------------------------------------
    PermitType:
      description: "Catalog of permit/license types, nestable into a category tree."
      features:
        audit: true
        search: true
        caching: true
        cacheTtlSeconds: 600
      object:
        id: { type: uuid, primaryKey: true }
        name: { type: string, required: true, maxLength: 200 }
        slug: { $ref: '#/properties/slugField', from: name }
        parentId: { type: uuid, description: "Parent type for nesting (null = top level)" }
        icon: { type: icon, iconSets: [lucide, seal], default: "lucide:file-text" }
        color: { type: color, default: "#3b82f6" }
        baseFee: { type: money, min: 0, required: true }
        defaultRisk: { $ref: '#/enums/RiskLevel', default: low }
        processingDays: { type: integer, min: 0, default: 30 }
        instructions: { type: markdown, description: "Applicant-facing guidance (markdown)" }
        active: { type: boolean, default: true }
        sortOrder: { type: integer, default: 0 }
        $merge:
          - { $ref: '#/objects/Timestamp' }
      relationships:
        parent: { type: belongsTo, target: PermitType, foreignKey: parentId, onDelete: restrict }
        children: { type: hasMany, target: PermitType, foreignKey: parentId }
        permits: { type: hasMany, target: Permit, foreignKey: permitTypeId }
      indexes:
        - { fields: [parentId] }
        - { fields: [active, sortOrder] }
      permissions:
        list: ["*"]
        get: ["*"]
        create: [admin]
        update: [admin]
        delete: [admin]

    # ----------------------------------------------------------------------
    #  Applicant — PII-heavy; privacy; password/token/bytes/set/language; owner
    # ----------------------------------------------------------------------
    Applicant:
      description: "A resident, business, or contractor who applies for permits."
      features:
        softDelete: true
        audit: true
        versioning: true
        search: true
        export: true
        import: true
        validationMode: strict
      object:
        id: { type: uuid, primaryKey: true, allowUserProvided: true }
        applicantNumber: { type: autoincrement }
        type: { $ref: '#/enums/ApplicantType', default: resident }
        firstName:
          type: string
          required: true
          maxLength: 100
          trim: true
          privacy: { classification: personalData, pii: true }
        lastName:
          type: string
          required: true
          maxLength: 100
          trim: true
          privacy: { classification: personalData, pii: true }
        email:
          $ref: '#/properties/emailAddr'
          required: true
          unique: true
          privacy: { classification: personalData, pii: true, subjectIdentifier: true }
        phone: { $ref: '#/properties/phoneNum', privacy: { classification: personalData, pii: true } }
        dateOfBirth:
          type: date
          privacy: { classification: sensitiveData, pii: true }
        ssn:
          type: string
          pattern: "^[0-9]{3}-[0-9]{2}-[0-9]{4}$"
          mask: true
          privacy: { classification: sensitiveData, pii: true, encrypted: true, subjectIdentifier: true }
        preferredLanguage: { type: language, default: "en" }
        preferredChannels: { type: set, items: { type: string }, description: "email | sms | mail" }
        mailingAddress:
          type: component
          object: { $ref: '#/objects/Address' }
          collapsed: true
        portalPin: { type: password, description: "Self-service portal credential (hashed)" }
        portalToken: { type: token, description: "Auto-generated portal access token" }
        signatureImage: { type: bytes, maxSize: 200000, description: "Captured signature bitmap" }
        legacyImportedAt: { type: timestamp, hidden: true, requestable: true }
        notes: { type: text, maxLength: 5000 }
        $merge:
          - { $ref: '#/objects/Timestamp' }
          - { $ref: '#/objects/SoftDelete' }
          - { $ref: '#/objects/AuditMeta' }
      computed:
        fullName:
          type: string
          formula: "concat(firstName, ' ', lastName)"
        permitCount:
          type: integer
          formula: "count(permits)"
          stored: true
          updateOn: [permits.create, permits.delete]
      relationships:
        permits: { type: hasMany, target: Permit, foreignKey: applicantId, orderBy: { createdAt: desc } }
      validation:
        adultIfBusinessOwner:
          rule: "type != 'business' || dateOfBirth == null || yearsBetween(dateOfBirth, now()) >= 18"
          message: "Business owners must be at least 18."
        uniqueSsn:
          rule: "ssn == null || !exists('Applicant', { ssn: ssn, id_ne: $this.id })"
          message: "An applicant with this SSN already exists."
          when: create
      indexes:
        - { fields: [email], unique: true }
        - { fields: [type] }
        - { fields: [deletedAt, type], description: "Active applicants by type" }
      permissions:
        list: [clerk, reviewer, admin]
        get: "isOwner() || hasAnyRole('clerk','reviewer','admin')"
        create: [authenticated]
        update: [owner, clerk, admin]
        delete: [admin]
        fields:
          ssn:
            read: [privacyOfficer, admin]
            write: [privacyOfficer, admin]
            mask: true
          notes:
            read: [clerk, reviewer, admin]
            write: [clerk, admin]
          legacyImportedAt:
            write: [admin]

    # ----------------------------------------------------------------------
    #  Inspector — staff; certification expiry feeds a relative schedule
    # ----------------------------------------------------------------------
    Inspector:
      description: "Field inspector employed by a department."
      features:
        audit: true
        search: true
      object:
        id: { type: uuid, primaryKey: true, allowUserProvided: true }
        name: { type: string, required: true, maxLength: 200 }
        email: { $ref: '#/properties/emailAddr', required: true, unique: true }
        departmentId: { type: uuid, required: true, immutable: true }
        certification: { type: string, maxLength: 120 }
        certExpiresAt: { type: date }
        active: { type: boolean, default: true }
        senior: { type: boolean, default: false }
        $merge:
          - { $ref: '#/objects/Timestamp' }
      relationships:
        department: { type: belongsTo, target: Department, foreignKey: departmentId, onDelete: restrict, required: true }
        inspections: { type: hasMany, target: Inspection, foreignKey: inspectorId }
      indexes:
        - { fields: [departmentId] }
        - { fields: [active] }
      permissions:
        list: [authenticated]
        get: [authenticated]
        create: [admin]
        update: [admin]
        delete: [admin]

    # ----------------------------------------------------------------------
    #  Permit — the core lifecycle resource. State machine, history, computed,
    #  conditional permissions, scopes, nearly every scalar type.
    # ----------------------------------------------------------------------
    Permit:
      description: >
        A permit/license application and its full lifecycle. The heart of the
        domain — exercises the state machine, history, rules, workflows,
        templates, integrations, computed aggregations, and conditional RBAC.
      features:
        softDelete: true
        audit: true
        versioning: true
        search: true
        export: true
        caching: true
        cacheTtlSeconds: 120
        transactions: true
        validationMode: strict
      object:
        id: { type: uuid, primaryKey: true, allowUserProvided: true }
        caseSequence: { type: autoincrement }
        permitNumber: { type: string, required: true, unique: true, immutable: true, pattern: "^PMT-[0-9]{4}-[0-9]{4}$", uppercase: true }
        title: { type: string, required: true, maxLength: 250, trim: true }
        slug: { $ref: '#/properties/slugField', from: title }
        status: { $ref: '#/enums/PermitStatus', default: draft }
        riskLevel: { $ref: '#/enums/RiskLevel', default: low }
        applicantId: { type: uuid, required: true, immutable: true }
        permitTypeId: { type: uuid, required: true }
        permitTypeName: { type: string, maxLength: 200, description: "Denormalized for display/reports" }
        departmentId: { type: uuid }
        departmentName: { type: string, maxLength: 150 }
        # money / decimal / percentage
        totalFeeAssessed: { type: money, min: 0, default: 0 }
        assessedValue: { type: decimal, precision: 12, scale: 2, min: 0 }
        completionPercent: { type: percentage, min: 0, max: 100, default: 0 }
        lotSizeAcres: { type: float, min: 0 }
        # geo (float) + time + date
        siteLatitude: { type: float, min: -90, max: 90 }
        siteLongitude: { type: float, min: -180, max: 180 }
        submittedAt: { type: datetime }
        decisionAt: { type: datetime }
        decisionReason: { type: text, maxLength: 2000 }
        expiresAt: { type: date }
        # arrays / set / json / markdown
        relatedCaseNumbers: { type: array, items: { type: string }, maxItems: 20, uniqueItems: true }
        flags: { type: set, items: { type: string } }
        metadata: { type: json }
        scopeOfWork: { type: markdown }
        # content / media
        applicationForm: { type: media, accept: [application/pdf], maxSize: 10mb }
        # repeater of line items (component-like array)
        feeBreakdown:
          type: repeater
          sortable: true
          minItems: 0
          maxItems: 50
          object:
            description: { type: string, required: true, maxLength: 200 }
            category: { type: string, enum: [application, plan_review, inspection, surcharge], default: application }
            amount: { type: money, required: true, min: 0 }
        # document attach target + external id (set by integration)
        certificateDocId: { type: uuid, internal: true }
        stateRegistryId: { type: string, maxLength: 80, hidden: true, requestable: true }
        stale: { type: boolean, default: false }
        $merge:
          - { $ref: '#/objects/Timestamp' }
          - { $ref: '#/objects/SoftDelete' }
          - { $ref: '#/objects/AuditMeta' }

      # §13 computed fields: aggregation + materialized dependency chain + virtual
      computed:
        feesCollected:
          type: money
          formula: "sum(payments.amount)"
          stored: true
          updateOn: [payments.create, payments.update, payments.delete]
        inspectionCount:
          type: integer
          formula: "count(inspections)"
          stored: true
          updateOn: [inspections.create, inspections.delete]
        balanceDue:
          type: money
          formula: "totalFeeAssessed - feesCollected"
          stored: true
          updateOn: [totalFeeAssessed, feesCollected]
        isExpired:
          type: boolean
          formula: "status != 'approved' ? false : (expiresAt != null && expiresAt < now())"
        displayLabel:
          type: string
          formula: "concat(permitNumber, ' — ', title)"

      relationships:
        applicant: { type: belongsTo, target: Applicant, foreignKey: applicantId, onDelete: restrict, required: true }
        permitType: { type: belongsTo, target: PermitType, foreignKey: permitTypeId, onDelete: restrict, required: true }
        department: { type: belongsTo, target: Department, foreignKey: departmentId, onDelete: nullify }
        inspections: { type: hasMany, target: Inspection, foreignKey: permitId, orderBy: { createdAt: desc } }
        payments: { type: hasMany, target: Payment, foreignKey: permitId }
        reviewers: { type: hasMany, target: PermitReviewer, foreignKey: permitId }
        tags: { type: belongsToMany, target: PermitTag, through: PermitTagLink, foreignKey: permitId, otherKey: tagId, timestamps: true }

      # §12 state machine
      stateMachine:
        field: status
        initial: draft
        states:
          draft:
            description: "Applicant is preparing the application."
            # §12 onExit hook — may abort the transition (e.g. final completeness check)
            onExit: [validateDraftCompleteness]
            transitions:
              submit:
                to: submitted
                guards: [owner, clerk, admin]
                requiredFields: [title, applicantId, permitTypeId]
                sets:
                  submittedAt: "now()"
              withdraw:
                to: withdrawn
                guards: [owner, admin]
          submitted:
            description: "Submitted, awaiting intake."
            onEnter: [acknowledgeSubmission]
            transitions:
              beginReview:
                to: under_review
                guards: [clerk, reviewer, admin]
              withdraw:
                to: withdrawn
                guards: [owner, admin]
          under_review:
            description: "Plan examiners reviewing."
            transitions:
              requestRevision:
                to: revision_requested
                guards: [reviewer, admin]
                requiredFields: [decisionReason]
              seniorSignoff:
                to: inspection_scheduled
                guards: [reviewer, admin]
              scheduleInspection:
                to: inspection_scheduled
                guards: [clerk, reviewer, admin]
              reject:
                to: rejected
                guards: [reviewer, admin]
                requiredFields: [decisionReason]
                sets:
                  decisionAt: "now()"
          revision_requested:
            description: "Awaiting applicant revision."
            transitions:
              resubmit:
                to: under_review
                guards: [owner, clerk, admin]
          inspection_scheduled:
            description: "Inspection pending."
            transitions:
              approve:
                to: approved
                guards: [reviewer, admin]
                sets:
                  decisionAt: "now()"
                  completionPercent: 100
              reject:
                to: rejected
                guards: [reviewer, admin]
                requiredFields: [decisionReason]
                sets:
                  decisionAt: "now()"
          approved:
            description: "Issued and active."
            onEnter: [pushToSearchIndex]
            transitions:
              expire:
                to: expired
                guards: [admin]
              withdraw:
                to: withdrawn
                guards: [admin]
          rejected:
            description: "Denied — terminal."
            final: true
          expired:
            description: "Lapsed — terminal."
            final: true
          withdrawn:
            description: "Withdrawn — terminal."
            final: true

      # §21 history of change
      history:
        events:
          created:
            on: create
            capture: [permitNumber, title, riskLevel]
            label: "Permit {permitNumber} created"
            icon: lucide:plus-circle
            severity: info
          submitted:
            on: transition
            transition: submit
            capture: [permitNumber, totalFeeAssessed]
            label: "Submitted for review"
            icon: lucide:send
            severity: info
          approved:
            on: transition
            transition: approve
            capture: [permitNumber, expiresAt]
            label: "Permit approved"
            icon: lucide:badge-check
            severity: success
          rejected:
            on: transition
            transition: reject
            capture: [decisionReason]
            label: "Permit rejected"
            detail: "{decisionReason}"
            icon: lucide:x-circle
            severity: error
          riskChanged:
            on: fieldChange
            field: riskLevel
            capture: [riskLevel]
            label: "Risk level changed to {riskLevel}"
            icon: lucide:flag
            severity: warning
          feeOverdue:
            on: fieldChange
            field: status
            condition: "status == 'approved' && balanceDue > 0"
            capture: [balanceDue]
            label: "Approved with outstanding balance {balanceDue}"
            icon: lucide:alert-octagon
            severity: error
          internalNote:
            on: fieldChange
            field: decisionReason
            capture: [decisionReason]
            label: "Internal decision note updated"
            roles: [reviewer, admin]
            icon: lucide:lock
            severity: info
        display:
          endpoint: true
          defaultLimit: 50
          maxLimit: 200
          order: desc
          includeActor: true
          groupBy: date
        retention:
          days: 730

      # §14 cross-field + cross-resource validation
      validation:
        decisionNeedsReason:
          rule: "status != 'rejected' || (decisionReason != null && len(decisionReason) >= 10)"
          message: "A rejection requires a decision reason of at least 10 characters."
        expiryAfterSubmit:
          rule: "expiresAt == null || submittedAt == null || expiresAt > submittedAt"
          message: "Expiry must be after the submission date."
        geoPaired:
          rule: "(siteLatitude == null) == (siteLongitude == null)"
          message: "Provide both latitude and longitude, or neither."
        applicantActive:
          rule: "query('Applicant', { id: applicantId })[0].deletedAt == null"
          message: "The applicant record must be active."
          when: create

      # §20 scopes
      scopes:
        open:
          filter: "status NOT IN ('approved','rejected','expired','withdrawn') AND deletedAt IS NULL"
          default: true
          description: "In-flight applications"
        issued:
          filter: "status = 'approved' AND deletedAt IS NULL"
          description: "Active issued permits"
        mine:
          filter: "createdById = :currentUserId AND deletedAt IS NULL"
          description: "Permits created by the current user"
        overdueBalance:
          filter: "status = 'approved' AND balanceDue > 0"
          description: "Issued permits with an outstanding balance"
        all:
          filter: ""
          description: "Every permit regardless of status or deletion"

      # §11 conditional permissions + field-level + transitions
      permissions:
        list: [authenticated]
        get: "isOwner() || hasAnyRole('clerk','reviewer','admin')"
        create: [authenticated]
        update: [owner, clerk, reviewer, admin]
        delete: [admin]
        conditional:
          update:
            draft: [owner, clerk, admin]
            under_review: [reviewer, admin]
            approved: [admin]
            rejected: []
            expired: []
            withdrawn: []
        fields:
          decisionReason:
            read: [reviewer, admin, owner]
            write: [reviewer, admin]
          totalFeeAssessed:
            write: [clerk, financeOfficer, admin]
          stateRegistryId:
            read: [admin, financeOfficer]
            write: [admin]
        transitions:
          submit: [owner, clerk, admin]
          beginReview: [clerk, reviewer, admin]
          requestRevision: [reviewer, admin]
          resubmit: [owner, clerk, admin]
          seniorSignoff: [reviewer, admin]
          scheduleInspection: [clerk, reviewer, admin]
          approve: [reviewer, admin]
          reject: [reviewer, admin]
          expire: [admin]
          withdraw: [owner, admin]

      indexes:
        - { fields: [permitNumber], unique: true }
        - { fields: [status] }
        - { fields: [applicantId] }
        - { fields: [permitTypeId] }
        - { fields: [departmentId, status], description: "Departmental status filter" }
        - { fields: [expiresAt], description: "Renewal/expiry scans" }
        - { fields: [deletedAt, status], description: "Active by status" }

    # ----------------------------------------------------------------------
    #  Inspection — child state machine; repeater checklist; webhook target
    # ----------------------------------------------------------------------
    Inspection:
      description: "A field inspection tied to a permit."
      features:
        audit: true
        search: true
        export: true
      object:
        id: { type: uuid, primaryKey: true, allowUserProvided: true }
        permitId: { type: uuid, required: true, immutable: true }
        inspectorId: { type: uuid }
        inspectionType: { type: string, enum: [routine, emergency, reinspection, final], default: routine }
        status: { $ref: '#/enums/InspectionStatus', default: scheduled }
        scheduledDate: { type: date }
        scheduledTime: { type: time }
        completedAt: { type: date }
        score: { type: integer, min: 0, max: 100 }
        rawResult: { type: string, maxLength: 120, description: "Raw result code from vendor webhook" }
        rawSensorData: { type: json, internal: true }
        gpsLat: { type: float }
        gpsLng: { type: float }
        signatureBlob: { type: bytes, maxSize: 100000 }
        checklist:
          type: repeater
          sortable: true
          object:
            item: { type: string, required: true, maxLength: 200 }
            passed: { type: boolean, default: false }
            notes: { type: text }
        $merge:
          - { $ref: '#/objects/Timestamp' }
      relationships:
        permit: { type: belongsTo, target: Permit, foreignKey: permitId, onDelete: cascade, required: true }
        inspector: { type: belongsTo, target: Inspector, foreignKey: inspectorId, onDelete: nullify }
      stateMachine:
        field: status
        initial: scheduled
        states:
          scheduled:
            transitions:
              start: { to: in_progress, guards: [inspector, admin] }
              cancel: { to: cancelled, guards: [clerk, admin] }
          in_progress:
            transitions:
              pass:
                to: passed
                guards: [inspector, admin]
                sets: { completedAt: "now()" }
              fail:
                to: failed
                guards: [inspector, admin]
                sets: { completedAt: "now()" }
          passed: { final: true }
          failed:
            transitions:
              reinspect: { to: scheduled, guards: [clerk, admin] }
          cancelled: { final: true }
      history:
        events:
          inspectionCreated:
            on: create
            capture: [inspectionType, scheduledDate]
            label: "Inspection scheduled ({inspectionType})"
            icon: lucide:calendar
            severity: info
          inspectionPassed:
            on: transition
            transition: pass
            label: "Inspection passed"
            icon: lucide:check
            severity: success
          inspectionFailed:
            on: transition
            transition: fail
            capture: [score]
            label: "Inspection failed (score {score})"
            icon: lucide:x
            severity: error
      indexes:
        - { fields: [permitId], description: "Aggregation trigger for Permit.inspectionCount" }
        - { fields: [inspectorId] }
        - { fields: [status] }
      permissions:
        list: [authenticated]
        get: [authenticated]
        create: [clerk, inspector, admin]
        update: [inspector, admin]
        delete: [admin]

    # ----------------------------------------------------------------------
    #  Payment — fee payment; state machine; aggregation source; integration
    # ----------------------------------------------------------------------
    Payment:
      description: "A fee payment against a permit."
      features:
        audit: true
        export: true
      object:
        id: { type: uuid, primaryKey: true, allowUserProvided: true }
        permitId: { type: uuid, required: true, immutable: true }
        amount: { type: money, required: true, min: 0, privacy: { classification: financialData } }
        currency: { type: currency, default: "USD" }
        method: { type: string, enum: [card, ach, check, cash, waiver], default: card }
        status: { $ref: '#/enums/PaymentStatus', default: pending }
        gatewayReference: { type: string, maxLength: 120, hidden: true, requestable: true }
        receivedAt: { type: datetime }
        $merge:
          - { $ref: '#/objects/Timestamp' }
          - { $ref: '#/objects/AuditMeta' }
      relationships:
        permit: { type: belongsTo, target: Permit, foreignKey: permitId, onDelete: cascade, required: true }
      stateMachine:
        field: status
        initial: pending
        states:
          pending:
            transitions:
              markPaid:
                to: paid
                guards: [financeOfficer, clerk, admin]
                sets: { receivedAt: "now()" }
              markFailed: { to: failed, guards: [financeOfficer, admin] }
          paid:
            transitions:
              refund: { to: refunded, guards: [financeOfficer, admin] }
          failed:
            transitions:
              retry: { to: pending, guards: [financeOfficer, clerk, admin] }
          refunded: { final: true }
      history:
        events:
          paymentReceived:
            on: transition
            transition: markPaid
            capture: [amount]
            label: "Payment of {amount} received"
            icon: lucide:dollar-sign
            severity: success
          paymentRefunded:
            on: transition
            transition: refund
            capture: [amount]
            label: "Payment of {amount} refunded"
            icon: lucide:rotate-ccw
            severity: warning
      indexes:
        - { fields: [permitId], description: "Aggregation trigger for Permit.feesCollected" }
        - { fields: [status] }
      permissions:
        list: [financeOfficer, clerk, admin]
        get: "isOwner() || hasAnyRole('financeOfficer','clerk','admin')"
        create: [financeOfficer, clerk, admin]
        update: [financeOfficer, admin]
        delete: [admin]

    # ----------------------------------------------------------------------
    #  PermitTag — belongsToMany target (junction PermitTagLink auto-table)
    # ----------------------------------------------------------------------
    PermitTag:
      description: "Free-form tags applied to permits (many-to-many)."
      features:
        search: true
      object:
        id: { type: uuid, primaryKey: true, allowUserProvided: true }
        name: { type: string, required: true, unique: true, maxLength: 60, lowercase: true }
        color: { type: color, default: "#64748b" }
        $merge:
          - { $ref: '#/objects/Timestamp' }
      relationships:
        permits: { type: belongsToMany, target: Permit, through: PermitTagLink, foreignKey: tagId, otherKey: permitId, timestamps: true }
      permissions:
        list: ["*"]
        get: ["*"]
        create: [clerk, admin]
        update: [admin]
        delete: [admin]

    # ----------------------------------------------------------------------
    #  PermitReviewer — §8.7 composite primary key junction with extra data
    # ----------------------------------------------------------------------
    PermitReviewer:
      description: "Assignment of a reviewer to a permit (composite-key resource)."
      object:
        permitId: { type: uuid, required: true }
        inspectorId: { type: uuid, required: true }
        assignedAt: { type: datetime, autoNow: create }
        senior: { type: boolean, default: false }
        role: { type: string, enum: [plan_examiner, fire, zoning, senior], default: plan_examiner }
      compositePrimaryKey: [permitId, inspectorId]
      relationships:
        permit: { type: belongsTo, target: Permit, foreignKey: permitId, onDelete: cascade }
        inspector: { type: belongsTo, target: Inspector, foreignKey: inspectorId, onDelete: restrict }
      indexes:
        - { fields: [permitId] }
        - { fields: [inspectorId] }
      permissions:
        list: [clerk, reviewer, admin]
        get: [clerk, reviewer, admin]
        create: [clerk, admin]
        update: [admin]
        delete: [admin]

    # ----------------------------------------------------------------------
    #  PublicNotice — content showcase: blocks/media/svg/icon/markdown; custom op
    # ----------------------------------------------------------------------
    PublicNotice:
      description: "Public-facing notices and bulletins (CMS-style content)."
      features:
        softDelete: true
        audit: true
        search: true
      api:
        customOperations:
          - name: publish
            method: POST
            path: /:id/publish
            permissions: [clerk, admin]
            fields:
              published: true
              publishedAt: "NOW()"
          - name: archive
            method: POST
            path: /:id/archive
            permissions: [admin]
            fields:
              published: false
      object:
        id: { type: uuid, primaryKey: true, allowUserProvided: true }
        headline: { type: string, required: true, maxLength: 200 }
        slug: { $ref: '#/properties/slugField', from: headline }
        severity: { $ref: '#/enums/NoticeSeverity', default: info }
        permitTypeId: { type: uuid, description: "Optional related permit type" }
        summary: { type: markdown }
        body:
          type: blocks
          maxBlocks: 100
          allowedBlocks:
            - paragraph
            - heading
            - list
            - image
            - $ref: '#/blocks/Callout'
            - $ref: '#/blocks/NoticeBanner'
        featuredImage: { type: media, accept: [image/jpeg, image/png, image/webp], maxSize: 5mb, aspectRatio: "16:9" }
        sealSvg: { type: svg, maxSize: 50kb }
        bannerIcon: { type: icon, iconSets: [lucide, seal] }
        attachments:
          type: repeater
          object:
            label: { type: string, required: true, maxLength: 120 }
            file: { type: media, accept: [application/pdf, image/*], maxSize: 10mb }
        published: { type: boolean, default: false }
        publishedAt: { type: datetime }
        $merge:
          - { $ref: '#/objects/Timestamp' }
          - { $ref: '#/objects/SoftDelete' }
      relationships:
        permitType: { type: belongsTo, target: PermitType, foreignKey: permitTypeId, onDelete: nullify }
      scopes:
        live:
          filter: "published = 1 AND deletedAt IS NULL"
          default: true
        drafts:
          filter: "published = 0 AND deletedAt IS NULL"
        all:
          filter: ""
      indexes:
        - { fields: [published, publishedAt] }
        - { fields: [severity] }
      permissions:
        list: ["*"]
        get: ["*"]
        create: [clerk, admin]
        update: [clerk, admin]
        delete: [admin]

    # ----------------------------------------------------------------------
    #  BudgetWorksheet — §28 sheet mode: dynamic columns + reactive computed
    # ----------------------------------------------------------------------
    BudgetWorksheet:
      description: "Departmental budget worksheet (spreadsheet/sheet-mode resource)."
      features:
        audit: true
      dynamicFields:
        enabled: true
        allowTypes: [string, money, integer, decimal, float, date, boolean, text]
        maxColumns: 50
        permissions:
          addColumn: [admin, financeOfficer]
          renameColumn: [admin, financeOfficer]
          deleteColumn: [admin]
      computedFields:
        reactive: true
        overridable: true
        crossRecord: true
      object:
        id: { type: uuid, primaryKey: true, allowUserProvided: true }
        departmentId: { type: uuid, required: true, immutable: true }
        fiscalYear: { type: integer, required: true, min: 2000, max: 2100 }
        name: { type: string, required: true, maxLength: 150 }
        $merge:
          - { $ref: '#/objects/Timestamp' }
      computed:
        grandTotal:
          type: money
          formula: "sum(lines.total)"
          stored: true
          updateOn: [lines.create, lines.update, lines.delete]
      relationships:
        department: { type: belongsTo, target: Department, foreignKey: departmentId, onDelete: cascade, required: true }
        lines: { type: hasMany, target: BudgetLine, foreignKey: worksheetId }
      indexes:
        - { fields: [departmentId, fiscalYear], unique: true }
      permissions:
        list: [financeOfficer, admin]
        get: [financeOfficer, admin]
        create: [financeOfficer, admin]
        update: [financeOfficer, admin]
        delete: [admin]

    # ----------------------------------------------------------------------
    #  BudgetLine — sheet-mode rows with reactive same-record formula chain
    # ----------------------------------------------------------------------
    BudgetLine:
      description: "A budget line item with quarterly columns and a reactive total."
      features:
        audit: true
      computedFields:
        reactive: true
        overridable: true
      object:
        id: { type: uuid, primaryKey: true, allowUserProvided: true }
        worksheetId: { type: uuid, required: true, immutable: true }
        category: { type: string, enum: [personnel, equipment, supplies, services, other], required: true }
        lineItem: { type: string, required: true, maxLength: 200 }
        q1: { type: money, default: 0 }
        q2: { type: money, default: 0 }
        q3: { type: money, default: 0 }
        q4: { type: money, default: 0 }
        $merge:
          - { $ref: '#/objects/Timestamp' }
      computed:
        total:
          type: money
          formula: "q1 + q2 + q3 + q4"
          stored: true
          reactive: true
          updateOn: [q1, q2, q3, q4]
        # reactive cross-record SUMIF: total spend for this category across the worksheet
        categoryTotal:
          type: money
          formula: "sum(BudgetLine, 'total', worksheetId == $this.worksheetId && category == $this.category)"
          stored: true
          reactive: true
          recomputeOn: [BudgetLine.create, BudgetLine.update, BudgetLine.delete]
      relationships:
        worksheet: { type: belongsTo, target: BudgetWorksheet, foreignKey: worksheetId, onDelete: cascade, required: true }
      history:
        events:
          cellEdited:
            on: fieldChange
            field: "*"
            label: "{fieldName} changed"
            severity: info
          columnAdded:
            on: schemaChange
            change: addColumn
            capture: [columnName, columnType]
            label: "Column '{columnName}' added"
            severity: info
      indexes:
        - { fields: [worksheetId] }
        - { fields: [worksheetId, category] }
      permissions:
        list: [financeOfficer, admin]
        get: [financeOfficer, admin]
        create: [financeOfficer, admin]
        update: [financeOfficer, admin]
        delete: [admin]