TownlightTownlight Beta 1

Historical Native commands reference for Windows 1.6.0.1. For Windows 1.8.0.1, use the matching source archive and technical reference index.

Townlight Beta 1

Native IPC Contracts

Staff guide and technical reference

Documentation edition 30 October 2026
Existing component versions retained
Signed suite installer 1.6.0.1
Private beta documentation companion

Native command boundary

These Rust sources are the fixed transport/IPC contracts from the candidate companion. They are not a new command service. Use the existing signed shell; native proof remains owned by that shell.

code_client.rs

use crate::domain_client::{send, DomainError};
use reqwest::{Method, Url};
use serde::Deserialize;
use serde_json::Value;

const BASE: &str = "http://127.0.0.1:15480";

#[derive(Deserialize)]
#[serde(tag = "operation", rename_all = "kebab-case", deny_unknown_fields)]
pub enum CodeRequest {
    Connections {},
    Connection {
        id: String,
    },
    SendProposal {
        payload: Value,
    },
    CloseConnection {
        id: String,
        payload: Value,
    },
    Workspace {},
    Sources {},
    Import {
        payload: Value,
    },
    Lookup {
        section_number: String,
        as_of: Option<String>,
    },
    Citation {
        section_number: String,
        as_of: Option<String>,
    },
    Search {
        query: String,
    },
    History {
        id: String,
    },
    Export {
        section_ref: String,
        as_of: Option<String>,
    },
    Question {
        question: String,
        section_number: Option<String>,
        as_of: Option<String>,
    },
    ImportTree {
        id: String,
    },
    ImportProvenance {
        id: String,
    },
}

/// Encode an identifier as a single URL path segment, rejecting values that
/// could escape the intended path.
pub(crate) fn encode_segment(id: &str) -> Result<String, DomainError> {
    if id.is_empty()
        || id == "."
        || id == ".."
        || id.contains('/')
        || id.contains('\\')
        || id.chars().any(|c| c.is_control())
    {
        return Err(DomainError::denied());
    }

    let mut out = String::with_capacity(id.len());
    for b in id.bytes() {
        match b {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
                out.push(b as char)
            }
            _ => out.push_str(&format!("%{:02X}", b)),
        }
    }
    Ok(out)
}

pub async fn execute(request: CodeRequest, proof: String) -> Result<Value, DomainError> {
    let base = Url::parse(BASE).map_err(|_| DomainError::denied())?;

    let (method, segment, body, params): (Method, String, Option<Value>, Vec<(&str, String)>) =
        match request {
            CodeRequest::Connections {} => {
                (Method::GET, "connections".to_string(), None, Vec::new())
            }
            CodeRequest::Connection { id } => (
                Method::GET,
                format!("connections/{}", encode_segment(&id)?),
                None,
                Vec::new(),
            ),
            CodeRequest::SendProposal { payload } => (
                Method::POST,
                "connections".to_string(),
                Some(serde_json::json!(payload)),
                Vec::new(),
            ),
            CodeRequest::CloseConnection { id, payload } => (
                Method::POST,
                format!("connections/{}/closeout", encode_segment(&id)?),
                Some(serde_json::json!(payload)),
                Vec::new(),
            ),
            CodeRequest::Workspace {} => (Method::GET, "workspace".to_string(), None, Vec::new()),
            CodeRequest::Sources {} => (Method::GET, "sources".to_string(), None, Vec::new()),
            CodeRequest::Import { payload } => (
                Method::POST,
                "imports".to_string(),
                Some(payload),
                Vec::new(),
            ),
            CodeRequest::Lookup {
                section_number,
                as_of,
            } => {
                let mut q = vec![("section_number", section_number)];
                if let Some(a) = as_of {
                    q.push(("as_of", a));
                }
                (Method::GET, "lookup".to_string(), None, q)
            }
            CodeRequest::Citation {
                section_number,
                as_of,
            } => {
                let mut q = vec![("section_number", section_number)];
                if let Some(a) = as_of {
                    q.push(("as_of", a));
                }
                (Method::GET, "citation".to_string(), None, q)
            }
            CodeRequest::Search { query } => {
                (Method::GET, "search".to_string(), None, vec![("q", query)])
            }
            CodeRequest::Export { section_ref, as_of } => {
                let mut params: Vec<(&str, String)> = vec![("section_ref", section_ref.clone())];
                if let Some(v) = as_of.clone() {
                    params.push(("as_of", v));
                }
                (reqwest::Method::GET, "export".to_string(), None, params)
            }
            CodeRequest::Question {
                question,
                section_number,
                as_of,
            } => {
                let body = serde_json::json!({
                    "question": question,
                    "section_number": section_number,
                    "as_of": as_of,
                });
                (
                    reqwest::Method::POST,
                    "questions".to_string(),
                    Some(body),
                    Vec::new(),
                )
            }
            CodeRequest::ImportTree { id } => (
                reqwest::Method::GET,
                format!("imports/{}/tree", encode_segment(&id)?),
                None,
                Vec::new(),
            ),
            CodeRequest::ImportProvenance { id } => (
                reqwest::Method::GET,
                format!("imports/{}/provenance", encode_segment(&id)?),
                None,
                Vec::new(),
            ),
            CodeRequest::History { id } => (
                Method::GET,
                format!("sections/{}/history", encode_segment(&id)?),
                None,
                Vec::new(),
            ),
        };

    let mut url = base
        .join(&format!("/desktop/v1/code/{}", segment))
        .map_err(|_| DomainError::denied())?;

    if !params.is_empty() {
        let mut pairs = url.query_pairs_mut();
        for (k, v) in &params {
            pairs.append_pair(k, v);
        }
    }

    let path_and_query = match url.query() {
        Some(q) => format!("{}?{}", url.path(), q),
        None => url.path().to_string(),
    };

    send(method, path_and_query, body, &proof).await
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[ignore = "requires paired disposable runtime at 127.0.0.1:15480"]
    fn native_code_roundtrip_with_real_runtime() {
        let proof = std::env::var("TOWNLIGHT_TEST_SESSION_PROOF")
            .expect("TOWNLIGHT_TEST_SESSION_PROOF must be set");
        let bundle_path = std::env::var("TOWNLIGHT_TEST_CODE_BUNDLE")
            .expect("TOWNLIGHT_TEST_CODE_BUNDLE must be set");
        let raw =
            std::fs::read_to_string(&bundle_path).expect("failed to read synthetic Code bundle");
        let payload: serde_json::Value =
            serde_json::from_str(&raw).expect("valid synthetic bundle JSON");
        tauri::async_runtime::block_on(async move {
            let workspace_err = execute(CodeRequest::Workspace {}, "0".repeat(64)).await;
            assert!(
                workspace_err.is_err(),
                "Workspace with bogus proof must fail"
            );
            let imported = execute(CodeRequest::Import { payload }, proof.clone())
                .await
                .expect("Import should succeed against real runtime");
            assert_eq!(
                imported["status"], "completed",
                "Import did not complete: {}",
                imported
            );
            let lookup = execute(
                CodeRequest::Lookup {
                    section_number: "6.12.040".to_string(),
                    as_of: Some("2025-06-01".to_string()),
                },
                proof.clone(),
            )
            .await
            .expect("Lookup should succeed");
            let body = lookup["version"]["body"]
                .as_str()
                .expect("Lookup version body");
            assert!(body.contains("four"), "Historical body: {}", body);
            let history = execute(
                CodeRequest::History {
                    id: "sec_6_12_040".to_string(),
                },
                proof.clone(),
            )
            .await
            .expect("History should succeed");
            assert_eq!(
                history["versions"]
                    .as_array()
                    .expect("versions array")
                    .len(),
                2
            );
            let citation = execute(
                CodeRequest::Citation {
                    section_number: "6.12.040".to_string(),
                    as_of: None,
                },
                proof,
            )
            .await
            .expect("Citation should succeed");
            assert_eq!(citation["status"], "ok");
            assert_eq!(citation["citation"]["source_id"], "csv_active");
        });
    }
}

domain_client.rs

//! Local transport only: no municipal rules, retries, JSON-state fallback,
//! caller-selected URL, or session proof exposed to the webview.
use reqwest::{Client, Method, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::time::Duration;
use uuid::Uuid;

const DOMAIN_ORIGIN: &str = "http://127.0.0.1:15480";
const MAX_RESPONSE_BYTES: usize = 16 * 1024 * 1024;

#[derive(Deserialize)]
#[serde(tag = "operation", rename_all = "kebab-case", deny_unknown_fields)]
pub enum MeetingsRequest {
    CodeConnections {},
    CodeConnection { id: String },
    RecordCodeDecision { id: String, payload: Value },
    RecordMeetingStage { id: Uuid, payload: MeetingStage },
    CapturePacket { id: Uuid, payload: PacketInput },
    DeliverPacket { id: Uuid },
    GetNoticeTemplate { id: Uuid },
    DeliverNotice { id: Uuid, payload: NoticeDocument },
    CaptureTranscript { id: Uuid, payload: TranscriptInput },
    CaptureActionItem { id: Uuid, payload: ActionItemInput },
    ArchiveMeeting { id: Uuid, payload: ArchiveInput },
    CheckNotice { id: Uuid, payload: NoticeInput },
    RecordNoticeProof { id: Uuid, payload: NoticeProof },
    RecordMinutesDecision { id: Uuid, payload: MinutesDecision },
    RecordAgendaReview { id: Uuid, payload: AgendaReview },
    AssignAgendaMeeting { id: Uuid, payload: AgendaAssignment },
    CreateMeeting { payload: MeetingIntake },
    GetMeeting { id: Uuid },
    CreateAgendaItem { payload: AgendaIntake },
    GetAgendaItem { id: Uuid },
    CaptureMotion { id: Uuid, payload: MotionInput },
    CorrectMotion { id: Uuid, payload: MotionCorrection },
    CaptureVote { id: Uuid, payload: VoteInput },
    CorrectVote { id: Uuid, payload: VoteCorrection },
    CreateMinutesDraft { id: Uuid, payload: MinutesInput },
    GetProceedings { id: Uuid },
}

#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct MinutesDecision {
    action: String,
    note: String,
}

#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PacketInput {
    agenda_item_ids: Vec<Uuid>,
    #[serde(default)]
    source_paths: Vec<String>,
}

#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct MeetingStage {
    to_status: String,
    statutory_basis: Option<String>,
}

#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NoticeInput {
    notice_type: String,
    posted_at: String,
    minimum_notice_hours: u32,
    statutory_basis: Option<String>,
    approved_by: Option<String>,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NoticeProof {
    location: String,
    note: String,
    document_delivery_id: Option<String>,
}

#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NoticeDocument {
    text: String,
}

#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AgendaReview {
    to_status: String,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AgendaAssignment {
    meeting_id: Uuid,
}

#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct MotionInput {
    text: String,
    seconded_by: Option<String>,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct MotionCorrection {
    text: String,
    reason: String,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct VoteInput {
    voter_name: String,
    vote: String,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct VoteCorrection {
    vote: String,
    reason: String,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct MinutesSource {
    source_id: String,
    label: String,
    text: String,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct MinutesSentence {
    text: String,
    citations: Vec<String>,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct MinutesInput {
    source_materials: Vec<MinutesSource>,
    sentences: Vec<MinutesSentence>,
}

#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct MeetingIntake {
    title: String,
    scheduled_start: String,
    location: String,
    #[serde(default = "regular_meeting_type")]
    meeting_type: String,
}
fn regular_meeting_type() -> String {
    "regular".into()
}

#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TranscriptInput {
    source_label: String,
    transcript_text: String,
    correction_of_id: Option<Uuid>,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ActionItemInput {
    description: String,
    assigned_to: Option<String>,
    source_motion_id: Option<Uuid>,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ArchiveInput {
    visibility: String,
    packet_id: Option<Uuid>,
    minutes_id: Option<Uuid>,
    plain_language_summary: String,
}

#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AgendaIntake {
    title: String,
    department_name: String,
}

pub async fn execute_meetings(
    request: MeetingsRequest,
    proof: String,
) -> Result<Value, DomainError> {
    let (method, path, body) = match request {
        MeetingsRequest::CodeConnections {} => (
            Method::GET,
            "/desktop/v1/meetings/code-connections".to_string(),
            None,
        ),
        MeetingsRequest::CodeConnection { id } => (
            Method::GET,
            format!(
                "/desktop/v1/meetings/code-connections/{}",
                crate::code_client::encode_segment(&id)?
            ),
            None,
        ),
        MeetingsRequest::RecordCodeDecision { id, payload } => (
            Method::POST,
            format!(
                "/desktop/v1/meetings/code-connections/{}/decisions",
                crate::code_client::encode_segment(&id)?
            ),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::GetNoticeTemplate { id } => (
            Method::GET,
            format!("/desktop/v1/meetings/notices/{id}/template"),
            None,
        ),
        MeetingsRequest::DeliverNotice { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/notices/{id}/document"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::CaptureTranscript { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/meetings/{id}/transcripts"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::CaptureActionItem { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/meetings/{id}/action-items"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::ArchiveMeeting { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/meetings/{id}/archive"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::CheckNotice { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/meetings/{id}/notice-checks"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::RecordNoticeProof { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/notices/{id}/proof"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::RecordMeetingStage { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/meetings/{id}/stages"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::CapturePacket { id, payload } => {
            let mut files = Vec::new();
            for path in payload.source_paths {
                files.push(crate::domain_files::stage_meetings(&path).map_err(|_| {
                    DomainError::new(
                        "file-unavailable",
                        "The selected source file could not be staged; no packet was submitted.",
                        false,
                    )
                })?);
            }
            (
                Method::POST,
                format!("/desktop/v1/meetings/meetings/{id}/packet-snapshots"),
                Some(
                    serde_json::json!({"agenda_item_ids":payload.agenda_item_ids,"source_files":files}),
                ),
            )
        }
        MeetingsRequest::DeliverPacket { id } => (
            Method::POST,
            format!("/desktop/v1/meetings/packets/{id}/deliver"),
            None,
        ),
        MeetingsRequest::RecordMinutesDecision { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/minutes/{id}/decisions"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::RecordAgendaReview { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/agenda-items/{id}/reviews"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::AssignAgendaMeeting { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/agenda-items/{id}/meeting"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::CaptureMotion { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/meetings/{id}/motions"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::CorrectMotion { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/motions/{id}/corrections"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::CaptureVote { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/motions/{id}/votes"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::CorrectVote { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/votes/{id}/corrections"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::CreateMinutesDraft { id, payload } => (
            Method::POST,
            format!("/desktop/v1/meetings/meetings/{id}/minutes/drafts"),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::GetProceedings { id } => (
            Method::GET,
            format!("/desktop/v1/meetings/meetings/{id}/proceedings"),
            None,
        ),
        MeetingsRequest::CreateMeeting { payload } => (
            Method::POST,
            "/desktop/v1/meetings/meetings".into(),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::GetMeeting { id } => (
            Method::GET,
            format!("/desktop/v1/meetings/meetings/{id}"),
            None,
        ),
        MeetingsRequest::CreateAgendaItem { payload } => (
            Method::POST,
            "/desktop/v1/meetings/agenda-items".into(),
            Some(serde_json::json!(payload)),
        ),
        MeetingsRequest::GetAgendaItem { id } => (
            Method::GET,
            format!("/desktop/v1/meetings/agenda-items/{id}"),
            None,
        ),
    };
    send(method, path, body, &proof).await
}

#[derive(Deserialize)]
#[serde(tag = "operation", rename_all = "kebab-case", deny_unknown_fields)]
pub enum SuiteRequest {
    State {},
    Search { query: String },
}

pub async fn execute_suite(request: SuiteRequest, proof: String) -> Result<Value, DomainError> {
    let (method, path, body) = match request {
        SuiteRequest::State {} => (Method::GET, "/desktop/v1/suite/state", None),
        SuiteRequest::Search { query } => (
            Method::POST,
            "/desktop/v1/suite/search",
            Some(serde_json::json!({"query":query})),
        ),
    };
    send(method, path.into(), body, &proof).await
}

#[derive(Deserialize)]
#[serde(tag = "operation", rename_all = "kebab-case", deny_unknown_fields)]
pub enum AccessRequest {
    State {},
    Review { payload: AccessReview },
    DeleteReview { review_id: Uuid },
    ExportReview { review_id: Uuid },
    Tool { payload: AccessTool },
    Rewrite { payload: AccessRewrite },
}

#[derive(Deserialize, Serialize)]
#[serde(
    tag = "action",
    rename_all = "kebab-case",
    rename_all_fields = "camelCase",
    deny_unknown_fields
)]
pub enum AccessTool {
    FormPlan {
        form_name: String,
        fields: Vec<String>,
    },
    PublishingWorkflow {
        title: String,
        has_review: bool,
        has_plain_language: bool,
        has_translation_review: bool,
    },
    AdaTitleIi {
        service_area: String,
        has_coordinator_review: bool,
    },
    TaggedPdf {
        heading_levels: Vec<u32>,
    },
}

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum RewriteAction {
    PlainLanguage,
    LanguageVariant,
}

#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AccessRewrite {
    action: RewriteAction,
    text: String,
    #[serde(default)]
    language: String,
}

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AccessReview {
    #[serde(default)]
    title: String,
    #[serde(default)]
    body: String,
    #[serde(default)]
    has_alt_text: bool,
    #[serde(default)]
    language: String,
}

pub async fn execute_access(request: AccessRequest, proof: String) -> Result<Value, DomainError> {
    match request {
        AccessRequest::ExportReview { review_id } => {
            let plan = send(
                Method::POST,
                format!("/desktop/v1/access/reviews/{review_id}/export"),
                None,
                &proof,
            )
            .await?;
            let plan: crate::domain_files::ArtifactPlan =
                serde_json::from_value(plan).map_err(|_| DomainError::transport(true))?;
            let artifact_id = plan.artifact_id;
            let path = tauri::async_runtime::spawn_blocking(move || {
                crate::domain_files::write_access_export(plan)
            })
            .await
            .map_err(|_| DomainError::transport(true))?
            .map_err(|_| {
                DomainError::new(
                    "export-incomplete",
                    "Access export was not finalized. Prepared evidence is retained.",
                    true,
                )
            })?;
            send(
                Method::POST,
                format!("/desktop/v1/access/exports/{artifact_id}/complete"),
                Some(serde_json::json!({"export_path":path})),
                &proof,
            )
            .await
        }
        AccessRequest::Tool { payload } => {
            send(
                Method::POST,
                "/desktop/v1/access/tools".into(),
                Some(serde_json::json!(payload)),
                &proof,
            )
            .await
        }
        AccessRequest::Rewrite { payload } => {
            let context = send(
                Method::POST,
                "/desktop/v1/access/rewrite-context".into(),
                Some(serde_json::json!(&payload)),
                &proof,
            )
            .await?;
            let prompt = context
                .get("prompt")
                .and_then(Value::as_str)
                .ok_or_else(|| DomainError::transport(false))?
                .to_owned();
            let generated = tauri::async_runtime::spawn_blocking(move || {
                if crate::model::local_generation_available()? {
                    crate::model::generate_local_text(&prompt).map(Some)
                } else { Ok(None) }
            }).await.map_err(|_| DomainError::transport(false))?
                .map_err(|_| DomainError::new("model-unavailable", "Local AI generation failed. No rewrite was recorded; no sample was substituted.", false))?;
            let mut body = serde_json::json!(payload);
            if let Some((model, text)) = generated {
                body["outcome"] = "generated".into();
                body["generated"] = text.into();
                body["model"] = model.into();
            } else {
                body["outcome"] = "unavailable".into();
            }
            send(
                Method::POST,
                "/desktop/v1/access/rewrite-result".into(),
                Some(body),
                &proof,
            )
            .await
        }
        AccessRequest::State {} => {
            send(Method::GET, "/desktop/v1/access/state".into(), None, &proof).await
        }
        AccessRequest::DeleteReview { review_id } => {
            send(
                Method::POST,
                format!("/desktop/v1/access/reviews/{review_id}/delete"),
                None,
                &proof,
            )
            .await
        }
        AccessRequest::Review { payload } => {
            let context = send(
                Method::POST,
                "/desktop/v1/access/review-context".into(),
                Some(serde_json::json!(&payload)),
                &proof,
            )
            .await?;
            let prompt = context
                .get("prompt")
                .and_then(Value::as_str)
                .ok_or_else(|| DomainError::transport(false))?
                .to_owned();
            // Runtime availability and generation only; findings/prompt and
            // persistence belong to Access. Failed AI must not lose a review.
            let generated = tauri::async_runtime::spawn_blocking(move || {
                if crate::model::local_generation_available().unwrap_or(false) {
                    Some(crate::model::generate_local_text(&prompt))
                } else {
                    None
                }
            })
            .await;
            let mut body = serde_json::json!(&payload);
            match generated {
                Ok(Some(Ok((model, text)))) => {
                    body["ai_outcome"] = "generated".into();
                    body["ai_analysis"] = text.into();
                    body["ai_analysis_model"] = model.into();
                }
                Ok(None) => body["ai_outcome"] = "unavailable".into(),
                _ => body["ai_outcome"] = "failed".into(),
            }
            send(
                Method::POST,
                "/desktop/v1/access/reviews".into(),
                Some(body),
                &proof,
            )
            .await
        }
    }
}

#[derive(Deserialize)]
#[serde(tag = "operation", rename_all = "kebab-case", deny_unknown_fields)]
pub enum PublicRecordsRequest {
    State {},
    Submit { payload: PublicIntake },
    Lookup { payload: PublicLookup },
    Message { payload: PublicMessage },
}

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PublicIntake {
    requester: String,
    requester_contact: String,
    summary: String,
    #[serde(default)]
    deadline: String,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PublicLookup {
    tracking_number: String,
    requester_contact: String,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PublicMessage {
    tracking_number: String,
    requester_contact: String,
    public_request_message: String,
}

pub async fn execute_public(
    request: PublicRecordsRequest,
    proof: String,
) -> Result<Value, DomainError> {
    let (method, path, body) = match request {
        PublicRecordsRequest::State {} => (Method::GET, "records-state", None),
        PublicRecordsRequest::Submit { payload } => {
            (Method::POST, "requests", Some(serde_json::json!(payload)))
        }
        PublicRecordsRequest::Lookup { payload } => {
            (Method::POST, "lookup", Some(serde_json::json!(payload)))
        }
        PublicRecordsRequest::Message { payload } => {
            (Method::POST, "messages", Some(serde_json::json!(payload)))
        }
    };
    send_with_header(
        method,
        format!("/desktop/v1/public/{path}"),
        body,
        &proof,
        "X-Townlight-Public",
    )
    .await
}

#[derive(Deserialize)]
#[serde(tag = "operation", rename_all = "kebab-case", deny_unknown_fields)]
pub enum RecordsRequest {
    SearchPlanning {
        query: String,
    },
    AttachPlanningSource {
        request_id: Uuid,
        source_id: String,
        expected_sha256: String,
    },
    SearchZoning {
        query: String,
    },
    AttachZoningSource {
        request_id: Uuid,
        source_id: String,
        expected_sha256: String,
    },
    SearchCode {
        query: String,
        as_of: Option<String>,
    },
    AttachCodeSource {
        request_id: Uuid,
        source_id: String,
        expected_sha256: String,
        as_of: Option<String>,
    },
    SearchMeetings {
        query: String,
    },
    AttachMeetingsSource {
        request_id: Uuid,
        source_id: String,
        expected_sha256: String,
    },
    State {},
    MarkNotification {
        notification_id: Uuid,
    },
    Suggest {
        request_id: Uuid,
    },
    Export {
        request_id: Uuid,
        kind: ArtifactKind,
    },
    AttachDocument {
        request_id: Uuid,
        title: String,
        source_path: String,
        #[serde(default)]
        citation: String,
    },
    AttachReleaseCopy {
        request_id: Uuid,
        attachment_id: Uuid,
        source_path: String,
        status: ReleaseCopyStatus,
        #[serde(default)]
        note: String,
        #[serde(default)]
        added_by: String,
    },
    Create {
        payload: Intake,
    },
    View {
        request_id: Uuid,
    },
    Apply {
        request_id: Uuid,
        action: RecordsAction,
        payload: Value,
    },
}

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Intake {
    requester: String,
    summary: String,
    deadline: String,
    #[serde(default)]
    deadline_basis: String,
}

#[derive(Deserialize, Serialize)]
pub enum ReleaseCopyStatus {
    #[serde(rename = "release-ready copy")]
    Ready,
    #[serde(rename = "redacted copy")]
    Redacted,
}

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ArtifactKind {
    Response,
    ReleasePackage,
}

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum RecordsAction {
    AssignRecordsRequest,
    SetRecordsDeadline,
    CalculateRecordsDeadline,
    RequestRecordsClarification,
    AddRecordsMessage,
    RecordRecordsSearch,
    RecordRecordsSearchSession,
    AddRecordsExemptionReview,
    AddRecordsExemptionDecision,
    EstimateRecordsFee,
    AddRecordsFeeLine,
    WaiveRecordsFee,
    DraftRecordsResponse,
    ApproveRecordsResponse,
    FulfillRecordsRequest,
    CloseRecordsRequest,
}

#[derive(Debug, Serialize)]
pub struct DomainError {
    pub code: &'static str,
    pub message: &'static str,
    pub outcome_unknown: bool,
}

impl DomainError {
    pub fn maintenance() -> Self {
        Self::new("profile-maintenance", "Profile maintenance is in progress. No Records operation was started; retry after maintenance finishes.", false)
    }
    fn new(code: &'static str, message: &'static str, outcome_unknown: bool) -> Self {
        Self {
            code,
            message,
            outcome_unknown,
        }
    }

    pub fn denied() -> Self {
        Self::new(
            "access-denied",
            "Sign in with an authorized Records account and enable Records in this profile.",
            false,
        )
    }

    fn transport(writing: bool) -> Self {
        Self::new("service-unavailable", "Records could not confirm the result. Restore service availability and refresh before repeating a change.", writing)
    }
}

impl RecordsRequest {
    fn wire(self) -> Result<(Method, String, Option<Value>), DomainError> {
        let stage = |source: &str, title: &str, note: &str| {
            crate::domain_files::stage(source, title, note).map_err(|_| {
                DomainError::new(
                    "file-unavailable",
                    "The Records file could not be staged; no domain change was submitted.",
                    false,
                )
            })
        };
        Ok(match self {
            Self::SearchPlanning { query } => (
                Method::POST,
                "/desktop/v1/planning-sources/search".into(),
                Some(serde_json::json!({"query":query})),
            ),
            Self::AttachPlanningSource {
                request_id,
                source_id,
                expected_sha256,
            } => (
                Method::POST,
                format!("/desktop/v1/requests/{request_id}/planning-sources"),
                Some(serde_json::json!({"source_id":source_id,"expected_sha256":expected_sha256})),
            ),
            Self::SearchZoning { query } => (
                Method::POST,
                "/desktop/v1/zoning-sources/search".into(),
                Some(serde_json::json!({ "query": query })),
            ),
            Self::AttachZoningSource {
                request_id,
                source_id,
                expected_sha256,
            } => (
                Method::POST,
                format!("/desktop/v1/requests/{request_id}/zoning-sources"),
                Some(
                    serde_json::json!({ "source_id": source_id, "expected_sha256": expected_sha256 }),
                ),
            ),
            Self::SearchCode { query, as_of } => (
                Method::POST,
                "/desktop/v1/code-sources/search".into(),
                Some(serde_json::json!({"query":query,"as_of":as_of})),
            ),
            Self::AttachCodeSource {
                request_id,
                source_id,
                expected_sha256,
                as_of,
            } => (
                Method::POST,
                format!("/desktop/v1/requests/{request_id}/code-sources"),
                Some(
                    serde_json::json!({"source_id":source_id,"expected_sha256":expected_sha256,"as_of":as_of}),
                ),
            ),
            Self::SearchMeetings { query } => (
                Method::POST,
                "/desktop/v1/meetings-sources/search".into(),
                Some(serde_json::json!({"query":query})),
            ),
            Self::AttachMeetingsSource {
                request_id,
                source_id,
                expected_sha256,
            } => (
                Method::POST,
                format!("/desktop/v1/requests/{request_id}/meetings-sources"),
                Some(serde_json::json!({"source_id":source_id,"expected_sha256":expected_sha256})),
            ),
            Self::Export { .. } | Self::Suggest { .. } => {
                return Err(DomainError::transport(false))
            }
            Self::State {} => (Method::GET, "/desktop/v1/records-state".into(), None),
            Self::MarkNotification { notification_id } => (
                Method::POST,
                format!("/desktop/v1/notifications/{notification_id}/sent"),
                None,
            ),
            Self::AttachDocument {
                request_id,
                title,
                source_path,
                citation,
            } => {
                let file = stage(&source_path, &title, &citation)?;
                (
                    Method::POST,
                    format!("/desktop/v1/requests/{request_id}/documents"),
                    Some(serde_json::json!({"title":title, "citation":citation, "file":file})),
                )
            }
            Self::AttachReleaseCopy {
                request_id,
                attachment_id,
                source_path,
                status,
                note,
                added_by,
            } => {
                let file = stage(&source_path, "Records release copy", &note)?;
                (
                    Method::POST,
                    format!(
                        "/desktop/v1/requests/{request_id}/documents/{attachment_id}/release-copy"
                    ),
                    Some(
                        serde_json::json!({"status":status, "note":note, "added_by":added_by, "file":file}),
                    ),
                )
            }
            Self::View { request_id } => (
                Method::GET,
                format!("/desktop/v1/requests/{request_id}/view"),
                None,
            ),
            Self::Create { payload } => (
                Method::POST,
                "/desktop/v1/requests".into(),
                Some(serde_json::json!(payload)),
            ),
            Self::Apply {
                request_id,
                action,
                payload,
            } => (
                Method::POST,
                format!("/desktop/v1/requests/{request_id}/actions"),
                Some(serde_json::json!({"action": action, "payload": payload})),
            ),
        })
    }
}

fn status_error(status: StatusCode, writing: bool) -> DomainError {
    match status.as_u16() {
        401 | 403 => DomainError::denied(),
        400 | 422 => DomainError::new("invalid-input", "Records rejected the input. Review the required fields; no change was accepted.", false),
        404 => DomainError::new("not-found", "The Records request or operation was not found. Refresh before continuing.", false),
        409 => DomainError::new("conflict", "Records reported a conflict. Refresh before continuing.", false),
        503 => DomainError::new("not-ready", "Records is not ready or migration is not activated. Refresh after service readiness is restored.", writing),
        _ => DomainError::transport(writing),
    }
}

pub async fn execute(request: RecordsRequest, proof: String) -> Result<Value, DomainError> {
    let request = match request {
        RecordsRequest::Suggest { request_id } => {
            #[derive(Deserialize)]
            #[serde(deny_unknown_fields)]
            struct DraftContext {
                prompt: String,
                previous_response_sha256: String,
            }
            let context = send(
                Method::GET,
                format!("/desktop/v1/requests/{request_id}/draft-context"),
                None,
                &proof,
            )
            .await?;
            let context: DraftContext =
                serde_json::from_value(context).map_err(|_| DomainError::transport(false))?;
            let generated = tauri::async_runtime::spawn_blocking(move || crate::model::generate_local_text(&context.prompt))
                .await.map_err(|_| DomainError::transport(false))?
                .map_err(|_| DomainError::new("model-unavailable", "Local AI could not generate a draft. The existing text and approval were not changed.", false))?;
            return send(Method::POST, format!("/desktop/v1/requests/{request_id}/generated-draft"),
                        Some(serde_json::json!({"text":generated.1, "model":generated.0, "previous_response_sha256":context.previous_response_sha256})), &proof).await;
        }
        RecordsRequest::Export { request_id, kind } => {
            let plan = send(
                Method::POST,
                format!("/desktop/v1/requests/{request_id}/artifacts"),
                Some(serde_json::json!({"kind":kind})),
                &proof,
            )
            .await?;
            let plan: crate::domain_files::ArtifactPlan =
                serde_json::from_value(plan).map_err(|_| DomainError::transport(true))?;
            let artifact_id = plan.artifact_id;
            let path = tauri::async_runtime::spawn_blocking(move || crate::domain_files::write_export(plan))
                .await.map_err(|_| DomainError::transport(true))?
                .map_err(|_| DomainError::new("export-incomplete", "The Records export was not finalized. Prepared evidence is retained; refresh before retrying.", true))?;
            return send(
                Method::POST,
                format!("/desktop/v1/requests/{request_id}/artifacts/{artifact_id}/complete"),
                Some(serde_json::json!({"export_path":path})),
                &proof,
            )
            .await;
        }
        other => other,
    };
    let (method, path, body) = tauri::async_runtime::spawn_blocking(move || request.wire())
        .await
        .map_err(|_| DomainError::transport(false))??;
    send(method, path, body, &proof).await
}

pub(crate) async fn send(
    method: Method,
    path: String,
    body: Option<Value>,
    proof: &str,
) -> Result<Value, DomainError> {
    send_with_header(method, path, body, proof, "X-Townlight-Session").await
}

async fn send_with_header(
    method: Method,
    path: String,
    body: Option<Value>,
    proof: &str,
    proof_header: &'static str,
) -> Result<Value, DomainError> {
    let writing = method == Method::POST;
    // Do not forward the proof through a system proxy or a server redirect.
    let client = Client::builder()
        .no_proxy()
        .redirect(reqwest::redirect::Policy::none())
        .retry(reqwest::retry::never())
        .connect_timeout(Duration::from_secs(3))
        .timeout(Duration::from_secs(60))
        .build()
        .map_err(|_| DomainError::transport(false))?;
    let mut outgoing = client
        .request(method, format!("{DOMAIN_ORIGIN}{path}"))
        .header(proof_header, proof);
    if let Some(body) = body {
        outgoing = outgoing.json(&body);
    }
    let mut response = outgoing
        .send()
        .await
        .map_err(|_| DomainError::transport(writing))?;
    if !response.status().is_success() {
        return Err(status_error(response.status(), writing));
    }
    // Error bodies are deliberately not echoed: framework errors can contain
    // internal details. Successful responses must be bounded JSON.
    let mut bytes = Vec::new();
    while let Some(chunk) = response
        .chunk()
        .await
        .map_err(|_| DomainError::transport(writing))?
    {
        if bytes.len().saturating_add(chunk.len()) > MAX_RESPONSE_BYTES {
            return Err(DomainError::new("response-too-large", "Records returned more data than this client can safely load. No partial state was applied.", writing));
        }
        bytes.extend_from_slice(&chunk);
    }
    serde_json::from_slice(&bytes).map_err(|_| {
        DomainError::new(
            "invalid-response",
            "Records returned an unreadable result. Refresh before repeating a change.",
            writing,
        )
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[ignore = "requires the paired disposable Python runtime on fixed loopback port 15480"]
    fn native_records_roundtrip_with_real_runtime() {
        let proof = std::env::var("TOWNLIGHT_TEST_SESSION_PROOF")
            .expect("synthetic proof supplied by paired test");
        let source = std::env::var("TOWNLIGHT_TEST_SOURCE_FILE")
            .expect("synthetic source supplied by paired test");
        tauri::async_runtime::block_on(async {
            let denied = execute(RecordsRequest::State {}, "0".repeat(64)).await;
            assert!(denied.is_err(), "invalid proof must fail");
            let create: RecordsRequest = serde_json::from_value(serde_json::json!({
                "operation":"create", "payload":{"requester":"Native transport fixture", "summary":"Synthetic transport acceptance", "deadline":"2026-10-15"}
            })).unwrap();
            let created = execute(create, proof.clone())
                .await
                .expect("real native create");
            let id = created["id"].as_str().expect("canonical id");
            let attach: RecordsRequest = serde_json::from_value(serde_json::json!({
                "operation":"attach-document", "request_id":id, "title":"Synthetic document", "source_path":source, "citation":"Synthetic transport citation"
            })).unwrap();
            let attached = execute(attach, proof.clone())
                .await
                .expect("native copy and SQL attachment");
            let stored = attached["request"]["documents"][0]["stored_path"]
                .as_str()
                .unwrap();
            assert_eq!(
                std::fs::read_to_string(stored).unwrap(),
                "Synthetic source bytes"
            );
            for (action, payload) in [
                (
                    "draft-records-response",
                    serde_json::json!({"responseDraft":"Synthetic native response", "citation":"Synthetic transport citation"}),
                ),
                (
                    "approve-records-response",
                    serde_json::json!({"approvalNote":"Synthetic human review"}),
                ),
            ] {
                let command = serde_json::from_value(serde_json::json!({"operation":"apply", "request_id":id, "action":action, "payload":payload})).unwrap();
                execute(command, proof.clone())
                    .await
                    .expect("native lifecycle action");
            }
            let export = serde_json::from_value(
                serde_json::json!({"operation":"export", "request_id":id, "kind":"response"}),
            )
            .unwrap();
            let exported = execute(export, proof.clone())
                .await
                .expect("prepare/native write/finalize");
            let path = exported["request"]["exports"][0].as_str().unwrap();
            assert!(std::fs::read_to_string(path)
                .unwrap()
                .contains("Synthetic native response"));
            assert!(std::path::Path::new(&format!("{path}.manifest.json")).is_file());
            let view = execute(
                RecordsRequest::View {
                    request_id: Uuid::parse_str(id).unwrap(),
                },
                proof,
            )
            .await
            .expect("canonical native readback");
            assert_eq!(view["exports"][0], path);
            assert_eq!(view["documents"][0]["stored_path"], stored);
        });
    }

    #[test]
    fn routes_are_fixed_and_ids_are_not_paths() {
        let request: RecordsRequest = serde_json::from_value(serde_json::json!({
            "operation": "view", "request_id": "a7abf8da-7c0b-47a5-b4ab-4fc1b33aab78"
        }))
        .unwrap();
        let (method, path, body) = request.wire().unwrap();
        assert_eq!(method, Method::GET);
        assert_eq!(
            path,
            "/desktop/v1/requests/a7abf8da-7c0b-47a5-b4ab-4fc1b33aab78/view"
        );
        assert!(body.is_none());
        for bad in [
            serde_json::json!({"operation":"view", "request_id":"../admin"}),
            serde_json::json!({"operation":"state", "url":"https://example.invalid"}),
            serde_json::json!({"operation":"create", "payload":{"requester":"x", "summary":"x", "deadline":"2026-10-15", "actor":"admin"}}),
            serde_json::json!({"operation":"apply", "request_id":"a7abf8da-7c0b-47a5-b4ab-4fc1b33aab78", "action":"create-meeting", "payload":{}}),
        ] {
            assert!(serde_json::from_value::<RecordsRequest>(bad).is_err());
        }
    }

    #[test]
    fn failures_do_not_encourage_duplicate_mutations() {
        assert!(DomainError::transport(true).outcome_unknown);
        assert!(!DomainError::transport(false).outcome_unknown);
        assert!(status_error(StatusCode::INTERNAL_SERVER_ERROR, true).outcome_unknown);
        assert!(!status_error(StatusCode::UNAUTHORIZED, true).outcome_unknown);
        assert!(!status_error(StatusCode::UNPROCESSABLE_ENTITY, true).outcome_unknown);
    }
}

inspections_client.rs

//! Fixed native transport. No inspection rules or caller-supplied URLs/paths.
use crate::domain_client::{send, DomainError};
use reqwest::Method;
use serde::Deserialize;
use serde_json::Value;
use uuid::Uuid;

#[derive(Debug, Deserialize)]
#[serde(tag = "operation", rename_all = "kebab-case", deny_unknown_fields)]
pub enum InspectionsRequest {
    RepeatCases { payload: Value },
    DraftNotice { payload: Value },
    Cases { offset: u32 },
    Case { id: Uuid },
    CreateCase { payload: Value },
    ReviseCase { id: Uuid, payload: Value },
    History { id: Uuid },
    CreatePacket { id: Uuid, payload: Value },
    ExportPacket { id: Uuid, expected_sha256: String },
    SearchConnections { payload: Value },
    SupportedTypes {},
    RetainEvidence { payload: Value },
    AnalyseMedia { payload: Value },
    AttachRecords { request_id: Uuid, payload: Value },
}

pub fn route(request: InspectionsRequest) -> (Method, String, Option<Value>) {
    use InspectionsRequest::*;
    let (method, path, body) = match request {
        RepeatCases { payload } => (Method::POST, "/repeat-cases".into(), Some(payload)),
        DraftNotice { payload } => (Method::POST, "/notice-draft".into(), Some(payload)),
        Cases { offset } => (Method::GET, format!("/cases?offset={offset}"), None),
        Case { id } => (Method::GET, format!("/cases/{id}"), None),
        CreateCase { payload } => (Method::POST, "/cases".into(), Some(payload)),
        ReviseCase { id, payload } => (
            Method::POST,
            format!("/cases/{id}/revisions"),
            Some(payload),
        ),
        History { id } => (Method::GET, format!("/cases/{id}/history"), None),
        CreatePacket { id, payload } => {
            (Method::POST, format!("/cases/{id}/packets"), Some(payload))
        }
        ExportPacket { id, .. } => (Method::GET, format!("/packets/{id}"), None),
        SearchConnections { payload } => {
            (Method::POST, "/connections/search".into(), Some(payload))
        }
        SupportedTypes {} => (Method::GET, "/evidence/supported-types".into(), None),
        RetainEvidence { payload } => (Method::POST, "/evidence/retain".into(), Some(payload)),
        AnalyseMedia { payload } => (Method::POST, "/media/analyse".into(), Some(payload)),
        AttachRecords {
            request_id,
            payload,
        } => {
            return (
                Method::POST,
                format!("/desktop/v1/requests/{request_id}/inspections-sources"),
                Some(payload),
            )
        }
    };
    (method, format!("/desktop/v1/inspections{path}"), body)
}

pub async fn execute(request: InspectionsRequest, proof: String) -> Result<Value, DomainError> {
    let export = match &request {
        InspectionsRequest::ExportPacket {
            id,
            expected_sha256,
        } => Some((*id, expected_sha256.clone())),
        _ => None,
    };
    let (method, path, body) = route(request);
    let result = send(method, path, body, &proof).await?;
    if let Some((id, expected)) = export {
        let content = result
            .get("text")
            .and_then(Value::as_str)
            .ok_or_else(DomainError::denied)?;
        let digest = result
            .get("sha256")
            .and_then(Value::as_str)
            .ok_or_else(DomainError::denied)?;
        if digest != expected {
            return Err(DomainError::denied());
        }
        let path = crate::domain_files::write_inspections_file(
            &format!("inspections-{id}-{}.json", Uuid::new_v4()),
            content.as_bytes(),
            digest,
        )
        .map_err(|_| DomainError::denied())?;
        return Ok(serde_json::json!({"path": path, "sha256": digest}));
    }
    Ok(result)
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn inspections_transport_is_fixed_and_does_not_accept_a_url_or_unknown_operation() {
        assert!(serde_json::from_value::<InspectionsRequest>(
            serde_json::json!({"operation":"approve-permit"})
        )
        .is_err());
        assert!(serde_json::from_value::<InspectionsRequest>(
            serde_json::json!({"operation":"cases","offset":0,"url":"https://example.com"})
        )
        .is_err());
        let (_, path, _) = route(InspectionsRequest::Cases { offset: 10 });
        assert_eq!(path, "/desktop/v1/inspections/cases?offset=10");
    }
}

main.rs

mod atomic_io;
mod auth;
mod code_client;
mod demo_fixture;
mod domain_client;
mod domain_files;
mod first_run;
mod inspections_client;
mod local_paths;
mod local_shell;
mod model;
mod module_registry;
mod permitting_client;
mod planning_client;
mod profile_gate;
mod records_authority;
mod supervisor;
mod workflows;
mod zoning_client;

use auth::{AccessState, AuthActionResult, LocalUserSummary};
use first_run::{FirstRunActionResult, FirstRunState, SavedCityProfile};
use model::{ModelActionResult, ModelState};
use module_registry::{ModuleProfileSummary, ModuleSelectionState, ModuleSummary};
use serde::Serialize;
use serde_json::Value;
use std::panic::{catch_unwind, AssertUnwindSafe};
use supervisor::{RuntimeHealthItem, SupervisorActionResult};
use workflows::{CityWorkActionResult, CityWorkState};

const MODULES_JSON: &str = include_str!("../../../installer/modules.json");

#[derive(Serialize)]
struct NavigationItem {
    id: &'static str,
    label: &'static str,
    description: &'static str,
}

#[derive(Serialize)]
struct AppState {
    domain_authority: &'static str,
    product_name: &'static str,
    status_label: &'static str,
    local_only: bool,
    navigation: Vec<NavigationItem>,
    modules: Vec<ModuleSummary>,
    module_profiles: Vec<ModuleProfileSummary>,
    module_selection: ModuleSelectionState,
    installer_steps: Vec<&'static str>,
    first_run: FirstRunState,
    city_profile: Option<SavedCityProfile>,
    users: Vec<LocalUserSummary>,
    access: AccessState,
    model: ModelState,
    health: Vec<RuntimeHealthItem>,
    city_work: CityWorkState,
}

#[derive(Serialize)]
struct ModuleActionResult {
    accepted: bool,
    action: String,
    status: &'static str,
    message: String,
    next_action: &'static str,
    selection: ModuleSelectionState,
    modules: Vec<ModuleSummary>,
}

fn navigation() -> Vec<NavigationItem> {
    vec![
        NavigationItem {
            id: "home",
            label: "Home",
            description: "Work that needs attention",
        },
        NavigationItem {
            id: "meetings",
            label: "Meetings & Notices",
            description: "Agendas, notices, minutes, votes",
        },
        NavigationItem {
            id: "records",
            label: "Records Requests",
            description: "Intake, review, response, exports",
        },
        NavigationItem {
            id: "code",
            label: "Code & Ordinances",
            description: "Search, imports, guidance, handoffs",
        },
        NavigationItem {
            id: "notice",
            label: "Public Notices",
            description: "Deadlines, proof, archive packets",
        },
        NavigationItem {
            id: "zoning".into(),
            label: "Zoning".into(),
            description: "Parcel information and cited rules".into(),
        },
        NavigationItem {
            id: "access",
            label: "Accessibility",
            description: "WCAG review, plain language, multilingual, exports",
        },
        NavigationItem {
            id: "permitting",
            label: "Permitting",
            description: "Pre-application intake and reviewed workpapers",
        },
        NavigationItem {
            id: "inspections",
            label: "Inspections",
            description: "Reviewed inspection workpapers and Records copies",
        },
        NavigationItem {
            id: "planning",
            label: "Planning",
            description: "Adopted policy sources and reviewed staff analysis",
        },
        NavigationItem {
            id: "search",
            label: "Search City Knowledge",
            description: "Cross-module local search with citations",
        },
        NavigationItem {
            id: "health",
            label: "System Health",
            description: "Local services, model, backup, repair",
        },
        NavigationItem {
            id: "settings",
            label: "Settings",
            description: "City profile, users, modules",
        },
    ]
}

fn installer_steps() -> Vec<&'static str> {
    vec![
        "Choose install and local data locations.",
        "Install Townlight Core and the selected Townlight product profile.",
        "Create city profile and first admin user.",
        "Download and verify Gemma 4 12B quantization-aware weights.",
        "Verify local health, backup, repair, and uninstall entry points.",
    ]
}

fn module_summaries() -> Result<Vec<ModuleSummary>, String> {
    let _registry_source = MODULES_JSON;
    module_registry::module_summaries()
}

fn access_is_local_admin(access: &AccessState) -> bool {
    access.signed_in && access.role.as_deref() == Some("local-admin")
}

fn access_is_signed_in(access: &AccessState) -> bool {
    access.signed_in
}

fn require_role_for_city_work(
    access: &AccessState,
    action: &str,
    payload: Option<&Value>,
) -> Result<(), String> {
    let Some(role) = access.role.as_deref() else {
        return Err(
            "Sign in with a staff or Townlight admin account before changing city work."
                .to_string(),
        );
    };
    let Some((module_ids, allow_any)) = city_work_action_module_requirement(action, payload) else {
        return Ok(());
    };
    if auth::role_allows_modules(role, &module_ids, allow_any) {
        return Ok(());
    }
    Err(format!(
        "Your local role ({role}) is not allowed to use this module workflow. Ask a Townlight admin to adjust your account."
    ))
}

fn public_model_state(mut model: ModelState) -> ModelState {
    model.artifact.local_path =
        "Sign in as Townlight admin to view the model file path.".to_string();
    model
}

fn public_runtime_health(mut health: Vec<RuntimeHealthItem>) -> Vec<RuntimeHealthItem> {
    for item in &mut health {
        item.admin_detail.clear();
    }
    health
}

fn city_work_action_module_requirement(
    action: &str,
    payload: Option<&Value>,
) -> Option<(Vec<&'static str>, bool)> {
    match action {
        "create-meeting-body"
        | "add-meeting-member"
        | "create-meeting"
        | "add-agenda-item"
        | "submit-agenda-intake"
        | "review-agenda-intake"
        | "promote-agenda-intake"
        | "record-staff-report"
        | "add-meeting-attachment"
        | "finalize-meeting-packet"
        | "add-code-handoff-agenda"
        | "calculate-notice-deadline"
        | "complete-notice-checklist"
        | "post-notice"
        | "record-minutes"
        | "record-motion"
        | "add-minute-citation"
        | "suggest-minutes-draft"
        | "record-vote"
        | "record-member-vote"
        | "record-meeting-attendance"
        | "record-quorum-check"
        | "add-action-item"
        | "record-resident-comment"
        | "submit-public-comment"
        | "review-public-comment"
        | "redact-public-comment"
        | "adopt-minutes"
        | "sign-minutes"
        | "record-closed-session"
        | "export-meeting-packet"
        | "archive-meeting" => Some((vec!["townlight_meetings"], false)),
        "townlight_notice-calculate-deadline"
        | "townlight_notice-complete-checklist"
        | "townlight_notice-post-notice"
        | "townlight_notice-export-archive-packet" => Some((vec!["townlight_meetings", "townlight_notice"], false)),
        "record-adopted-legislation" => Some((vec!["townlight_meetings", "townlight_code"], false)),
        "create-records-request"
        | "load-demo-town"
        | "submit-public-records-request"
        | "lookup-public-records-request"
        | "add-public-records-message"
        | "set-records-deadline"
        | "calculate-records-deadline"
        | "request-records-clarification"
        | "add-records-message"
        | "assign-records-request"
        | "record-records-search"
        | "record-records-search-session"
        | "add-records-document"
        | "add-records-release-copy"
        | "add-records-exemption-review"
        | "add-records-exemption-decision"
        | "estimate-records-fee"
        | "add-records-fee-line"
        | "waive-records-fee"
        | "suggest-records-response"
        | "draft-records-response"
        | "approve-records-response"
        | "build-records-release-package"
        | "export-records-response"
        | "fulfill-records-request"
        | "close-records-request"
        | "mark-notification-sent" => Some((vec!["townlight_records"], false)),
        "import-code-source"
        | "record-codifier-sync"
        | "record-codifier-sync-failure"
        | "retry-codifier-sync"
        | "mark-code-stale"
        | "suggest-code-guidance"
        | "draft-code-guidance"
        | "approve-code-guidance"
        | "publish-code-source"
        | "unpublish-code-source"
        | "create-code-handoff"
        | "answer-code-question" => Some((vec!["townlight_code"], false)),
        "search-city-knowledge" => Some((
            vec![
                "townlight_meetings",
                "townlight_records",
                "townlight_code",
                "townlight_notice",
                "townlight_access",
            ],
            true,
        )),
        "open-exports-folder" => match payload
            .and_then(|value| value.get("folder"))
            .and_then(Value::as_str)
        {
            Some("meetings") => Some((vec!["townlight_meetings"], false)),
            Some("records") => Some((vec!["townlight_records"], false)),
            Some("code") => Some((vec!["townlight_code"], false)),
            Some("notice") => Some((vec!["townlight_notice"], false)),
            Some("access") => Some((vec!["townlight_access"], false)),
            Some("planning") => Some((vec!["townlight_planning"], false)),
            Some("permitting") => Some((vec!["townlight_permitting"], false)),
            Some("inspections") => Some((vec!["townlight_inspections"], false)),
            _ => Some((
                vec!["townlight_meetings", "townlight_records", "townlight_code", "townlight_notice"],
                true,
            )),
        },
        "accessibility-review"
        | "townlight_access-records-export"
        | "townlight_access-delete-review"
        | "townlight_access-plain-language"
        | "townlight_access-language-variant"
        | "townlight_access-form-plan"
        | "townlight_access-publishing-workflow"
        | "townlight_access-ada-title-ii"
        | "townlight_access-tagged-pdf" => Some((vec!["townlight_access"], false)),
        _ => None,
    }
}

fn module_display_name(module_id: &str) -> String {
    module_summaries()
        .ok()
        .and_then(|modules| {
            modules
                .into_iter()
                .find(|module| module.id == module_id)
                .map(|module| module.display_name)
        })
        .unwrap_or_else(|| module_id.to_string())
}

fn module_exports_folder(module_id: &str) -> Option<(&'static str, &'static str)> {
    match module_id {
        "townlight_meetings" => Some(("meetings", "meeting exports")),
        "townlight_records" => Some(("records", "records exports")),
        "townlight_code" => Some(("code", "code exports")),
        "townlight_notice" => Some(("notice", "notice exports")),
        "townlight_access" => Some(("access", "accessibility/records exports")),
        _ => None,
    }
}

fn require_enabled_city_modules(action: &str, payload: Option<&Value>) -> Result<(), String> {
    let Some((module_ids, allow_any)) = city_work_action_module_requirement(action, payload) else {
        return Ok(());
    };
    let selection = module_registry::module_selection_state()?;
    let enabled = module_ids
        .iter()
        .filter(|module_id| {
            selection
                .enabled_module_ids
                .iter()
                .any(|enabled_id| enabled_id == **module_id)
        })
        .copied()
        .collect::<Vec<_>>();
    if (allow_any && !enabled.is_empty()) || (!allow_any && enabled.len() == module_ids.len()) {
        return Ok(());
    }
    let missing = module_ids
        .iter()
        .filter(|module_id| !enabled.iter().any(|enabled_id| enabled_id == *module_id))
        .map(|module_id| module_display_name(module_id))
        .collect::<Vec<_>>();
    Err(format!(
        "{} {} not enabled in this local profile. Install or enable {} in Settings before using this workflow.",
        missing.join(", "),
        if missing.len() == 1 { "is" } else { "are" },
        if missing.len() == 1 { "it" } else { "them" }
    ))
}

fn filter_search_results_for_enabled_modules(
    result: &mut CityWorkActionResult,
) -> Result<(), String> {
    let selection = module_registry::module_selection_state()?;
    result.search_results.retain(|search_result| {
        selection
            .enabled_module_ids
            .iter()
            .any(|module_id| module_id == &search_result.module_id)
    });
    Ok(())
}

fn filter_search_results_for_access(result: &mut CityWorkActionResult, access: &AccessState) {
    let Some(role) = access.role.as_deref() else {
        return;
    };
    result
        .search_results
        .retain(|search_result| auth::role_allows_module(role, &search_result.module_id));
}

fn first_run_action_requires_admin_after_setup(action: &str) -> bool {
    matches!(
        action,
        "choose-location"
            | "select-modules"
            | "download-model"
            | "defer-model"
            | "create-city-profile"
            | "create-admin"
            | "choose-backup"
            | "verify-health"
            | "open-app"
            | "repair"
            | "backup"
            | "uninstall"
    )
}

fn app_state() -> Result<AppState, String> {
    let access = auth::access_state()?;
    let admin_signed_in = access_is_local_admin(&access);
    let signed_in = access_is_signed_in(&access);
    let authority = records_authority::read()?;
    let domain_authority = match authority.as_ref().map(|journal| journal.phase.as_str()) {
        Some("active") => "python-records",
        Some(_) => "records-migration-pending",
        None => "legacy",
    };
    let city_work = if authority.is_some() {
        // The typed async client loads canonical state; never read old JSON here.
        CityWorkState::default()
    } else if signed_in {
        workflows::city_work_state()?
    } else {
        workflows::public_city_work_state()?
    };
    let users = if admin_signed_in {
        auth::saved_users()?
    } else {
        Vec::new()
    };
    let model = if admin_signed_in {
        model::model_state()?
    } else {
        public_model_state(model::model_state()?)
    };
    let health = if admin_signed_in {
        supervisor::runtime_health()?
    } else {
        public_runtime_health(supervisor::runtime_health()?)
    };
    Ok(AppState {
        domain_authority,
        product_name: "Townlight",
        status_label: "Windows Local 1.0 desktop",
        local_only: true,
        navigation: navigation(),
        modules: module_summaries()?,
        module_profiles: module_registry::module_profiles()?,
        module_selection: module_registry::module_selection_state()?,
        installer_steps: installer_steps(),
        first_run: first_run::first_run_state(&[])?,
        city_profile: first_run::saved_city_profile()?,
        users,
        access,
        model,
        health,
        city_work,
    })
}

#[tauri::command]
async fn get_app_state() -> Result<AppState, String> {
    tauri::async_runtime::spawn_blocking(move || {
        catch_unwind(AssertUnwindSafe(app_state)).map_err(|_| {
            "App state stopped unexpectedly. Restart Townlight and retry; if this repeats, use Repair from System Health."
                .to_string()
        })?
    })
    .await
    .map_err(|error| format!("App state could not load: {error}"))?
}

#[tauri::command]
fn get_model_state() -> Result<ModelState, String> {
    let access = auth::access_state()?;
    let model = model::model_state()?;
    if access_is_local_admin(&access) {
        Ok(model)
    } else {
        Ok(public_model_state(model))
    }
}

fn model_action_authorized(action: String) -> Result<ModelActionResult, String> {
    let access = auth::access_state()?;
    if !access_is_local_admin(&access) {
        if !access.configured {
            return Err(
                "Create the first Townlight admin and sign in before changing local model setup."
                    .to_string(),
            );
        }
        return Err(
            "Sign in as the Townlight admin before changing local model setup.".to_string(),
        );
    }
    model::model_action(&action)
}

#[tauri::command]
async fn model_action(action: String) -> Result<ModelActionResult, String> {
    tauri::async_runtime::spawn_blocking(move || {
        catch_unwind(AssertUnwindSafe(|| model_action_authorized(action)))
            .map_err(|_| "Model setup action stopped unexpectedly. Restart Townlight and retry; if this repeats, use Repair from System Health.".to_string())?
    })
    .await
    .map_err(|error| format!("Model setup action could not complete: {error}"))?
}

#[tauri::command]
fn preview_first_run_state(completed_step_ids: Vec<String>) -> Result<FirstRunState, String> {
    first_run::first_run_state(&completed_step_ids)
}

fn first_run_action_authorized(
    action: String,
    step_id: Option<String>,
    payload: Option<Value>,
) -> Result<FirstRunActionResult, String> {
    let access = auth::access_state()?;
    if access.configured
        && !access_is_local_admin(&access)
        && first_run_action_requires_admin_after_setup(&action)
    {
        return Err(
            "Sign in as the Townlight admin before changing setup, profile, model, backup, or runtime settings."
                .to_string(),
        );
    }
    first_run::first_run_action(&action, step_id.as_deref(), payload.as_ref())
}

#[tauri::command]
async fn first_run_action(
    action: String,
    step_id: Option<String>,
    payload: Option<Value>,
) -> Result<FirstRunActionResult, String> {
    tauri::async_runtime::spawn_blocking(move || {
        catch_unwind(AssertUnwindSafe(|| {
            first_run_action_authorized(action, step_id, payload)
        }))
        .map_err(|_| {
            "First-run action stopped unexpectedly. Restart Townlight and retry; if this repeats, use Repair from System Health."
                .to_string()
        })?
    })
    .await
    .map_err(|error| format!("First-run action could not complete: {error}"))?
}

#[tauri::command]
fn auth_action(action: String, payload: Option<Value>) -> Result<AuthActionResult, String> {
    auth::auth_action(&action, payload.as_ref())
}

fn supervisor_action_authorized(
    action: String,
    service_id: Option<String>,
) -> Result<SupervisorActionResult, String> {
    if action != "health" {
        auth::require_admin_session()?;
    }
    supervisor::supervisor_action(&action, service_id.as_deref())
}

#[tauri::command]
async fn supervisor_action(
    action: String,
    service_id: Option<String>,
) -> Result<SupervisorActionResult, String> {
    tauri::async_runtime::spawn_blocking(move || {
        catch_unwind(AssertUnwindSafe(|| {
            supervisor_action_authorized(action, service_id)
        }))
        .map_err(|_| {
            "System Health action stopped unexpectedly. Restart Townlight and retry; if this repeats, use Repair from System Health."
                .to_string()
        })?
    })
    .await
    .map_err(|error| format!("System Health action could not complete: {error}"))?
}

fn picked_file_path_for_desktop() -> Result<Option<String>, String> {
    if let Ok(path) = std::env::var("TOWNLIGHT_TEST_FILE_PICKER_PATH") {
        let trimmed = path.trim();
        return Ok(if trimmed.is_empty() {
            None
        } else {
            Some(trimmed.to_string())
        });
    }

    #[cfg(target_os = "windows")]
    {
        Ok(rfd::FileDialog::new()
            .set_title("Choose Townlight evidence file")
            .pick_file()
            .map(|path| path.display().to_string()))
    }

    #[cfg(not(target_os = "windows"))]
    {
        Err("Native file selection is only available in the Windows desktop app.".to_string())
    }
}

fn picked_folder_path_for_desktop() -> Result<Option<String>, String> {
    if let Ok(path) = std::env::var("TOWNLIGHT_TEST_FOLDER_PICKER_PATH") {
        let trimmed = path.trim();
        return Ok(if trimmed.is_empty() {
            None
        } else {
            Some(trimmed.to_string())
        });
    }

    #[cfg(target_os = "windows")]
    {
        Ok(rfd::FileDialog::new()
            .set_title("Choose Townlight folder")
            .pick_folder()
            .map(|path| path.display().to_string()))
    }

    #[cfg(not(target_os = "windows"))]
    {
        Err("Native folder selection is only available in the Windows desktop app.".to_string())
    }
}

#[tauri::command]
fn choose_file_path() -> Result<Option<String>, String> {
    auth::require_signed_in_session()?;
    picked_file_path_for_desktop()
}

#[tauri::command]
fn choose_folder_path() -> Result<Option<String>, String> {
    let access = auth::access_state()?;
    if access.configured && !access_is_local_admin(&access) {
        return Err("Sign in as the Townlight admin before choosing folders.".to_string());
    }
    picked_folder_path_for_desktop()
}

#[tauri::command]
fn module_action(action: String, module_id: String) -> Result<ModuleActionResult, String> {
    let access = auth::access_state()?;
    if access.configured && !access_is_local_admin(&access) {
        return Err(
            "Sign in as the Townlight admin before changing installed modules.".to_string(),
        );
    }
    let previous_selection = module_registry::module_selection_state()?;
    let was_installed = previous_selection
        .installed_module_ids
        .iter()
        .any(|installed_id| installed_id == &module_id);
    let display_name = module_display_name(&module_id);
    let mut removal_backup_message = None;
    let selection = match action.as_str() {
        "enable-module" => module_registry::set_module_enabled(&module_id, true)?,
        "disable-module" => module_registry::set_module_enabled(&module_id, false)?,
        "install-module" => module_registry::set_module_installed(&module_id, true)?,
        "remove-module" => {
            if was_installed {
                let backup = supervisor::supervisor_action("backup", None)?;
                removal_backup_message = Some(backup.message);
                module_registry::set_module_installed(&module_id, false)?
            } else {
                previous_selection
            }
        }
        "update-module" => previous_selection,
        "open-module-exports" => {
            if !was_installed {
                return Err(format!(
                    "{display_name} is not installed in this local profile. Install it before opening its exports folder."
                ));
            }
            let Some((folder, _label)) = module_exports_folder(&module_id) else {
                return Err(format!(
                    "{display_name} does not have a module exports folder."
                ));
            };
            let path = local_paths::data_root().join("exports").join(folder);
            local_shell::open_local_folder(&path)?;
            previous_selection
        }
        _ => return Err(format!("Unsupported module action: {action}")),
    };
    let modules = module_summaries()?;
    let is_installed = selection
        .installed_module_ids
        .iter()
        .any(|installed_id| installed_id == &module_id);
    let is_enabled = selection
        .enabled_module_ids
        .iter()
        .any(|enabled_id| enabled_id == &module_id);
    let (status, message, next_action) = match action.as_str() {
        "enable-module" => (
            "Module enabled",
            format!("{display_name} is enabled in the local Townlight shell."),
            "Review the module list or continue city work.",
        ),
        "disable-module" => (
            "Module disabled",
            format!("{display_name} is disabled in the local Townlight shell. Its data remains installed and can be re-enabled."),
            "Review the module list or continue city work.",
        ),
        "install-module" if was_installed => (
            "Module already installed",
            format!("{display_name} is already installed in this local profile."),
            "Enable the module if it is disabled, or continue city work.",
        ),
        "install-module" => (
            "Module installed",
            format!("{display_name} is installed and enabled in this local profile."),
            "Review the module list or open the module work area.",
        ),
        "remove-module" if !was_installed => (
            "Module not installed",
            format!("{display_name} is not installed in this local profile."),
            "Choose an available module to install.",
        ),
        "remove-module" => (
            "Module removed",
            format!(
                "{display_name} is removed from the active local profile after a verified profile backup. Existing module data was not deleted and remains covered by profile backup/restore. {}",
                removal_backup_message
                    .as_deref()
                    .unwrap_or("The profile backup completed before removal.")
            ),
            "Install the module again if staff need this work area.",
        ),
        "update-module" => (
            "Module current",
            if is_installed {
                format!("{display_name} is already on the pinned version for this module manifest.")
            } else {
                format!("{display_name} is not installed; install it before checking updates.")
            },
            "Review module versions in Settings.",
        ),
        "open-module-exports" => {
            let (folder, label) =
                module_exports_folder(&module_id).expect("module export folder already checked");
            let path = local_paths::data_root().join("exports").join(folder);
            (
                "Module exports opened",
                format!(
                    "Opened the local {label} folder for {display_name}: {}",
                    path.display()
                ),
                "Use generated export files for clerk review, public records response, or local backup evidence.",
            )
        }
        _ => unreachable!("unsupported module action already returned"),
    };
    Ok(ModuleActionResult {
        accepted: true,
        action,
        status,
        message,
        next_action: if is_enabled || !matches!(status, "Module installed") {
            next_action
        } else {
            "Enable required dependencies before opening this module."
        },
        selection,
        modules,
    })
}

#[tauri::command]
fn get_city_work_state() -> Result<CityWorkState, String> {
    let access = auth::access_state()?;
    if access_is_signed_in(&access) {
        workflows::city_work_state()
    } else {
        workflows::public_city_work_state()
    }
}

/// Typed domain transport remains inactive until coherent profile activation.
/// Retired JSON municipal actions cannot substitute for that activation.
#[tauri::command]
async fn records_domain_request(
    request: domain_client::RecordsRequest,
) -> Result<Value, domain_client::DomainError> {
    let _operation =
        profile_gate::operation().map_err(|_| domain_client::DomainError::maintenance())?;
    let access =
        auth::require_signed_in_session().map_err(|_| domain_client::DomainError::denied())?;
    require_role_for_city_work(&access, "create-records-request", None)
        .map_err(|_| domain_client::DomainError::denied())?;
    require_enabled_city_modules("create-records-request", None)
        .map_err(|_| domain_client::DomainError::denied())?;
    let proof = auth::domain_session_proof().map_err(|_| domain_client::DomainError::denied())?;
    domain_client::execute(request, proof).await
}

#[tauri::command]
async fn records_public_request(
    request: domain_client::PublicRecordsRequest,
) -> Result<Value, domain_client::DomainError> {
    let _operation =
        profile_gate::operation().map_err(|_| domain_client::DomainError::maintenance())?;
    require_enabled_city_modules("submit-public-records-request", None)
        .map_err(|_| domain_client::DomainError::denied())?;
    let proof = auth::domain_public_proof().map_err(|_| domain_client::DomainError::denied())?;
    domain_client::execute_public(request, proof).await
}

#[tauri::command]
async fn access_domain_request(
    request: domain_client::AccessRequest,
) -> Result<Value, domain_client::DomainError> {
    let _operation =
        profile_gate::operation().map_err(|_| domain_client::DomainError::maintenance())?;
    let access =
        auth::require_signed_in_session().map_err(|_| domain_client::DomainError::denied())?;
    require_role_for_city_work(&access, "accessibility-review", None)
        .map_err(|_| domain_client::DomainError::denied())?;
    require_enabled_city_modules("accessibility-review", None)
        .map_err(|_| domain_client::DomainError::denied())?;
    let proof = auth::domain_session_proof().map_err(|_| domain_client::DomainError::denied())?;
    domain_client::execute_access(request, proof).await
}

#[tauri::command]
async fn suite_domain_request(
    request: domain_client::SuiteRequest,
) -> Result<Value, domain_client::DomainError> {
    let _operation =
        profile_gate::operation().map_err(|_| domain_client::DomainError::maintenance())?;
    let access =
        auth::require_signed_in_session().map_err(|_| domain_client::DomainError::denied())?;
    require_role_for_city_work(&access, "search-city-knowledge", None)
        .map_err(|_| domain_client::DomainError::denied())?;
    require_enabled_city_modules("search-city-knowledge", None)
        .map_err(|_| domain_client::DomainError::denied())?;
    let proof = auth::domain_session_proof().map_err(|_| domain_client::DomainError::denied())?;
    domain_client::execute_suite(request, proof).await
}

#[tauri::command]
async fn meetings_domain_request(
    request: domain_client::MeetingsRequest,
) -> Result<Value, domain_client::DomainError> {
    let _operation =
        profile_gate::operation().map_err(|_| domain_client::DomainError::maintenance())?;
    let profile_id = module_registry::module_selection_state()
        .map_err(|_| domain_client::DomainError::denied())?
        .profile_id;
    if !matches!(
        profile_id.as_str(),
        "meetings-beta"
            | "city-core"
            | "zoning-beta"
            | "planning-preview"
            | "permitting-preview"
            | "inspections-preview"
    ) {
        return Err(domain_client::DomainError::denied());
    }
    let access =
        auth::require_signed_in_session().map_err(|_| domain_client::DomainError::denied())?;
    require_role_for_city_work(&access, "create-meeting", None)
        .map_err(|_| domain_client::DomainError::denied())?;
    require_enabled_city_modules("create-meeting", None)
        .map_err(|_| domain_client::DomainError::denied())?;
    let proof = auth::domain_session_proof().map_err(|_| domain_client::DomainError::denied())?;
    domain_client::execute_meetings(request, proof).await
}

#[tauri::command]
async fn code_domain_request(
    request: code_client::CodeRequest,
) -> Result<Value, domain_client::DomainError> {
    let _operation =
        profile_gate::operation().map_err(|_| domain_client::DomainError::maintenance())?;
    let profile_id = module_registry::module_selection_state()
        .map_err(|_| domain_client::DomainError::denied())?
        .profile_id;
    if !matches!(
        profile_id.as_str(),
        "city-core"
            | "zoning-beta"
            | "planning-preview"
            | "permitting-preview"
            | "inspections-preview"
    ) {
        return Err(domain_client::DomainError::denied());
    }
    let access =
        auth::require_signed_in_session().map_err(|_| domain_client::DomainError::denied())?;
    require_role_for_city_work(&access, "import-code-source", None)
        .map_err(|_| domain_client::DomainError::denied())?;
    require_enabled_city_modules("import-code-source", None)
        .map_err(|_| domain_client::DomainError::denied())?;
    let proof = auth::domain_session_proof().map_err(|_| domain_client::DomainError::denied())?;
    code_client::execute(request, proof).await
}

#[tauri::command]
async fn open_domain_exports(request: domain_files::OpenExports) -> Result<Value, String> {
    let access = auth::require_signed_in_session()?;
    let payload = serde_json::json!({"folder":request.folder.name()});
    require_role_for_city_work(&access, "open-exports-folder", Some(&payload))?;
    require_enabled_city_modules("open-exports-folder", Some(&payload))?;
    let message =
        tauri::async_runtime::spawn_blocking(move || domain_files::open_exports(request.folder))
            .await
            .map_err(|_| "Could not open the local exports folder.".to_string())??;
    Ok(serde_json::json!({"message":message}))
}

#[tauri::command]
async fn zoning_domain_request(
    request: zoning_client::ZoningRequest,
) -> Result<Value, domain_client::DomainError> {
    let _guard =
        profile_gate::operation().map_err(|_| domain_client::DomainError::maintenance())?;
    let selection = module_registry::module_selection_state()
        .map_err(|_| domain_client::DomainError::denied())?;
    if !selection
        .enabled_module_ids
        .iter()
        .any(|id| id == "townlight_zoning")
    {
        return Err(domain_client::DomainError::denied());
    }
    let session =
        auth::require_signed_in_session().map_err(|_| domain_client::DomainError::denied())?;
    let role = session.role.as_deref();
    if role != Some("local-admin") && role != Some("city-staff") {
        return Err(domain_client::DomainError::denied());
    }
    let proof = auth::domain_session_proof().map_err(|_| domain_client::DomainError::denied())?;
    zoning_client::execute(request, proof, false).await
}

#[tauri::command]
async fn zoning_public_request(
    request: zoning_client::ZoningRequest,
) -> Result<Value, domain_client::DomainError> {
    let _guard =
        profile_gate::operation().map_err(|_| domain_client::DomainError::maintenance())?;
    let selection = module_registry::module_selection_state()
        .map_err(|_| domain_client::DomainError::denied())?;
    if !selection
        .enabled_module_ids
        .iter()
        .any(|id| id == "townlight_zoning")
    {
        return Err(domain_client::DomainError::denied());
    }
    zoning_client::execute(request, String::new(), true).await
}

fn planning_capability(staff: bool) -> Result<String, domain_client::DomainError> {
    let selection = module_registry::module_selection_state()
        .map_err(|_| domain_client::DomainError::denied())?;
    if !matches!(
        selection.profile_id.as_str(),
        "planning-preview" | "permitting-preview" | "inspections-preview"
    ) || !selection
        .enabled_module_ids
        .iter()
        .any(|id| id == "townlight_planning")
    {
        return Err(domain_client::DomainError::denied());
    }
    if !staff {
        return Ok(String::new());
    }
    let session =
        auth::require_signed_in_session().map_err(|_| domain_client::DomainError::denied())?;
    if !matches!(
        session.role.as_deref(),
        Some("local-admin") | Some("city-staff")
    ) {
        return Err(domain_client::DomainError::denied());
    }
    auth::domain_session_proof().map_err(|_| domain_client::DomainError::denied())
}

#[tauri::command]
async fn planning_domain_request(
    request: planning_client::PlanningRequest,
) -> Result<Value, domain_client::DomainError> {
    let _guard =
        profile_gate::operation().map_err(|_| domain_client::DomainError::maintenance())?;
    planning_client::execute(request, planning_capability(true)?, false).await
}

fn permitting_capability() -> Result<String, domain_client::DomainError> {
    let selection = module_registry::module_selection_state()
        .map_err(|_| domain_client::DomainError::denied())?;
    if !matches!(
        selection.profile_id.as_str(),
        "permitting-preview" | "inspections-preview"
    ) || !selection
        .enabled_module_ids
        .iter()
        .any(|id| id == "townlight_permitting")
    {
        return Err(domain_client::DomainError::denied());
    }
    planning_capability(true)
}

#[tauri::command]
async fn permitting_domain_request(
    request: permitting_client::PermittingRequest,
) -> Result<Value, domain_client::DomainError> {
    let _guard =
        profile_gate::operation().map_err(|_| domain_client::DomainError::maintenance())?;
    permitting_client::execute(request, permitting_capability()?).await
}

fn inspections_capability() -> Result<String, domain_client::DomainError> {
    let selection = module_registry::module_selection_state()
        .map_err(|_| domain_client::DomainError::denied())?;
    if !matches!(selection.profile_id.as_str(), "inspections-preview")
        || !selection
            .enabled_module_ids
            .iter()
            .any(|id| id == "townlight_inspections")
    {
        return Err(domain_client::DomainError::denied());
    }
    planning_capability(true)
}

#[tauri::command]
async fn inspections_domain_request(
    request: inspections_client::InspectionsRequest,
) -> Result<Value, domain_client::DomainError> {
    let _guard =
        profile_gate::operation().map_err(|_| domain_client::DomainError::maintenance())?;
    inspections_client::execute(request, inspections_capability()?).await
}

#[tauri::command]
async fn permitting_pick_bundle() -> Result<Value, String> {
    permitting_capability().map_err(|_| "Sign in with an authorized Permitting account.")?;
    planning_pick_bundle().await
}

#[tauri::command]
async fn planning_public_request(
    request: planning_client::PlanningRequest,
) -> Result<Value, domain_client::DomainError> {
    let _guard =
        profile_gate::operation().map_err(|_| domain_client::DomainError::maintenance())?;
    planning_client::execute(request, planning_capability(false)?, true).await
}

#[tauri::command]
async fn planning_pick_bundle() -> Result<Value, String> {
    let _guard = profile_gate::operation().map_err(|_| "Profile maintenance in progress.")?;
    planning_capability(true).map_err(|_| "Sign in with an authorized Planning account.")?;
    tauri::async_runtime::spawn_blocking(|| {
        #[cfg(windows)]
        {
            use std::io::Read;
            let Some(path) = picked_file_path_for_desktop()? else {
                return Ok(Value::Null);
            };
            let mut file =
                std::fs::File::open(path).map_err(|_| "Could not read the selected bundle.")?;
            let mut bytes = Vec::new();
            file.by_ref()
                .take(36_000_001)
                .read_to_end(&mut bytes)
                .map_err(|_| "Could not read the bundle.")?;
            if bytes.len() > 36_000_000 {
                return Err("Bundle exceeds the local import limit.".into());
            }
            serde_json::from_slice(&bytes)
                .map_err(|_| "Choose a valid Planning JSON source bundle.".into())
        }
        #[cfg(not(windows))]
        {
            Err("Planning file picker requires Windows.".into())
        }
    })
    .await
    .map_err(|_| "Could not complete the source picker.".to_string())?
}

fn city_work_action_authorized(
    action: String,
    payload: Option<Value>,
) -> Result<CityWorkActionResult, String> {
    let mut payload = payload;
    let access = auth::access_state()?;
    let signed_in = access_is_signed_in(&access);
    let public_action = workflows::city_work_action_allows_public(&action);
    if action == "load-demo-town" && !access_is_local_admin(&access) {
        return Err(
            "Sign in with a Townlight admin account before loading demonstration data.".to_string(),
        );
    }
    if !signed_in {
        if !access.configured {
            return Err(
                "Finish first-run setup before public city workflows are available.".to_string(),
            );
        }
        if !public_action {
            auth::require_signed_in_session()?;
        }
        if action == "answer-code-question" {
            let mut public_payload = payload.unwrap_or_else(|| serde_json::json!({}));
            let Some(fields) = public_payload.as_object_mut() else {
                return Err("Public code questions must use a JSON object payload.".to_string());
            };
            fields.insert("publicOnly".to_string(), Value::Bool(true));
            payload = Some(public_payload);
        }
    } else {
        require_role_for_city_work(&access, &action, payload.as_ref())?;
    }
    require_enabled_city_modules(&action, payload.as_ref())?;
    if module_registry::module_selection_state()?.profile_id == "meetings-beta" {
        // This development profile has a single Python Meetings authority.
        // Unmigrated shared actions stay unavailable, never write old JSON.
        return Err("Use the typed Python product workflow in this Meetings candidate; legacy actions are unavailable.".into());
    }
    let mut result = workflows::city_work_action(&action, payload.as_ref())?;
    if action == "search-city-knowledge" {
        filter_search_results_for_enabled_modules(&mut result)?;
        filter_search_results_for_access(&mut result, &access);
        result.message = format!(
            "Local search completed across enabled modules with {} result(s).",
            result.search_results.len()
        );
    }
    if !signed_in
        && public_action
        && action != "lookup-public-records-request"
        && action != "add-public-records-message"
    {
        result.state = workflows::city_work_public_projection(&result.state);
    }
    Ok(result)
}

#[tauri::command]
async fn city_work_action(
    action: String,
    payload: Option<Value>,
) -> Result<CityWorkActionResult, String> {
    tauri::async_runtime::spawn_blocking(move || {
        catch_unwind(AssertUnwindSafe(|| {
            city_work_action_authorized(action, payload)
        }))
        .map_err(|_| {
            "City workflow action stopped unexpectedly. Restart Townlight and retry; if this repeats, use Repair from System Health."
                .to_string()
        })?
    })
    .await
    .map_err(|error| format!("City workflow action could not complete: {error}"))?
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
            // A second launch was attempted. Do NOT start a second process that
            // would concurrently read-modify-write the system-of-record files.
            // Focus the already-running instance's window instead.
            use tauri::Manager;
            if let Some(window) = app.get_webview_window("main") {
                let _ = window.unminimize();
                let _ = window.show();
                let _ = window.set_focus();
            }
        }))
        .on_window_event(|_window, event| {
            if matches!(event, tauri::WindowEvent::CloseRequested { .. }) {
                let _ = supervisor::supervisor_action("stop", None);
            }
        })
        .invoke_handler(tauri::generate_handler![
            get_app_state,
            get_model_state,
            model_action,
            preview_first_run_state,
            first_run_action,
            auth_action,
            supervisor_action,
            choose_file_path,
            choose_folder_path,
            module_action,
            get_city_work_state,
            records_domain_request,
            records_public_request,
            access_domain_request,
            suite_domain_request,
            meetings_domain_request,
            code_domain_request,
            zoning_domain_request,
            zoning_public_request,
            planning_domain_request,
            permitting_domain_request,
            inspections_domain_request,
            permitting_pick_bundle,
            planning_public_request,
            planning_pick_bundle,
            open_domain_exports,
            city_work_action
        ])
        .run(tauri::generate_context!())
        .expect("failed to run Townlight desktop");
}

fn main() {
    run();
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::env;
    use std::fs;
    use std::path::PathBuf;

    fn with_clean_first_run_state<T>(test: impl FnOnce(PathBuf) -> T) -> T {
        let _guard = first_run::test_env_lock().lock().expect("test env lock");
        let root = env::temp_dir().join(format!(
            "townlight-desktop-app-state-test-{}",
            std::process::id()
        ));
        let _ = fs::remove_dir_all(&root);
        env::set_var("TOWNLIGHT_DESKTOP_STATE_DIR", &root);
        let result = test(root.clone());
        env::remove_var("TOWNLIGHT_DESKTOP_STATE_DIR");
        let _ = fs::remove_dir_all(root);
        result
    }

    fn create_first_admin() {
        first_run::first_run_action("choose-location", Some("locations"), None)
            .expect("locations saved");
        let module_payload = serde_json::json!({ "profileId": "city-core" });
        first_run::first_run_action("select-modules", Some("modules"), Some(&module_payload))
            .expect("modules saved");
        let city_payload = serde_json::json!({
            "cityName": "Brookfield",
            "state": "CO",
            "timeZone": "America/Denver",
            "recordsContact": "records@example.gov",
            "clerkContact": "clerk@example.gov"
        });
        first_run::first_run_action(
            "create-city-profile",
            Some("city-profile"),
            Some(&city_payload),
        )
        .expect("city profile saved");
        let admin_payload = serde_json::json!({
            "adminName": "Alex Clerk",
            "adminEmail": "alex@example.gov",
            "adminPasscode": "correct horse battery staple"
        });
        let result =
            first_run::first_run_action("create-admin", Some("first-admin"), Some(&admin_payload))
                .expect("admin saved");
        assert!(result.accepted);
    }

    fn sign_in_as_first_admin() {
        let sign_in_payload = serde_json::json!({
            "email": "alex@example.gov",
            "passcode": "correct horse battery staple"
        });
        auth::auth_action("sign-in", Some(&sign_in_payload)).expect("admin signed in");
    }

    fn create_staff_user(name: &str, email: &str, role: &str, passcode: &str) {
        let payload = serde_json::json!({
            "userName": name,
            "userEmail": email,
            "userRole": role,
            "userPasscode": passcode
        });
        auth::auth_action("create-user", Some(&payload)).expect("staff user saved");
    }

    fn sign_in_as_user(email: &str, passcode: &str) {
        let payload = serde_json::json!({
            "email": email,
            "passcode": passcode
        });
        auth::auth_action("sign-in", Some(&payload)).expect("user signed in");
    }

    #[test]
    fn records_beta_modules_are_reported_installed() {
        module_registry::validate_default_registry().expect("module registry contract validates");
        let modules = module_summaries().expect("module registry parses");
        for module_id in ["townlight_core", "townlight_records", "townlight_notice", "townlight_access"] {
            let module = modules
                .iter()
                .find(|candidate| candidate.id == module_id)
                .expect("records-beta module exists");
            assert!(
                module.installed,
                "{module_id} should be installed in records-beta"
            );
        }
        for module_id in ["townlight_meetings", "townlight_code"] {
            let module = modules
                .iter()
                .find(|candidate| candidate.id == module_id)
                .expect("queued module exists");
            assert!(
                !module.installed,
                "{module_id} should not be installed in records-beta"
            );
        }
    }

    #[test]
    fn navigation_is_task_first() {
        let labels: Vec<&str> = navigation().into_iter().map(|item| item.label).collect();
        assert!(labels.contains(&"Meetings & Notices"));
        assert!(labels.contains(&"Records Requests"));
        assert!(labels.contains(&"Code & Ordinances"));
        assert!(!labels.contains(&"Docker"));
        assert!(!labels.contains(&"WSL"));
    }

    #[test]
    fn navigation_matches_frontend_fallback_state() {
        // The live shell renders navigation() while main.js keeps a hardcoded
        // fallbackState list for the offline path. v1.0.2 shipped with `access`
        // present in the fallback but absent here, so the Accessibility tab was
        // unreachable on every install. Keep the two ids in lockstep.
        let main_js = include_str!("../../src/main.js");
        let start = main_js
            .find("navigation: [")
            .expect("fallbackState.navigation block");
        let end = main_js[start..]
            .find("].map(")
            .expect("end of fallbackState.navigation");
        let block = &main_js[start..start + end];

        for item in navigation() {
            let needle = format!("[\"{}\"", item.id);
            assert!(
                block.contains(&needle),
                "navigation() id `{}` is missing from main.js fallbackState.navigation",
                item.id
            );
        }
        let rust_ids: Vec<&str> = navigation().into_iter().map(|item| item.id).collect();
        for line in block.lines() {
            let trimmed = line.trim();
            if !trimmed.starts_with("[\"") {
                continue;
            }
            let id = trimmed
                .trim_start_matches("[\"")
                .split('"')
                .next()
                .expect("fallback nav id");
            assert!(
                rust_ids.contains(&id),
                "main.js fallbackState.navigation id `{id}` is missing from navigation()"
            );
        }
    }

    #[test]
    fn app_state_reports_runtime_health_from_manifest() {
        with_clean_first_run_state(|_| {
            let state = app_state().expect("app state builds");
            assert!(state.health.iter().any(|item| item.id == "desktop-shell"));
            assert!(state.health.iter().any(|item| item.id == "postgres"));
            assert!(state.health.iter().any(|item| item.id == "model-runtime"));
        });
    }

    #[test]
    fn app_state_reports_model_readiness_contract() {
        with_clean_first_run_state(|_| {
            let state = app_state().expect("app state builds");
            assert_eq!(state.model.display_name, "Gemma 4 12B QAT Q4_0");
            assert_eq!(state.model.status, "Needs download");
            assert!(state.model.artifact.checksum_required);
            assert!(state
                .model
                .checks
                .iter()
                .any(|check| check.id == "checksum" && !check.ok));
        });
    }

    #[test]
    fn model_state_hides_local_path_without_admin_session() {
        with_clean_first_run_state(|_| {
            create_first_admin();

            let public_model = get_model_state().expect("model state");
            assert_eq!(
                public_model.artifact.local_path,
                "Sign in as Townlight admin to view the model file path."
            );

            sign_in_as_first_admin();
            let staff_model = get_model_state().expect("staff model state");
            assert!(staff_model
                .artifact
                .local_path
                .contains("gemma-4-12b-it-qat-q4_0.gguf"));
        });
    }

    #[test]
    fn model_actions_require_local_admin_session() {
        with_clean_first_run_state(|_| {
            let pre_admin_result = model_action_authorized("open-model-folder".to_string());
            assert!(pre_admin_result.is_err());
            assert!(pre_admin_result
                .err()
                .expect("pre-admin model action auth error")
                .contains("Create the first Townlight admin and sign in"));

            create_first_admin();

            let signed_out_result = model_action_authorized("open-model-folder".to_string());

            assert!(signed_out_result.is_err());
            assert!(signed_out_result
                .err()
                .expect("model action auth error")
                .contains("Sign in as the Townlight admin"));

            sign_in_as_first_admin();
            let signed_in_result = model_action_authorized("open-model-folder".to_string())
                .expect("model action allowed");
            assert!(signed_in_result.accepted);
        });
    }

    #[test]
    fn supervisor_command_wrapper_completes_backup_action() {
        with_clean_first_run_state(|root| {
            create_first_admin();
            sign_in_as_first_admin();

            let result =
                tauri::async_runtime::block_on(supervisor_action("backup".to_string(), None))
                    .expect("backup command returns through async wrapper");

            assert!(result.accepted);
            assert_eq!(result.status, "Backup complete");
            assert!(result.message.contains("backup-manifest.json"));
            assert!(fs::read_dir(root.join("Backups"))
                .expect("backup root exists")
                .any(|entry| entry
                    .expect("backup entry")
                    .path()
                    .join("backup-manifest.json")
                    .is_file()));
        });
    }

    #[test]
    fn module_actions_require_admin_after_first_admin_exists() {
        with_clean_first_run_state(|root| {
            create_first_admin();

            let signed_out_result =
                module_action("disable-module".to_string(), "townlight_code".to_string());

            assert!(signed_out_result.is_err());
            assert!(signed_out_result
                .err()
                .expect("module action auth error")
                .contains("Sign in as the Townlight admin"));

            sign_in_as_first_admin();
            let disabled = module_action("disable-module".to_string(), "townlight_code".to_string())
                .expect("module disable allowed");
            assert!(disabled.accepted);
            assert_eq!(disabled.status, "Module disabled");
            assert!(disabled
                .selection
                .installed_module_ids
                .iter()
                .any(|module_id| module_id == "townlight_code"));
            assert!(!disabled
                .selection
                .enabled_module_ids
                .iter()
                .any(|module_id| module_id == "townlight_code"));
            assert!(disabled
                .modules
                .iter()
                .any(|module| module.id == "townlight_code" && module.installed && !module.enabled));

            let enabled = module_action("enable-module".to_string(), "townlight_code".to_string())
                .expect("module enable allowed");
            assert!(enabled
                .selection
                .enabled_module_ids
                .iter()
                .any(|module_id| module_id == "townlight_code"));

            let current = module_action("update-module".to_string(), "townlight_code".to_string())
                .expect("module update check allowed");
            assert_eq!(current.status, "Module current");
            assert!(current.message.contains("pinned version"));

            let opened_exports =
                module_action("open-module-exports".to_string(), "townlight_code".to_string())
                    .expect("module exports open");
            assert_eq!(opened_exports.status, "Module exports opened");
            assert!(opened_exports.message.contains("code exports"));
            assert!(root.join("Data").join("exports").join("code").is_dir());

            let removed = module_action("remove-module".to_string(), "townlight_code".to_string())
                .expect("module remove allowed");
            assert_eq!(removed.status, "Module removed");
            assert!(removed.message.contains("verified profile backup"));
            assert!(removed.message.contains("was not deleted"));
            assert!(!removed
                .selection
                .installed_module_ids
                .iter()
                .any(|module_id| module_id == "townlight_code"));
            let backup_count = fs::read_dir(root.join("Backups"))
                .expect("backup root exists")
                .filter(|entry| {
                    entry
                        .as_ref()
                        .map(|entry| entry.path().join("backup-manifest.json").is_file())
                        .unwrap_or(false)
                })
                .count();
            assert_eq!(backup_count, 1);

            let removed_exports =
                module_action("open-module-exports".to_string(), "townlight_code".to_string());
            assert!(removed_exports.is_err());
            assert!(removed_exports
                .err()
                .expect("removed module export error")
                .contains("is not installed in this local profile"));

            let installed = module_action("install-module".to_string(), "townlight_code".to_string())
                .expect("module install allowed");
            assert_eq!(installed.status, "Module installed");
            assert!(installed
                .selection
                .enabled_module_ids
                .iter()
                .any(|module_id| module_id == "townlight_code"));
        });
    }

    #[test]
    fn choose_file_path_requires_signed_in_staff_and_uses_native_picker_result() {
        with_clean_first_run_state(|_| {
            create_first_admin();
            env::set_var(
                "TOWNLIGHT_TEST_FILE_PICKER_PATH",
                r"C:\City\Records\packet.pdf",
            );
            let signed_out = choose_file_path();
            env::remove_var("TOWNLIGHT_TEST_FILE_PICKER_PATH");
            assert!(signed_out.is_err());

            sign_in_as_first_admin();
            env::set_var(
                "TOWNLIGHT_TEST_FILE_PICKER_PATH",
                r"C:\City\Records\packet.pdf",
            );
            let picked = choose_file_path().expect("signed-in file picker");
            env::remove_var("TOWNLIGHT_TEST_FILE_PICKER_PATH");

            assert_eq!(picked.as_deref(), Some(r"C:\City\Records\packet.pdf"));
        });
    }

    #[test]
    fn choose_folder_path_allows_first_run_then_requires_local_admin() {
        with_clean_first_run_state(|_| {
            env::set_var(
                "TOWNLIGHT_TEST_FOLDER_PICKER_PATH",
                r"C:\City\TownlightData",
            );
            let first_run_folder = choose_folder_path().expect("pre-admin folder picker");
            env::remove_var("TOWNLIGHT_TEST_FOLDER_PICKER_PATH");
            assert_eq!(first_run_folder.as_deref(), Some(r"C:\City\TownlightData"));

            create_first_admin();
            env::set_var(
                "TOWNLIGHT_TEST_FOLDER_PICKER_PATH",
                r"C:\City\TownlightBackups",
            );
            let signed_out = choose_folder_path();
            env::remove_var("TOWNLIGHT_TEST_FOLDER_PICKER_PATH");
            assert!(signed_out.is_err());

            sign_in_as_first_admin();
            env::set_var(
                "TOWNLIGHT_TEST_FOLDER_PICKER_PATH",
                r"C:\City\TownlightBackups",
            );
            let picked = choose_folder_path().expect("admin folder picker");
            env::remove_var("TOWNLIGHT_TEST_FOLDER_PICKER_PATH");

            assert_eq!(picked.as_deref(), Some(r"C:\City\TownlightBackups"));
        });
    }

    #[test]
    fn staff_user_roles_gate_city_work_modules() {
        with_clean_first_run_state(|_| {
            create_first_admin();
            sign_in_as_first_admin();
            create_staff_user(
                "Riley Records",
                "riley@example.gov",
                "records-staff",
                "records passcode 123",
            );
            create_staff_user(
                "Casey Clerk",
                "casey@example.gov",
                "clerk",
                "clerk passcode 123",
            );
            create_staff_user(
                "Jordan Code",
                "jordan@example.gov",
                "code-staff",
                "code passcode 123",
            );
            auth::auth_action("sign-out", None).expect("admin signed out");

            sign_in_as_user("riley@example.gov", "records passcode 123");
            let records_payload = serde_json::json!({
                "requester": "Alex Rivera",
                "summary": "Emails about park contract",
                "deadline": "2026-07-10"
            });
            assert!(require_role_for_city_work(
                &auth::access_state().unwrap(),
                "create-records-request",
                Some(&records_payload)
            )
            .is_ok());
            let meeting_body = serde_json::json!({
                "meetingBodyName": "City Council",
                "meetingBodyType": "legislative",
                "meetingBodyStatutoryBasis": "City Charter Section 2.1",
                "meetingBodyCadence": "Third Wednesday",
                "meetingBodyDefaultNoticeDays": "3",
                "meetingBodyQuorumRule": "majority of seated members"
            });
            let records_error = match city_work_action_authorized(
                "create-meeting-body".to_string(),
                Some(meeting_body.clone()),
            ) {
                Ok(_) => panic!("records staff cannot create meeting body"),
                Err(error) => error,
            };
            assert!(records_error.contains("not allowed"));
            auth::auth_action("sign-out", None).expect("records staff signed out");

            sign_in_as_user("casey@example.gov", "clerk passcode 123");
            assert!(require_role_for_city_work(
                &auth::access_state().unwrap(),
                "create-meeting-body",
                Some(&meeting_body)
            )
            .is_ok());
            let clerk_error = match city_work_action_authorized(
                "create-records-request".to_string(),
                Some(records_payload),
            ) {
                Ok(_) => panic!("clerk cannot create records request"),
                Err(error) => error,
            };
            assert!(clerk_error.contains("not allowed"));
            auth::auth_action("sign-out", None).expect("clerk signed out");

            sign_in_as_user("jordan@example.gov", "code passcode 123");
            let code_payload = serde_json::json!({
                "title": "Noise Ordinance",
                "citation": "CMC 8.12",
                "body": "Quiet hours begin at 10 PM.",
                "importedBy": "Deputy Clerk"
            });
            assert!(require_role_for_city_work(
                &auth::access_state().unwrap(),
                "import-code-source",
                Some(&code_payload)
            )
            .is_ok());
            let code_error = match city_work_action_authorized(
                "create-records-request".to_string(),
                Some(serde_json::json!({
                    "requester": "Blake Chen",
                    "summary": "Permit logs",
                    "deadline": "2026-07-12"
                })),
            ) {
                Ok(_) => panic!("code staff cannot create records request"),
                Err(error) => error,
            };
            assert!(code_error.contains("not allowed"));
        });
    }

    #[test]
    fn only_local_admin_can_load_demo_town() {
        with_clean_first_run_state(|_| {
            create_first_admin();
            sign_in_as_first_admin();
            create_staff_user(
                "Riley Records",
                "riley@example.gov",
                "records-staff",
                "records passcode 123",
            );
            auth::auth_action("sign-out", None).expect("admin signed out");
            sign_in_as_user("riley@example.gov", "records passcode 123");

            let error = city_work_action_authorized("load-demo-town".to_string(), None)
                .err()
                .expect("records staff must not load a demo town");
            assert!(error.contains("Townlight admin"));
            assert!(workflows::city_work_state()
                .expect("city state remains readable")
                .demo_fixture
                .is_none());
        });
    }

    #[test]
    fn disabled_modules_block_owned_city_work_actions() {
        with_clean_first_run_state(|_| {
            create_first_admin();
            sign_in_as_first_admin();
            module_action("disable-module".to_string(), "townlight_code".to_string())
                .expect("townlight_code disabled");

            let code_payload = serde_json::json!({
                "title": "Backyard Chicken Rules",
                "citation": "CMC 6.16.040",
                "body": "Backyard chickens are allowed with a coop permit."
            });
            let result =
                city_work_action_authorized("import-code-source".to_string(), Some(code_payload));

            assert!(result.is_err());
            assert!(result
                .err()
                .expect("disabled module error")
                .contains("Townlight Code is not enabled"));
        });
    }

    #[test]
    fn townlight_access_only_profile_can_still_reach_search_city_knowledge() {
        with_clean_first_run_state(|_| {
            create_first_admin();
            sign_in_as_first_admin();
            for m in ["townlight_code", "townlight_notice", "townlight_meetings", "townlight_records"] {
                module_action("disable-module".to_string(), m.to_string()).unwrap();
            }
            require_enabled_city_modules("search-city-knowledge", None)
                .expect("search-city-knowledge should remain enabled");
            let err = city_work_action_authorized(
                "search-city-knowledge".into(),
                Some(serde_json::json!({"query": "water"})),
            )
            .err()
            .unwrap();
            assert!(err.contains("retired"));
        });
    }

    #[test]
    fn disabled_modules_are_filtered_from_cross_module_search() {
        with_clean_first_run_state(|_| {
            create_first_admin();
            sign_in_as_first_admin();
            module_action("disable-module".to_string(), "townlight_code".to_string()).unwrap();
            let mut result = CityWorkActionResult {
                accepted: true,
                action: "search-city-knowledge".into(),
                status: "Saved".into(),
                message: String::new(),
                next_action: String::new(),
                state: CityWorkState::default(),
                search_results: vec![workflows::SearchResult {
                    module_id: "townlight_code".into(),
                    record_id: "synthetic".into(),
                    title: "Synthetic".into(),
                    snippet: String::new(),
                    citation: String::new(),
                    status: "draft".into(),
                }],
                next_steps: vec![],
                disclaimer: String::new(),
            };
            filter_search_results_for_enabled_modules(&mut result).unwrap();
            assert!(result.search_results.is_empty());
        });
    }

    #[test]
    fn first_run_setup_actions_require_admin_after_first_admin_exists() {
        with_clean_first_run_state(|_| {
            create_first_admin();

            let city_payload = serde_json::json!({
                "cityName": "Brookfield",
                "state": "CO",
                "timeZone": "America/Denver",
                "recordsContact": "records@example.gov",
                "clerkContact": "clerk@example.gov"
            });
            let signed_out_profile_result = first_run_action_authorized(
                "create-city-profile".to_string(),
                Some("city-profile".to_string()),
                Some(city_payload),
            );
            assert!(signed_out_profile_result.is_err());
            assert!(signed_out_profile_result
                .err()
                .expect("first-run profile auth error")
                .contains("Sign in as the Townlight admin"));

            let signed_out_model_result = first_run_action_authorized(
                "download-model".to_string(),
                Some("model".to_string()),
                None,
            );
            assert!(signed_out_model_result.is_err());
            assert!(signed_out_model_result
                .err()
                .expect("first-run model auth error")
                .contains("Sign in as the Townlight admin"));

            let signed_out_result = first_run_action_authorized("backup".to_string(), None, None);
            let signed_out_deferral = first_run_action_authorized(
                "defer-model".to_string(),
                Some("model".to_string()),
                None,
            );
            assert!(signed_out_deferral.is_err());

            assert!(signed_out_result.is_err());
            assert!(signed_out_result
                .err()
                .expect("first-run lifecycle auth error")
                .contains("Sign in as the Townlight admin"));

            sign_in_as_first_admin();
            let signed_in_result = first_run_action_authorized("backup".to_string(), None, None)
                .expect("backup allowed");
            assert!(signed_in_result.accepted);
            assert_eq!(signed_in_result.action, "backup");
        });
    }

    #[test]
    fn first_run_setup_actions_can_bootstrap_before_admin_exists() {
        with_clean_first_run_state(|_| {
            first_run_action_authorized(
                "choose-location".to_string(),
                Some("locations".to_string()),
                None,
            )
            .expect("locations can bootstrap before admin exists");
            let module_payload = serde_json::json!({ "profileId": "city-core" });
            first_run_action_authorized(
                "select-modules".to_string(),
                Some("modules".to_string()),
                Some(module_payload),
            )
            .expect("module selection can bootstrap before admin exists");
            let city_payload = serde_json::json!({
                "cityName": "Brookfield",
                "state": "CO",
                "timeZone": "America/Denver",
                "recordsContact": "records@example.gov",
                "clerkContact": "clerk@example.gov"
            });
            let city_result = first_run_action_authorized(
                "create-city-profile".to_string(),
                Some("city-profile".to_string()),
                Some(city_payload),
            )
            .expect("city profile can bootstrap before admin exists");
            assert!(city_result.accepted);

            let admin_payload = serde_json::json!({
                "adminName": "Alex Clerk",
                "adminEmail": "alex@example.gov",
                "adminPasscode": "correct horse battery staple"
            });
            let admin_result = first_run_action_authorized(
                "create-admin".to_string(),
                Some("first-admin".to_string()),
                Some(admin_payload),
            )
            .expect("first admin can bootstrap local access");
            assert!(admin_result.accepted);
            assert!(admin_result.next_action.contains("Sign in"));
        });
    }

    #[test]
    fn app_state_reports_first_run_setup_contract() {
        with_clean_first_run_state(|_| {
            let state = app_state().expect("app state builds");
            assert_eq!(state.first_run.status, "Needs setup");
            assert_eq!(
                state.first_run.current_step_id.as_deref(),
                Some("locations")
            );
            assert!(state.first_run.local_only);
        });
    }

    #[test]
    fn app_state_reports_saved_city_profile_and_first_admin() {
        with_clean_first_run_state(|_| {
            create_first_admin();
            sign_in_as_first_admin();

            let state = app_state().expect("app state");

            let profile = state.city_profile.expect("city profile");
            assert_eq!(profile.city_name, "Brookfield");
            assert_eq!(profile.records_contact, "records@example.gov");
            assert_eq!(state.users.len(), 1);
            assert_eq!(state.users[0].email, "alex@example.gov");
            assert_eq!(state.users[0].role, "local-admin");
        });
    }

    #[test]
    fn app_state_reports_module_selection_profile() {
        with_clean_first_run_state(|_| {
            module_registry::persist_profile_selection("city-core")
                .expect("module selection persists");
            let state = app_state().expect("app state");
            assert_eq!(state.module_selection.profile_id, "city-core");
            assert_eq!(state.module_selection.installed_module_ids.len(), 6);
            assert!(state
                .module_profiles
                .iter()
                .any(|profile| profile.id == "city-core" && profile.selected));
            assert!(state
                .modules
                .iter()
                .filter(|module| module.installed)
                .all(|module| !module.proof_required.is_empty()));
        });
    }

    #[test]
    fn app_state_reports_local_city_work_state() {
        with_clean_first_run_state(|_| {
            let state = app_state().expect("app state builds");
            assert_eq!(state.city_work.meetings.len(), 0);
            assert_eq!(state.city_work.records_requests.len(), 0);
            assert_eq!(state.city_work.code_sources.len(), 0);
        });
    }

    #[test]
    fn unauthenticated_city_work_state_is_public_projection() {
        with_clean_first_run_state(|_| {
            create_first_admin();
            let mut raw = serde_json::to_value(CityWorkState::default()).unwrap();
            raw["records_requests"] = serde_json::json!([{"id":"old","requester":"Synthetic","summary":"Private","deadline":"2026-11-01","status":"received","requester_contact":"private@example.invalid","created_at_unix_seconds":1}]);
            let path = local_paths::data_root().join("workflows/city-work.json");
            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
            let original = serde_json::to_vec(&raw).unwrap();
            std::fs::write(&path, &original).unwrap();
            let public = get_city_work_state().expect("...");
            assert!(public.records_requests.is_empty());
            let state = app_state().expect("...");
            assert!(state.users.is_empty());
            assert!(state.health.iter().all(|item| item.admin_detail.is_empty()));
            sign_in_as_first_admin();
            let admin = get_city_work_state().expect("...");
            assert_eq!(admin.records_requests.len(), 1);
            assert_eq!(
                admin.records_requests[0].requester_contact,
                "private@example.invalid"
            );
            assert_eq!(std::fs::read(&path).unwrap(), original);
        });
    }

    #[test]
    fn public_city_work_actions_require_first_run_owner() {
        with_clean_first_run_state(|_| {
            let public_payload = serde_json::json!({
                "requester": "Riley Resident",
                "requesterContact": "riley@example.org",
                "summary": "Please provide the current council packet."
            });
            let result = city_work_action_authorized(
                "submit-public-records-request".to_string(),
                Some(public_payload),
            );

            assert!(result.is_err());
            assert!(result
                .err()
                .expect("setup error")
                .contains("Finish first-run setup"));
        });
    }

    #[test]
    fn public_city_work_actions_do_not_require_admin_session() {
        with_clean_first_run_state(|_| {
            create_first_admin();
            for action in [
                "submit-public-records-request",
                "lookup-public-records-request",
            ] {
                let err = city_work_action_authorized(action.to_string(), None)
                    .err()
                    .unwrap_or_else(|| panic!("expected retired error for {action}"));
                assert!(format!("{err:?}").contains("retired"), "{action}: {err:?}");
            }
            let err = city_work_action_authorized("create-records-request".to_string(), None)
                .err()
                .unwrap_or_else(|| panic!("expected sign-in error"));
            assert!(
                err.contains("Sign in with a staff or Townlight admin account"),
                "{err:?}"
            );
        });
    }
}

permitting_client.rs

//! Fixed native transport. No permit rules or caller-supplied URLs/paths.
use crate::domain_client::{send, DomainError};
use reqwest::Method;
use serde::Deserialize;
use serde_json::Value;
use uuid::Uuid;

#[derive(Debug, Deserialize)]
#[serde(tag = "operation", rename_all = "kebab-case", deny_unknown_fields)]
pub enum PermittingRequest {
    Requirements { offset: u32 },
    Requirement { id: Uuid },
    ImportRequirements { payload: Value },
    Cases { offset: u32 },
    Case { id: Uuid },
    CreateCase { payload: Value },
    ReviseCase { id: Uuid, payload: Value },
    History { id: Uuid },
    CreatePacket { id: Uuid, payload: Value },
    ExportPacket { id: Uuid, expected_sha256: String },
    SearchConnections { payload: Value },
    Pathway { payload: Value },
    AttachRecords { request_id: Uuid, payload: Value },
}

pub fn route(request: PermittingRequest) -> (Method, String, Option<Value>) {
    use PermittingRequest::*;
    let (method, path, body) = match request {
        Requirements { offset } => (Method::GET, format!("/requirements?offset={offset}"), None),
        Requirement { id } => (Method::GET, format!("/requirements/{id}"), None),
        ImportRequirements { payload } => (Method::POST, "/requirements".into(), Some(payload)),
        Cases { offset } => (Method::GET, format!("/cases?offset={offset}"), None),
        Case { id } => (Method::GET, format!("/cases/{id}"), None),
        CreateCase { payload } => (Method::POST, "/cases".into(), Some(payload)),
        ReviseCase { id, payload } => (
            Method::POST,
            format!("/cases/{id}/revisions"),
            Some(payload),
        ),
        History { id } => (Method::GET, format!("/cases/{id}/history"), None),
        CreatePacket { id, payload } => {
            (Method::POST, format!("/cases/{id}/packets"), Some(payload))
        }
        ExportPacket { id, .. } => (Method::GET, format!("/packets/{id}"), None),
        SearchConnections { payload } => {
            (Method::POST, "/connections/search".into(), Some(payload))
        }
        Pathway { payload } => (Method::POST, "/pathway".into(), Some(payload)),
        AttachRecords {
            request_id,
            payload,
        } => {
            return (
                Method::POST,
                format!("/desktop/v1/requests/{request_id}/permitting-sources"),
                Some(payload),
            )
        }
    };
    (method, format!("/desktop/v1/permitting{path}"), body)
}

pub async fn execute(request: PermittingRequest, proof: String) -> Result<Value, DomainError> {
    let export = match &request {
        PermittingRequest::ExportPacket {
            id,
            expected_sha256,
        } => Some((*id, expected_sha256.clone())),
        _ => None,
    };
    let (method, path, body) = route(request);
    let result = send(method, path, body, &proof).await?;
    if let Some((id, expected)) = export {
        let content = result
            .get("text")
            .and_then(Value::as_str)
            .ok_or_else(DomainError::denied)?;
        let digest = result
            .get("sha256")
            .and_then(Value::as_str)
            .ok_or_else(DomainError::denied)?;
        if digest != expected {
            return Err(DomainError::denied());
        }
        let path = crate::domain_files::write_permitting_file(
            &format!("permitting-{id}-{}.json", Uuid::new_v4()),
            content.as_bytes(),
            digest,
        )
        .map_err(|_| DomainError::denied())?;
        return Ok(serde_json::json!({"path": path, "sha256": digest}));
    }
    Ok(result)
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn permitting_transport_is_fixed_and_does_not_accept_a_url_or_unknown_operation() {
        assert!(serde_json::from_value::<PermittingRequest>(
            serde_json::json!({"operation":"approve-permit"})
        )
        .is_err());
        assert!(serde_json::from_value::<PermittingRequest>(
            serde_json::json!({"operation":"cases","offset":0,"url":"https://example.com"})
        )
        .is_err());
        let (_, path, _) = route(PermittingRequest::Cases { offset: 10 });
        assert_eq!(path, "/desktop/v1/permitting/cases?offset=10");
    }
}

planning_client.rs

//! Fixed native transport and confined exact-byte exports. No business rules.
use crate::domain_client::{send, DomainError};
use reqwest::Method;
use serde::Deserialize;
use serde_json::Value;
use uuid::Uuid;

#[derive(Debug, Deserialize)]
#[serde(tag = "operation", rename_all = "kebab-case", deny_unknown_fields)]
pub enum PlanningRequest {
    Initiatives {
        offset: u32,
    },
    Amendments {
        offset: u32,
    },
    MunicipalHistory {
        id: Uuid,
    },
    CreateInitiative {
        payload: Value,
    },
    ReviseInitiative {
        id: Uuid,
        payload: Value,
    },
    CreateAmendment {
        payload: Value,
    },
    ReviseAmendment {
        id: Uuid,
        payload: Value,
    },

    SearchPolicies {
        payload: Value,
    },
    Sources {
        offset: u32,
    },
    SourceHistory {
        plan_id: String,
        #[serde(default)]
        offset: u32,
    },
    Revision {
        id: Uuid,
    },
    Original {
        id: Uuid,
        document_id: String,
    },
    PreviewImport {
        payload: Value,
    },
    ImportBundle {
        payload: Value,
    },
    Analyses {
        offset: u32,
    },
    Analysis {
        id: Uuid,
    },
    AnalysisHistory {
        id: Uuid,
        #[serde(default)]
        offset: u32,
    },
    CreateAnalysis {
        payload: Value,
    },
    ReviseAnalysis {
        id: Uuid,
        payload: Value,
    },
    PreviewSnapshot {
        id: Uuid,
        payload: Value,
    },
    CreateSnapshot {
        id: Uuid,
        payload: Value,
    },
    Snapshots {
        offset: u32,
    },
    Snapshot {
        id: Uuid,
    },
    ExportSnapshot {
        id: Uuid,
        expected_sha256: String,
    },
    AccessAdvice {
        id: Uuid,
    },
    SearchConnections {
        payload: Value,
    },
    PlanExplore {
        payload: Value,
    },
}

pub fn route(
    request: PlanningRequest,
    public: bool,
) -> Result<(Method, String, Option<Value>), DomainError> {
    use PlanningRequest::*;
    if public
        && !matches!(
            &request,
            SearchPolicies { .. }
                | PlanExplore { .. }
                | Snapshots { .. }
                | Snapshot { .. }
                | Original { .. }
                | ExportSnapshot { .. }
        )
    {
        return Err(DomainError::denied());
    }
    let result = match request {
        Initiatives { offset } => (
            Method::GET,
            format!("/progress/targets?offset={offset}"),
            None,
        ),
        Amendments { offset } => (
            Method::GET,
            format!("/amendments/history?offset={offset}"),
            None,
        ),
        MunicipalHistory { id } => (Method::GET, format!("/municipal-work/{id}/history"), None),
        CreateInitiative { payload } => (Method::POST, "/initiatives".into(), Some(payload)),
        ReviseInitiative { id, payload } => (
            Method::POST,
            format!("/initiatives/{id}/revisions"),
            Some(payload),
        ),
        CreateAmendment { payload } => (Method::POST, "/amendments".into(), Some(payload)),
        ReviseAmendment { id, payload } => (
            Method::POST,
            format!("/amendments/{id}/revisions"),
            Some(payload),
        ),

        SearchPolicies { payload } => (Method::POST, "/policies/search".into(), Some(payload)),
        Sources { offset } => (Method::GET, format!("/sources?offset={offset}"), None),
        SourceHistory { plan_id, offset } => (
            Method::GET,
            format!(
                "/sources/{}/history?offset={offset}",
                crate::code_client::encode_segment(&plan_id)?
            ),
            None,
        ),
        Revision { id } => (Method::GET, format!("/revisions/{id}"), None),
        Original { id, document_id } => (
            Method::GET,
            format!(
                "/revisions/{id}/documents/{}",
                crate::code_client::encode_segment(&document_id)?
            ),
            None,
        ),
        PreviewImport { payload } => (Method::POST, "/imports/preview".into(), Some(payload)),
        ImportBundle { payload } => (Method::POST, "/imports".into(), Some(payload)),
        Analyses { offset } => (Method::GET, format!("/analyses?offset={offset}"), None),
        Analysis { id } => (Method::GET, format!("/analyses/{id}"), None),
        AnalysisHistory { id, offset } => (
            Method::GET,
            format!("/analyses/{id}/history?offset={offset}"),
            None,
        ),
        CreateAnalysis { payload } => (Method::POST, "/analyses".into(), Some(payload)),
        ReviseAnalysis { id, payload } => (
            Method::POST,
            format!("/analyses/{id}/revisions"),
            Some(payload),
        ),
        PreviewSnapshot { id, payload } => (
            Method::POST,
            format!("/analyses/{id}/snapshot-preview"),
            Some(payload),
        ),
        CreateSnapshot { id, payload } => (
            Method::POST,
            format!("/analyses/{id}/snapshots"),
            Some(payload),
        ),
        Snapshots { offset } => (Method::GET, format!("/snapshots?offset={offset}"), None),
        Snapshot { id } | ExportSnapshot { id, .. } => {
            (Method::GET, format!("/snapshots/{id}"), None)
        }
        AccessAdvice { id } => (
            Method::POST,
            format!("/snapshots/{id}/access-advice"),
            Some(serde_json::json!({})),
        ),
        SearchConnections { payload } => {
            (Method::POST, "/connections/search".into(), Some(payload))
        }
        PlanExplore { payload } => (Method::POST, "/plan-explore".into(), Some(payload)),
    };
    let prefix = if public {
        "/townlight_planning"
    } else {
        "/desktop/v1/planning"
    };
    Ok((result.0, format!("{prefix}{}", result.1), result.2))
}

pub async fn execute(
    request: PlanningRequest,
    proof: String,
    public: bool,
) -> Result<Value, DomainError> {
    let export = match &request {
        PlanningRequest::ExportSnapshot {
            id,
            expected_sha256,
        } => Some((*id, expected_sha256.clone())),
        _ => None,
    };
    let original = matches!(&request, PlanningRequest::Original { .. });
    let (method, path, body) = route(request, public)?;
    let result = send(method, path, body, &proof).await?;
    if let Some((id, expected)) = export {
        let content = result
            .get("text")
            .and_then(Value::as_str)
            .ok_or_else(DomainError::denied)?;
        let digest = result
            .get("sha256")
            .and_then(Value::as_str)
            .ok_or_else(DomainError::denied)?;
        if digest != expected {
            return Err(DomainError::denied());
        }
        let path = crate::domain_files::write_planning_file(
            &format!("planning-{id}-{}.json", Uuid::new_v4()),
            content.as_bytes(),
            digest,
        )
        .map_err(|_| DomainError::denied())?;
        return Ok(serde_json::json!({"path": path, "sha256": digest}));
    }
    if original {
        use base64::Engine;
        let encoded = result
            .get("content_base64")
            .and_then(Value::as_str)
            .ok_or_else(DomainError::denied)?;
        let bytes = base64::engine::general_purpose::STANDARD
            .decode(encoded)
            .map_err(|_| DomainError::denied())?;
        let filename = result
            .get("filename")
            .and_then(Value::as_str)
            .ok_or_else(DomainError::denied)?;
        let digest = result
            .get("sha256")
            .and_then(Value::as_str)
            .ok_or_else(DomainError::denied)?;
        let path = crate::domain_files::write_planning_file(
            &format!("{}-{filename}", Uuid::new_v4()),
            &bytes,
            digest,
        )
        .map_err(|_| DomainError::denied())?;
        return Ok(serde_json::json!({"path": path, "sha256": digest}));
    }
    Ok(result)
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn public_transport_rejects_staff_reads_and_writes() {
        for operation in ["sources", "analyses", "initiatives", "amendments"] {
            let request =
                serde_json::from_value(serde_json::json!({"operation":operation,"offset":0}))
                    .unwrap();
            assert!(route(request, true).is_err());
        }
        assert!(route(
            PlanningRequest::CreateAnalysis {
                payload: serde_json::json!({})
            },
            true
        )
        .is_err());
        let (_, path, _) = route(PlanningRequest::Snapshots { offset: 50 }, true).unwrap();
        assert_eq!(path, "/townlight_planning/snapshots?offset=50");
        assert!(serde_json::from_value::<PlanningRequest>(
            serde_json::json!({"operation":"snapshot","id":"../../private"})
        )
        .is_err());
        assert!(serde_json::from_value::<PlanningRequest>(
            serde_json::json!({"operation":"snapshots","offset":0,"actor":"forged"})
        )
        .is_err());
    }

    #[test]
    fn municipal_routes_are_fixed_and_staff_only() {
        for (operation, endpoint) in [
            ("create-initiative", "/initiatives"),
            ("create-amendment", "/amendments"),
        ] {
            let value = serde_json::json!({"operation": operation, "payload": {"title":"fixture"}});
            let request = serde_json::from_value(value.clone()).unwrap();
            assert!(route(request, true).is_err());
            let (method, path, body) =
                route(serde_json::from_value(value).unwrap(), false).unwrap();
            assert_eq!(method, Method::POST);
            assert_eq!(path, format!("/desktop/v1/planning{endpoint}"));
            assert_eq!(body.unwrap()["title"], "fixture");
        }
        for operation in ["revise-initiative", "revise-amendment", "municipal-history"] {
            assert!(
                serde_json::from_value::<PlanningRequest>(serde_json::json!({
                    "operation": operation, "id":"../../private", "payload":{}
                }))
                .is_err()
            );
        }
    }
}

zoning_client.rs

use crate::domain_client::{send, DomainError};
use reqwest::Method;
use serde::Deserialize;
use serde_json::Value;

#[derive(Debug, Deserialize)]
#[serde(tag = "operation", rename_all = "kebab-case", deny_unknown_fields)]
pub enum ZoningRequest {
    Lookup { payload: Value },
    Question { payload: Value },
    RegisterRule { payload: Value },
    ImportBundle { payload: Value },
    Workspace {},
    Review { id: uuid::Uuid, payload: Value },
    Outline { payload: Value },
    CodeContext { payload: Value },
    SearchMeetings { payload: Value },
    LinkMeeting { id: uuid::Uuid, payload: Value },
    History { id: uuid::Uuid },
    Snapshot { id: uuid::Uuid, payload: Value },
    Standards { payload: Value },
}

pub async fn execute(
    request: ZoningRequest,
    proof: String,
    public: bool,
) -> Result<Value, DomainError> {
    let (method, path, body): (Method, String, Option<Value>) = match request {
        ZoningRequest::Lookup { payload } => {
            (Method::POST, "/parcels/lookup".into(), Some(payload))
        }
        ZoningRequest::Question { payload } => (Method::POST, "/questions".into(), Some(payload)),
        ZoningRequest::RegisterRule { payload } => {
            if public {
                return Err(DomainError::denied());
            }
            (Method::POST, "/rules".into(), Some(payload))
        }
        ZoningRequest::ImportBundle { payload } => {
            if public {
                return Err(DomainError::denied());
            }
            (Method::POST, "/imports".into(), Some(payload))
        }
        ZoningRequest::Workspace {} => {
            if public {
                return Err(DomainError::denied());
            }
            (Method::GET, "/workspace".into(), None)
        }
        ZoningRequest::Review { id, payload } => {
            if public {
                return Err(DomainError::denied());
            }
            (
                Method::POST,
                format!("/questions/{id}/reviews"),
                Some(payload),
            )
        }
        ZoningRequest::Outline { payload } => {
            if public {
                return Err(DomainError::denied());
            }
            (Method::POST, "/outlines".into(), Some(payload))
        }
        ZoningRequest::CodeContext { payload } => {
            if public {
                return Err(DomainError::denied());
            }
            (Method::POST, "/code-context".into(), Some(payload))
        }
        ZoningRequest::SearchMeetings { payload } => {
            if public {
                return Err(DomainError::denied());
            }
            (Method::POST, "/meetings/search".into(), Some(payload))
        }
        ZoningRequest::LinkMeeting { id, payload } => {
            if public {
                return Err(DomainError::denied());
            }
            (
                Method::POST,
                format!("/questions/{id}/meetings"),
                Some(payload),
            )
        }
        ZoningRequest::History { id } => {
            if public {
                return Err(DomainError::denied());
            }
            (Method::GET, format!("/questions/{id}/history"), None)
        }
        ZoningRequest::Standards { payload } => {
            if public {
                return Err(DomainError::denied());
            }
            (
                Method::POST,
                "/standards/process-explanation".into(),
                Some(payload),
            )
        }
        ZoningRequest::Snapshot { id, payload } => {
            if public {
                return Err(DomainError::denied());
            }
            (
                Method::POST,
                format!("/questions/{id}/snapshots"),
                Some(payload),
            )
        }
    };
    let prefix = if public {
        "/townlight_zoning"
    } else {
        "/desktop/v1/zoning"
    };
    let full_path = format!("{prefix}{path}");
    send(method, full_path, body, &proof).await
}

Windows staff workspaces

municipal_staff_request invokes the local /desktop/v1/municipal-staff/ routes for Boards, Contracts, and Procurement with the signed-in staff proof. Boards uses the Meetings permission. The award and draft-contract operations require the local administrator. See the workflow guide and exact native operation mapping.