//! Schema emission — generates JSON schema artifacts.
//!
//! Enabled via `--features schema`. The `emit-schemas` binary writes
//! schema files to `artifacts/schemas/`.

#[cfg(feature = "schema")]
pub fn emit_all_schemas(output_dir: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
    use schemars::schema_for;
    use serde_json::{Map, Value};
    use std::fs;

    fs::create_dir_all(output_dir)?;

    fn write_pretty_json(
        path: std::path::PathBuf,
        value: &Value,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let mut body = serde_json::to_string_pretty(value)?;
        body.push('\n');
        fs::write(path, body)?;
        Ok(())
    }

    fn unique_contract_values(groups: &[&[&'static str]]) -> Vec<&'static str> {
        let mut values = Vec::new();
        for group in groups {
            for value in *group {
                if !values.contains(value) {
                    values.push(*value);
                }
            }
        }
        values
    }

    // Version
    let version_schema = serde_json::json!({
        "contract_version": crate::version::ContractVersion::CURRENT.to_string(),
    });
    write_pretty_json(output_dir.join("version.json"), &version_schema)?;

    // Wire types (contracts-owned types only — types embedding core types
    // without JsonSchema use serde for serialization but not for schema generation)
    let wire_types = serde_json::json!({
        "WireUsage": schema_for!(crate::wire::WireUsage),
        "ContractVersion": schema_for!(crate::version::ContractVersion),
        "WireRunResult": schema_for!(crate::wire::WireRunResult),
        "HelpExecutionMode": schema_for!(crate::wire::HelpExecutionMode),
        "HelpRequest": schema_for!(crate::wire::HelpRequest),
        "HelpResponse": schema_for!(crate::wire::HelpResponse),
        "McpLiveOpStatus": schema_for!(crate::wire::McpLiveOpStatus),
        "McpLiveOperation": schema_for!(crate::wire::McpLiveOperation),
        "McpLiveOpResponse": schema_for!(crate::wire::McpLiveOpResponse),
        "WireTrustedPeerSpec": schema_for!(crate::wire::WireTrustedPeerSpec),
        "MobPeerTarget": schema_for!(crate::wire::MobPeerTarget),
        "WireMobBackendKind": schema_for!(crate::wire::WireMobBackendKind),
        "WireMobRuntimeMode": schema_for!(crate::wire::WireMobRuntimeMode),
        "WireRuntimeBinding": schema_for!(crate::wire::WireRuntimeBinding),
        "WireMemberLaunchMode": schema_for!(crate::wire::WireMemberLaunchMode),
        "WireForkContext": schema_for!(crate::wire::WireForkContext),
        "WireToolAccessPolicy": schema_for!(crate::wire::WireToolAccessPolicy),
        "WireBudgetSplitPolicy": schema_for!(crate::wire::WireBudgetSplitPolicy),
        "WireToolFilter": schema_for!(crate::wire::WireToolFilter),
        "WireMobToolConfig": schema_for!(crate::wire::WireMobToolConfig),
        "WireMobProfile": schema_for!(crate::wire::WireMobProfile),
        "MobDefinitionInput": schema_for!(crate::wire::MobDefinitionInput),
        "MobCreateResult": schema_for!(crate::wire::MobCreateResult),
        "MobListResult": schema_for!(crate::wire::MobListResult),
        "MobStatusResult": schema_for!(crate::wire::MobStatusResult),
        "MobLifecycleResult": schema_for!(crate::wire::MobLifecycleResult),
        "MobSpawnResult": schema_for!(crate::wire::MobSpawnResult),
        "MobSpawnManyResult": schema_for!(crate::wire::MobSpawnManyResult),
        "MobRetireResult": schema_for!(crate::wire::MobRetireResult),
        "MobRespawnResult": schema_for!(crate::wire::MobRespawnResult),
        "MobMembersResult": schema_for!(crate::wire::MobMembersResult),
        "MobEventsResult": schema_for!(crate::wire::MobEventsResult),
        "MobMemberSendParams": schema_for!(crate::wire::MobMemberSendParams),
        "MobMemberSendResult": schema_for!(crate::wire::MobMemberSendResult),
        "MobIngressInteractionParams": schema_for!(crate::wire::MobIngressInteractionParams),
        "MobIngressInteractionResult": schema_for!(crate::wire::MobIngressInteractionResult),
        "MobAppendSystemContextResult": schema_for!(crate::wire::MobAppendSystemContextResult),
        "MobFlowsResult": schema_for!(crate::wire::MobFlowsResult),
        "MobFlowRunResult": schema_for!(crate::wire::MobFlowRunResult),
        "MobFlowStatusResult": schema_for!(crate::wire::MobFlowStatusResult),
        "MobFlowCancelResult": schema_for!(crate::wire::MobFlowCancelResult),
        "MobHelperResult": schema_for!(crate::wire::MobHelperResult),
        "MobForceCancelResult": schema_for!(crate::wire::MobForceCancelResult),
        "MobMemberStatusResult": schema_for!(crate::wire::MobMemberStatusResult),
        "MobSnapshotResult": schema_for!(crate::wire::MobSnapshotResult),
        "MobDestroyResult": schema_for!(crate::wire::MobDestroyResult),
        "MobRotateSupervisorResult": schema_for!(crate::wire::MobRotateSupervisorResult),
        "SupervisorRotationReportWire": schema_for!(crate::wire::SupervisorRotationReportWire),
        "MobWaitMembersResult": schema_for!(crate::wire::MobWaitMembersResult),
        "MobEnsureMemberResult": schema_for!(crate::wire::MobEnsureMemberResult),
        "MobReconcileResult": schema_for!(crate::wire::MobReconcileResult),
        "MobListMembersMatchingResult": schema_for!(crate::wire::MobListMembersMatchingResult),
        "MobSubmitWorkResult": schema_for!(crate::wire::MobSubmitWorkResult),
        "MobCancelWorkResult": schema_for!(crate::wire::MobCancelWorkResult),
        "MobCancelAllWorkResult": schema_for!(crate::wire::MobCancelAllWorkResult),
        "MobProfileLookupResult": schema_for!(crate::wire::MobProfileLookupResult),
        "MobProfileListResult": schema_for!(crate::wire::MobProfileListResult),
        "MobProfileDeleteResult": schema_for!(crate::wire::MobProfileDeleteResult),
        "MobStreamOpenResult": schema_for!(crate::wire::MobStreamOpenResult),
        "MobStreamCloseResult": schema_for!(crate::wire::MobStreamCloseResult),
        "WireHandlingMode": schema_for!(crate::wire::WireHandlingMode),
        "WireRenderClass": schema_for!(crate::wire::WireRenderClass),
        "WireRenderSalience": schema_for!(crate::wire::WireRenderSalience),
        "WireRenderMetadata": schema_for!(crate::wire::WireRenderMetadata),
        "MobWireResult": schema_for!(crate::wire::MobWireResult),
        "MobWireMembersBatchEdge": schema_for!(crate::wire::MobWireMembersBatchEdge),
        "MobWireMembersBatchParams": schema_for!(crate::wire::MobWireMembersBatchParams),
        "MobWireMembersBatchResult": schema_for!(crate::wire::MobWireMembersBatchResult),
        "MobUnwireResult": schema_for!(crate::wire::MobUnwireResult),
        "WireRuntimeState": schema_for!(crate::wire::WireRuntimeState),
        "RuntimeStateResult": schema_for!(crate::wire::RuntimeStateResult),
        "PeerResponseTerminalStatusWire": schema_for!(crate::wire::PeerResponseTerminalStatusWire),
        "SessionExternalEventEnvelope": schema_for!(crate::wire::SessionExternalEventEnvelope),
        "RealtimeTurningMode": schema_for!(crate::wire::RealtimeTurningMode),
        "RealtimeInputKind": schema_for!(crate::wire::RealtimeInputKind),
        "RealtimeOutputKind": schema_for!(crate::wire::RealtimeOutputKind),
        "RealtimeCapabilities": schema_for!(crate::wire::RealtimeCapabilities),
        "RealtimeTextChunk": schema_for!(crate::wire::RealtimeTextChunk),
        "RealtimeAudioChunk": schema_for!(crate::wire::RealtimeAudioChunk),
        "RealtimeVideoChunk": schema_for!(crate::wire::RealtimeVideoChunk),
        "RealtimeInputChunk": schema_for!(crate::wire::RealtimeInputChunk),
        "LiveOpenParams": schema_for!(crate::wire::LiveOpenParams),
        "LiveOpenTransport": schema_for!(crate::wire::LiveOpenTransport),
        "LiveOpenResult": schema_for!(crate::wire::LiveOpenResult),
        "LiveWebrtcAnswerParams": schema_for!(crate::wire::LiveWebrtcAnswerParams),
        "LiveWebrtcAnswerResult": schema_for!(crate::wire::LiveWebrtcAnswerResult),
        "LiveChannelParams": schema_for!(crate::wire::LiveChannelParams),
        "LiveStatusResult": schema_for!(crate::wire::LiveStatusResult),
        "LiveSendInputParams": schema_for!(crate::wire::LiveSendInputParams),
        "LiveInputChunkWire": schema_for!(crate::wire::LiveInputChunkWire),
        "LiveTruncateParams": schema_for!(crate::wire::LiveTruncateParams),
        // G9 (P2): emit `LiveCommitInputParams` and `WireLiveResponseModality`
        // at the top level so SDK codegen produces typed shapes (param
        // struct + discriminated modality union) for `live/commit_input`
        // instead of falling back to the opaque `LiveChannelParams` shape.
        "LiveCommitInputParams": schema_for!(crate::wire::LiveCommitInputParams),
        "WireLiveResponseModality": schema_for!(crate::wire::WireLiveResponseModality),
        // R4-5 (P3): emit the typed `live/refresh` result so SDK codegen
        // produces a typed shape (TypedDict / interface) carrying both the
        // typed `LiveRefreshStatus` discriminator and the legacy
        // `refresh_enqueued: true` back-compat field, instead of falling
        // back to opaque `Value` / `Any` / `unknown`.
        "LiveRefreshResult": schema_for!(crate::wire::LiveRefreshResult),
        "LiveRefreshStatus": schema_for!(crate::wire::LiveRefreshStatus),
        // CC5/CC6: emit the typed wire mirrors at the top level so SDK
        // codegen produces named typed shapes (TypedDict / interface /
        // discriminated union) instead of inlining them as anonymous `Any`
        // / `unknown` blobs inside `LiveOpenResult`.
        "WireLiveChannelCapabilities": schema_for!(crate::wire::WireLiveChannelCapabilities),
        "WireLiveContinuityMode": schema_for!(crate::wire::WireLiveContinuityMode),
        // G8 (P2): emit `WireLiveTransportBootstrap` at the top level so
        // SDK codegen produces a typed discriminated union (TS) /
        // tagged-variant TypedDict (Python) for `LiveOpenResult.transport`
        // instead of `unknown` / `Any`.
        "WireLiveTransportBootstrap": schema_for!(crate::wire::WireLiveTransportBootstrap),
        // FIX-SDK-OBS: emit `WireLiveAdapterObservation` and its supporting
        // typed wire mirrors so SDK codegen sees the discriminated union of
        // adapter observations (R5-4 identity fields on
        // `assistant_audio_chunk`, R5-9 `command_rejected` typed channel
        // survives error) rather than treating them as opaque blobs.
        "WireLiveAdapterObservation": schema_for!(crate::wire::WireLiveAdapterObservation),
        "WireLiveAdapterStatus": schema_for!(crate::wire::WireLiveAdapterStatus),
        "WireLiveDegradationReason": schema_for!(crate::wire::WireLiveDegradationReason),
        "WireLiveAdapterErrorCode": schema_for!(crate::wire::WireLiveAdapterErrorCode),
        "RuntimeAcceptOutcomeType": schema_for!(crate::wire::RuntimeAcceptOutcomeType),
        "WireInputLifecycleState": schema_for!(crate::wire::WireInputLifecycleState),
        "WireInputStateHistoryEntry": schema_for!(crate::wire::WireInputStateHistoryEntry),
        "WireInputState": schema_for!(crate::wire::WireInputState),
        "RuntimeAcceptResult": schema_for!(crate::wire::RuntimeAcceptResult),
        "WireContentBlock": schema_for!(crate::wire::WireContentBlock),
        "WireContentInput": schema_for!(crate::wire::WireContentInput),
        "WireToolResultContent": schema_for!(crate::wire::WireToolResultContent),
        "WireAssistantBlock": schema_for!(crate::wire::WireAssistantBlock),
        "WireAssistantImageRef": schema_for!(crate::wire::WireAssistantImageRef),
        "WireGenerateImageRequest": schema_for!(crate::wire::WireGenerateImageRequest),
        "WireGenerateImageExecutionPlan": schema_for!(crate::wire::WireGenerateImageExecutionPlan),
        "WireImageGenerationToolResult": schema_for!(crate::wire::WireImageGenerationToolResult),
        "WireImageOperationPhase": schema_for!(crate::wire::WireImageOperationPhase),
        "WireSwitchTurnIntent": schema_for!(crate::wire::WireSwitchTurnIntent),
        "WireSwitchTurnControlResult": schema_for!(crate::wire::WireSwitchTurnControlResult),
        "WireSwitchTurnPhase": schema_for!(crate::wire::WireSwitchTurnPhase),
        "WireModelRoutingApprovalPhase": schema_for!(crate::wire::WireModelRoutingApprovalPhase),
        "WireModelRoutingApprovalRequest": schema_for!(crate::wire::WireModelRoutingApprovalRequest),
        "WireScopedModelOverride": schema_for!(crate::wire::WireScopedModelOverride),
        "WireSessionModelRoutingStatus": schema_for!(crate::wire::WireSessionModelRoutingStatus),
        "WireProviderMeta": schema_for!(crate::wire::WireProviderMeta),
        "WireSessionHistory": schema_for!(crate::wire::WireSessionHistory),
        "SessionForkResult": schema_for!(meerkat_core::SessionForkResult),
        "TranscriptEditRunningBehavior": schema_for!(meerkat_core::TranscriptEditRunningBehavior),
        "WireSessionInfo": schema_for!(crate::wire::WireSessionInfo),
        "WireSessionMessage": schema_for!(crate::wire::WireSessionMessage),
        "WireSessionSummary": schema_for!(crate::wire::WireSessionSummary),
        "WireStopReason": schema_for!(crate::wire::WireStopReason),
        "WireToolCall": schema_for!(crate::wire::WireToolCall),
        "WireToolResult": schema_for!(crate::wire::WireToolResult),
        "ExecutionPlacement": schema_for!(meerkat_core::ExecutionPlacement),
        "ExecutionPlacementIdentity": schema_for!(meerkat_core::ExecutionPlacementIdentity),
        "ScheduleListResult": schema_for!(crate::wire::ScheduleListResult),
        "ScheduleOccurrencesResult": schema_for!(crate::wire::ScheduleOccurrencesResult),
        "WorkItem": schema_for!(meerkat_workgraph::WorkItem),
        "WorkGraphSnapshot": schema_for!(meerkat_workgraph::WorkGraphSnapshot),
        "WorkGraphItemsResponse": schema_for!(meerkat_workgraph::WorkGraphItemsResponse),
        "WorkGraphEventsResponse": schema_for!(meerkat_workgraph::WorkGraphEventsResponse),
        // Phase 4c — auth-binding wire types.
        "WireAuthBindingRef": schema_for!(crate::wire::WireAuthBindingRef),
        "WireBackendProfile": schema_for!(crate::wire::WireBackendProfile),
        "WireAuthProfile": schema_for!(crate::wire::WireAuthProfile),
        "WireProviderBinding": schema_for!(crate::wire::WireProviderBinding),
        "WireRealmConnectionSet": schema_for!(crate::wire::WireRealmConnectionSet),
        "WireBindingIdentity": schema_for!(crate::wire::WireBindingIdentity),
        "WireAuthProfileCreated": schema_for!(crate::wire::WireAuthProfileCreated),
        "WireAuthProfileDetail": schema_for!(crate::wire::WireAuthProfileDetail),
        "WireAuthProfileCleared": schema_for!(crate::wire::WireAuthProfileCleared),
        "WireLoginStart": schema_for!(crate::wire::WireLoginStart),
        "WireLoginReady": schema_for!(crate::wire::WireLoginReady),
        "WireDeviceStart": schema_for!(crate::wire::WireDeviceStart),
        "WireDeviceCompleteResult": schema_for!(crate::wire::WireDeviceCompleteResult),
        "WireProvisionApiKeyResult": schema_for!(crate::wire::WireProvisionApiKeyResult),
        "WireRealmSummary": schema_for!(crate::wire::WireRealmSummary),
        "WireRealmList": schema_for!(crate::wire::WireRealmList),
        "WireAuthProfilesList": schema_for!(crate::wire::WireAuthProfilesList),
        "WireAuthStatus": schema_for!(crate::wire::WireAuthStatus),
        "WireAuthStatusDetail": schema_for!(crate::wire::WireAuthStatusDetail),
        "WireAuthError": schema_for!(crate::wire::WireAuthError),
        "SkillEntry": schema_for!(crate::wire::SkillEntry),
        "SkillListResponse": schema_for!(crate::wire::SkillListResponse),
        "SkillInspectResponse": schema_for!(crate::wire::SkillInspectResponse),
        "BridgeAck": schema_for!(crate::wire::BridgeAck),
        "BridgeBindPayload": schema_for!(crate::wire::BridgeBindPayload),
        "BridgeBindResponse": schema_for!(crate::wire::BridgeBindResponse),
        "BridgeBootstrapToken": schema_for!(crate::wire::supervisor_bridge::BridgeBootstrapToken),
        "BridgeCapabilities": schema_for!(crate::wire::BridgeCapabilities),
        "BridgeCommand": schema_for!(crate::wire::BridgeCommand),
        "BridgeDeliveryOutcome": schema_for!(crate::wire::BridgeDeliveryOutcome),
        "BridgeDeliveryPayload": schema_for!(crate::wire::BridgeDeliveryPayload),
        "BridgeDeliveryRejectionCause": schema_for!(crate::wire::BridgeDeliveryRejectionCause),
        "BridgeDeliveryResponse": schema_for!(crate::wire::BridgeDeliveryResponse),
        "BridgeDestroyResponse": schema_for!(crate::wire::BridgeDestroyResponse),
        "BridgeHardCancelPayload": schema_for!(crate::wire::BridgeHardCancelPayload),
        "BridgeMemberRuntimeState": schema_for!(crate::wire::BridgeMemberRuntimeState),
        "BridgeObservationResponse": schema_for!(crate::wire::BridgeObservationResponse),
        "BridgePeerConnectivity": schema_for!(crate::wire::BridgePeerConnectivity),
        "BridgePeerSpec": schema_for!(crate::wire::BridgePeerSpec),
        "BridgePeerWiringPayload": schema_for!(crate::wire::BridgePeerWiringPayload),
        "BridgeProtocolVersion": schema_for!(crate::wire::BridgeProtocolVersion),
        "BridgeRejectionCause": schema_for!(crate::wire::supervisor_bridge::BridgeRejectionCause),
        "BridgeReply": schema_for!(crate::wire::BridgeReply),
        "BridgeRetireResponse": schema_for!(crate::wire::BridgeRetireResponse),
        "BridgeSupervisorPayload": schema_for!(crate::wire::BridgeSupervisorPayload),
        "CommsChecksumTokenParams": schema_for!(crate::wire::CommsChecksumTokenParams),
        "CommsChecksumTokenResult": schema_for!(crate::wire::CommsChecksumTokenResult),
        "CommsChecksumTokenResultIntent": schema_for!(crate::wire::CommsChecksumTokenResultIntent),
        "CommsCommandRequest": schema_for!(crate::wire::CommsCommandRequest),
        "CommsPeerLifecycleParams": schema_for!(crate::wire::CommsPeerLifecycleParams),
        "CommsPeerRequestIntent": schema_for!(crate::wire::CommsPeerRequestIntent),
        "CommsPeerRequestParams": schema_for!(crate::wire::CommsPeerRequestParams),
        "CommsPeerResponseResult": schema_for!(crate::wire::CommsPeerResponseResult),
        "CommsSendResult": schema_for!(crate::wire::CommsSendResult),
        "PeerId": schema_for!(crate::wire::PeerId),
        "PeerName": schema_for!(crate::wire::WireCommsPeerName),
        "PeerTransport": schema_for!(crate::wire::PeerTransport),
        "PeerAddress": schema_for!(crate::wire::PeerAddress),
        "PeerDirectorySource": schema_for!(crate::wire::PeerDirectorySource),
        "PeerSendability": schema_for!(crate::wire::PeerSendability),
        "PeerCapabilitySet": schema_for!(crate::wire::PeerCapabilitySet),
        "PeerReachability": schema_for!(crate::wire::PeerReachability),
        "PeerReachabilityReason": schema_for!(crate::wire::PeerReachabilityReason),
        "PeerDirectoryEntry": schema_for!(crate::wire::PeerDirectoryEntry),
        "PeerDirectoryListing": schema_for!(crate::wire::PeerDirectoryListing),
        "CommsPeersResult": schema_for!(crate::wire::CommsPeersResult),
        "AgentEventEnvelope": schema_for!(meerkat_core::EventEnvelope<meerkat_core::AgentEvent>),
        "SessionStreamOpenResult": schema_for!(crate::wire::SessionStreamOpenResult),
        "SessionStreamCloseResult": schema_for!(crate::wire::SessionStreamCloseResult),
    });
    write_pretty_json(output_dir.join("wire-types.json"), &wire_types)?;

    // Params (only contracts-owned param types)
    let params = serde_json::json!({
        "CoreCreateParams": schema_for!(crate::wire::CoreCreateParams),
        "HelpRequest": schema_for!(crate::wire::HelpRequest),
        "CommsParams": schema_for!(crate::wire::CommsParams),
        "SkillsParams": schema_for!(crate::wire::SkillsParams),
        "McpAddParams": schema_for!(crate::wire::McpAddParams),
        "McpRemoveParams": schema_for!(crate::wire::McpRemoveParams),
        "McpReloadParams": schema_for!(crate::wire::McpReloadParams),
        "MobCreateParams": schema_for!(crate::wire::MobCreateParams),
        "MobIdParams": schema_for!(crate::wire::MobIdParams),
        "MobMemberParams": schema_for!(crate::wire::MobMemberParams),
        "MobSpawnParams": schema_for!(crate::wire::MobSpawnParams),
        "MobSpawnManyParams": schema_for!(crate::wire::MobSpawnManyParams),
        "MobRespawnParams": schema_for!(crate::wire::MobRespawnParams),
        "MobEventsParams": schema_for!(crate::wire::MobEventsParams),
        "MobWireParams": schema_for!(crate::wire::MobWireParams),
        "MobUnwireParams": schema_for!(crate::wire::MobUnwireParams),
        "MobLifecycleParams": schema_for!(crate::wire::MobLifecycleParams),
        "MobAppendSystemContextParams": schema_for!(crate::wire::MobAppendSystemContextParams),
        "MobFlowRunParams": schema_for!(crate::wire::MobFlowRunParams),
        "MobFlowStatusParams": schema_for!(crate::wire::MobFlowStatusParams),
        "MobFlowCancelParams": schema_for!(crate::wire::MobFlowCancelParams),
        "MobSpawnHelperParams": schema_for!(crate::wire::MobSpawnHelperParams),
        "MobForkHelperParams": schema_for!(crate::wire::MobForkHelperParams),
        "MobTurnStartParams": schema_for!(crate::wire::MobTurnStartParams),
        "MobWaitParams": schema_for!(crate::wire::MobWaitParams),
        "MobProfileCreateParams": schema_for!(crate::wire::MobProfileCreateParams),
        "MobProfileNameParams": schema_for!(crate::wire::MobProfileNameParams),
        "MobProfileUpdateParams": schema_for!(crate::wire::MobProfileUpdateParams),
        "MobProfileDeleteParams": schema_for!(crate::wire::MobProfileDeleteParams),
        "MobStreamOpenParams": schema_for!(crate::wire::MobStreamOpenParams),
        "MobStreamCloseParams": schema_for!(crate::wire::MobStreamCloseParams),
        "MobMemberSendParams": schema_for!(crate::wire::MobMemberSendParams),
        "MobIngressInteractionParams": schema_for!(crate::wire::MobIngressInteractionParams),
        "MobEnsureMemberParams": schema_for!(crate::wire::MobEnsureMemberParams),
        "MobReconcileParams": schema_for!(crate::wire::MobReconcileParams),
        "MobListMembersMatchingParams": schema_for!(crate::wire::MobListMembersMatchingParams),
        "MobSubmitWorkParams": schema_for!(crate::wire::MobSubmitWorkParams),
        "MobCancelWorkParams": schema_for!(crate::wire::MobCancelWorkParams),
        "MobCancelAllWorkParams": schema_for!(crate::wire::MobCancelAllWorkParams),
        "RealmIdParams": schema_for!(crate::wire::RealmIdParams),
        "BindingIdParams": schema_for!(crate::wire::BindingIdParams),
        "CreateProfileParams": schema_for!(crate::wire::CreateProfileParams),
        "LoginStartParams": schema_for!(crate::wire::LoginStartParams),
        "LoginCompleteParams": schema_for!(crate::wire::LoginCompleteParams),
        "DeviceStartParams": schema_for!(crate::wire::DeviceStartParams),
        "DeviceCompleteParams": schema_for!(crate::wire::DeviceCompleteParams),
        "ProvisionApiKeyParams": schema_for!(crate::wire::ProvisionApiKeyParams),
        "SessionPeerResponseTerminalParams": schema_for!(crate::wire::SessionPeerResponseTerminalParams),
        "SessionStreamOpenParams": schema_for!(crate::wire::SessionStreamOpenParams),
        "SessionStreamCloseParams": schema_for!(crate::wire::SessionStreamCloseParams),
        "ForkSessionAtParams": schema_for!(crate::wire::ForkSessionAtParams),
        "ForkSessionReplaceParams": schema_for!(crate::wire::ForkSessionReplaceParams),
        "CommsSendParams": schema_for!(crate::wire::CommsSendParams),
        "CommsPeersParams": schema_for!(crate::wire::CommsPeersParams),
        "ScheduleIdParams": schema_for!(crate::wire::ScheduleIdParams),
        "ListSchedulesParams": schema_for!(crate::wire::ListSchedulesParams),
        "ScheduleOccurrencesParams": schema_for!(crate::wire::ScheduleOccurrencesParams),
        "UpdateScheduleParams": schema_for!(crate::wire::UpdateScheduleParams),
    });
    write_pretty_json(output_dir.join("params.json"), &params)?;

    // Errors
    let errors = serde_json::json!({
        "ErrorCode": schema_for!(crate::error::ErrorCode),
        "ErrorCategory": schema_for!(crate::error::ErrorCategory),
        "WireError": schema_for!(crate::error::WireError),
        "CapabilityHint": schema_for!(crate::error::CapabilityHint),
    });
    write_pretty_json(output_dir.join("errors.json"), &errors)?;

    // Capabilities
    let capabilities = serde_json::json!({
        "CapabilityId": schema_for!(crate::capability::CapabilityId),
        "CapabilityScope": schema_for!(crate::capability::CapabilityScope),
        "CapabilityStatus": schema_for!(crate::capability::CapabilityStatus),
        "CapabilitiesResponse": schema_for!(crate::capability::CapabilitiesResponse),
    });
    write_pretty_json(output_dir.join("capabilities.json"), &capabilities)?;

    // Runtime host projections
    let runtime_host = serde_json::json!({
        "RuntimeHostIdScope": schema_for!(crate::wire::RuntimeHostIdScope),
        "RuntimeHostHealthStatus": schema_for!(crate::wire::RuntimeHostHealthStatus),
        "RuntimeHostFeatureFlags": schema_for!(crate::wire::RuntimeHostFeatureFlags),
        "RuntimeHostRealmProjection": schema_for!(crate::wire::RuntimeHostRealmProjection),
        "RuntimeHostEndpointProjection": schema_for!(crate::wire::RuntimeHostEndpointProjection),
        "RuntimeHostCapabilities": schema_for!(crate::wire::RuntimeHostCapabilities),
        "RuntimeHostHealth": schema_for!(crate::wire::RuntimeHostHealth),
        "RuntimeHostInfo": schema_for!(crate::wire::RuntimeHostInfo),
    });
    write_pretty_json(output_dir.join("runtime-host.json"), &runtime_host)?;

    // Models catalog
    let models = serde_json::json!({
        "WireModelTier": schema_for!(crate::wire::WireModelTier),
        "WireModelProfile": schema_for!(crate::wire::WireModelProfile),
        "WireResolvedModelCapabilities": schema_for!(crate::wire::WireResolvedModelCapabilities),
        "CatalogModelEntry": schema_for!(crate::wire::CatalogModelEntry),
        "ProviderCatalog": schema_for!(crate::wire::ProviderCatalog),
        "ModelsCatalogResponse": schema_for!(crate::wire::ModelsCatalogResponse),
    });
    write_pretty_json(output_dir.join("models.json"), &models)?;

    // Events — includes canonical event type inventory plus schema-ready
    // payload definitions where available.
    let events = serde_json::json!({
        "AgentEvent": schema_for!(meerkat_core::AgentEvent),
        "ScopedAgentEvent": schema_for!(meerkat_core::ScopedAgentEvent),
        "WireEvent": {
            "description": "Event envelope: session_id, sequence, event (AgentEvent), contract_version",
            "known_event_types": crate::KNOWN_AGENT_EVENT_TYPES,
            "known_payloads": {
                "tool_config_changed": {
                    "type": "object",
                    "required": ["payload"],
                    "properties": {
                        "payload": {
                            "type": "object",
                            "required": ["operation", "target", "status", "persisted"],
                            "properties": {
                                "operation": {"type": "string", "enum": ["add", "remove", "reload"]},
                                "target": {"type": "string"},
                                "status": {"type": "string"},
                                "persisted": {"type": "boolean"},
                                "applied_at_turn": {"type": ["integer", "null"], "minimum": 0},
                            }
                        }
                    }
                }
            },
            "note": "AgentEvent schema is emitted above. known_event_types stays as a lightweight canonical inventory for surface drift checks."
        }
    });
    write_pretty_json(output_dir.join("events.json"), &events)?;

    // RPC methods — structural description of the method surface.
    // This is documentation, not a consumable schema for codegen.
    let rpc_methods = serde_json::json!({
        "methods": crate::rpc_method_catalog(crate::RpcMethodCatalogOptions::documented_surface()),
        "notifications": crate::rpc_notification_catalog(
            crate::RpcMethodCatalogOptions::documented_surface()
        )
    });
    write_pretty_json(output_dir.join("rpc-methods.json"), &rpc_methods)?;

    // Auth/connection semantic vocabulary consumed by generated SDK helpers.
    // Values are projected from typed provider/auth/source/state enums so Web
    // helpers fail closed without carrying local string authority.
    let openai_backend_kinds: Vec<&'static str> =
        meerkat_core::provider_matrix::OpenAiBackendKind::ALL
            .iter()
            .copied()
            .map(meerkat_core::provider_matrix::OpenAiBackendKind::as_str)
            .collect();
    let anthropic_backend_kinds: Vec<&'static str> =
        meerkat_core::provider_matrix::AnthropicBackendKind::ALL
            .iter()
            .copied()
            .map(meerkat_core::provider_matrix::AnthropicBackendKind::as_str)
            .collect();
    let google_backend_kinds: Vec<&'static str> =
        meerkat_core::provider_matrix::GoogleBackendKind::ALL
            .iter()
            .copied()
            .map(meerkat_core::provider_matrix::GoogleBackendKind::as_str)
            .collect();
    let self_hosted_backend_kinds = ["self_hosted", "openai_compatible"];
    let backend_kinds = unique_contract_values(&[
        &openai_backend_kinds,
        &anthropic_backend_kinds,
        &google_backend_kinds,
        &self_hosted_backend_kinds,
    ]);

    let openai_auth_methods: Vec<&'static str> =
        meerkat_core::provider_matrix::OpenAiAuthMethod::ALL
            .iter()
            .copied()
            .map(meerkat_core::provider_matrix::OpenAiAuthMethod::as_str)
            .collect();
    let anthropic_auth_methods: Vec<&'static str> =
        meerkat_core::provider_matrix::AnthropicAuthMethod::ALL
            .iter()
            .copied()
            .map(meerkat_core::provider_matrix::AnthropicAuthMethod::as_str)
            .collect();
    let google_auth_methods: Vec<&'static str> =
        meerkat_core::provider_matrix::GoogleAuthMethod::ALL
            .iter()
            .copied()
            .map(meerkat_core::provider_matrix::GoogleAuthMethod::as_str)
            .collect();
    let self_hosted_auth_methods = ["api_key", "static_bearer", "none"];
    let auth_methods = unique_contract_values(&[
        &openai_auth_methods,
        &anthropic_auth_methods,
        &google_auth_methods,
        &self_hosted_auth_methods,
    ]);

    let providers: Vec<&'static str> = meerkat_core::Provider::ALL_CONCRETE
        .iter()
        .map(meerkat_core::Provider::as_str)
        .collect();
    let auth_status_states: Vec<&'static str> = meerkat_core::AuthStatusPhase::ALL
        .iter()
        .copied()
        .map(meerkat_core::AuthStatusPhase::as_public_str)
        .collect();
    let auth_connection_contracts = serde_json::json!({
        "providers": providers,
        "backend_kinds": backend_kinds,
        "auth_methods": auth_methods,
        "credential_source_kinds": meerkat_core::CredentialSourceSpec::ALL_KIND_LABELS,
        "auth_status_states": auth_status_states,
        "device_complete_states": ["pending", "slow_down", "access_denied", "expired", "ready"],
        "login_ready_state": "ready",
        "provider_backend_kinds": {
            "openai": openai_backend_kinds,
            "anthropic": anthropic_backend_kinds,
            "gemini": google_backend_kinds,
            "self_hosted": self_hosted_backend_kinds,
        },
        "provider_auth_methods": {
            "openai": openai_auth_methods,
            "anthropic": anthropic_auth_methods,
            "gemini": google_auth_methods,
            "self_hosted": self_hosted_auth_methods,
        }
    });
    write_pretty_json(
        output_dir.join("auth-connection-contracts.json"),
        &auth_connection_contracts,
    )?;

    fn schema_ref(name: &str) -> Value {
        serde_json::json!({ "$ref": format!("#/components/schemas/{name}") })
    }

    fn media_content(content_type: &str, schema_name: &str) -> Value {
        serde_json::json!({
            content_type: {
                "schema": schema_ref(schema_name),
            }
        })
    }

    fn rewrite_component_refs(value: &mut Value) {
        match value {
            Value::Object(object) => {
                if let Some(Value::String(reference)) = object.get_mut("$ref")
                    && let Some(name) = reference.strip_prefix("#/$defs/")
                {
                    *reference = format!("#/components/schemas/{name}");
                }
                for nested in object.values_mut() {
                    rewrite_component_refs(nested);
                }
            }
            Value::Array(values) => {
                for nested in values {
                    rewrite_component_refs(nested);
                }
            }
            _ => {}
        }
    }

    fn add_component_schema(components: &mut Map<String, Value>, name: String, mut schema: Value) {
        let defs = schema
            .as_object_mut()
            .and_then(|object| object.remove("$defs"));
        if let Some(Value::Object(defs)) = defs {
            for (def_name, def_schema) in defs {
                add_component_schema(components, def_name, def_schema);
            }
        }
        if let Some(object) = schema.as_object_mut() {
            object.remove("$schema");
        }
        rewrite_component_refs(&mut schema);
        components.entry(name).or_insert(schema);
    }

    fn add_component_section(components: &mut Map<String, Value>, section: &Value) {
        if let Some(schemas) = section.as_object() {
            for (name, schema) in schemas {
                add_component_schema(components, name.clone(), schema.clone());
            }
        }
    }

    fn collect_component_refs(value: &Value, refs: &mut std::collections::BTreeSet<String>) {
        match value {
            Value::Object(object) => {
                if let Some(Value::String(reference)) = object.get("$ref")
                    && let Some(name) = reference.strip_prefix("#/components/schemas/")
                {
                    refs.insert(name.to_string());
                }
                for nested in object.values() {
                    collect_component_refs(nested, refs);
                }
            }
            Value::Array(values) => {
                for nested in values {
                    collect_component_refs(nested, refs);
                }
            }
            _ => {}
        }
    }

    fn referenced_rest_components(
        components: Map<String, Value>,
        rest_paths: &Value,
    ) -> Map<String, Value> {
        let mut needed = std::collections::BTreeSet::new();
        collect_component_refs(rest_paths, &mut needed);

        let mut queue = needed
            .iter()
            .cloned()
            .collect::<std::collections::VecDeque<_>>();
        while let Some(name) = queue.pop_front() {
            let Some(schema) = components.get(&name) else {
                continue;
            };
            let before = needed.clone();
            collect_component_refs(schema, &mut needed);
            for discovered in needed.difference(&before) {
                queue.push_back(discovered.clone());
            }
        }

        components
            .into_iter()
            .filter(|(name, _schema)| needed.contains(name))
            .collect()
    }

    fn object_schema(properties: Vec<(&str, Value)>, required: Vec<&str>) -> Value {
        let mut property_map = Map::new();
        for (name, schema) in properties {
            property_map.insert(name.to_string(), schema);
        }
        serde_json::json!({
            "type": "object",
            "properties": Value::Object(property_map),
            "required": required,
        })
    }

    fn closed_object_schema(properties: Vec<(&str, Value)>, required: Vec<&str>) -> Value {
        let mut schema = object_schema(properties, required);
        if let Value::Object(object) = &mut schema {
            object.insert("additionalProperties".to_string(), Value::Bool(false));
        }
        schema
    }

    fn string_schema() -> Value {
        serde_json::json!({ "type": "string" })
    }

    fn bool_schema() -> Value {
        serde_json::json!({ "type": "boolean" })
    }

    fn integer_schema() -> Value {
        serde_json::json!({ "type": "integer", "minimum": 0 })
    }

    fn json_value_schema() -> Value {
        serde_json::json!({
            "description": "Arbitrary JSON value.",
        })
    }

    fn nullable(schema: Value) -> Value {
        serde_json::json!({ "anyOf": [schema, { "type": "null" }] })
    }

    fn rest_manual_components() -> Map<String, Value> {
        let mut components = Map::new();
        let json_value = schema_ref("JsonValue");
        let content_input = serde_json::json!({
            "oneOf": [
                { "type": "string" },
                schema_ref("WireContentInput")
            ]
        });
        let labels = serde_json::json!({
            "type": "object",
            "additionalProperties": { "type": "string" }
        });
        let string_array = serde_json::json!({
            "type": "array",
            "items": { "type": "string" }
        });

        components.insert("JsonValue".to_string(), json_value_schema());
        components.insert(
            "PlainTextResponse".to_string(),
            serde_json::json!({ "type": "string" }),
        );
        components.insert(
            "SseEventStream".to_string(),
            serde_json::json!({
                "type": "string",
                "description": "Server-sent event stream."
            }),
        );
        components.insert(
            "StatusResponse".to_string(),
            serde_json::json!({
                "type": "object",
                "additionalProperties": true
            }),
        );
        components.insert(
            "ListSessionsResponse".to_string(),
            object_schema(
                vec![(
                    "sessions",
                    serde_json::json!({
                        "type": "array",
                        "items": schema_ref("WireSessionSummary")
                    }),
                )],
                vec!["sessions"],
            ),
        );
        components.insert(
            "SessionDetailsResponse".to_string(),
            object_schema(
                vec![
                    ("session_id", string_schema()),
                    ("session_ref", string_schema()),
                    ("created_at", string_schema()),
                    ("updated_at", string_schema()),
                    ("message_count", integer_schema()),
                    ("total_tokens", integer_schema()),
                    ("labels", labels.clone()),
                ],
                vec![
                    "session_id",
                    "session_ref",
                    "created_at",
                    "updated_at",
                    "message_count",
                    "total_tokens",
                ],
            ),
        );
        components.insert(
            "ConfigEnvelope".to_string(),
            object_schema(
                vec![
                    ("config", json_value.clone()),
                    ("generation", integer_schema()),
                    ("realm_id", string_schema()),
                    ("instance_id", string_schema()),
                    ("backend", string_schema()),
                    (
                        "resolved_paths",
                        serde_json::json!({
                            "type": "object",
                            "additionalProperties": { "type": "string" }
                        }),
                    ),
                ],
                vec!["config", "generation"],
            ),
        );
        components.insert(
            "RestCreateSessionRequest".to_string(),
            object_schema(
                vec![
                    ("prompt", content_input.clone()),
                    ("system_prompt", string_schema()),
                    ("model", string_schema()),
                    ("provider", string_schema()),
                    ("max_tokens", integer_schema()),
                    ("output_schema", json_value.clone()),
                    ("structured_output_retries", integer_schema()),
                    ("verbose", bool_schema()),
                    ("keep_alive", nullable(bool_schema())),
                    ("comms_name", string_schema()),
                    ("peer_meta", json_value.clone()),
                    ("hooks_override", json_value.clone()),
                    ("enable_builtins", bool_schema()),
                    ("enable_shell", bool_schema()),
                    ("enable_memory", bool_schema()),
                    ("enable_schedule", bool_schema()),
                    ("enable_workgraph", bool_schema()),
                    ("enable_mob", bool_schema()),
                    ("budget_limits", json_value.clone()),
                    ("provider_params", json_value.clone()),
                    (
                        "preload_skills",
                        serde_json::json!({
                            "type": "array",
                            "items": json_value
                        }),
                    ),
                    (
                        "skill_refs",
                        serde_json::json!({
                            "type": "array",
                            "items": json_value
                        }),
                    ),
                    ("labels", labels),
                    ("additional_instructions", string_array.clone()),
                    ("app_context", json_value.clone()),
                    (
                        "shell_env",
                        serde_json::json!({
                            "type": "object",
                            "additionalProperties": { "type": "string" }
                        }),
                    ),
                ],
                vec!["prompt"],
            ),
        );
        components.insert(
            "RestContinueSessionRequest".to_string(),
            object_schema(
                vec![
                    ("session_id", string_schema()),
                    ("prompt", content_input),
                    ("system_prompt", string_schema()),
                    ("output_schema", json_value.clone()),
                    ("structured_output_retries", integer_schema()),
                    ("keep_alive", nullable(bool_schema())),
                    ("comms_name", string_schema()),
                    ("peer_meta", json_value.clone()),
                    ("verbose", bool_schema()),
                    ("model", string_schema()),
                    ("provider", string_schema()),
                    ("max_tokens", integer_schema()),
                    ("hooks_override", json_value.clone()),
                    (
                        "skill_refs",
                        serde_json::json!({
                            "type": "array",
                            "items": json_value
                        }),
                    ),
                    ("flow_tool_overlay", json_value.clone()),
                    ("additional_instructions", string_array),
                ],
                vec!["session_id", "prompt"],
            ),
        );
        components.insert(
            "RestAppendSystemContextRequest".to_string(),
            object_schema(
                vec![
                    ("text", string_schema()),
                    ("source", string_schema()),
                    ("idempotency_key", string_schema()),
                ],
                vec!["text"],
            ),
        );
        components.insert(
            "RestPeerResponseTerminalRequest".to_string(),
            closed_object_schema(
                vec![
                    ("peer_id", schema_ref("PeerId")),
                    ("display_name", schema_ref("PeerName")),
                    ("request_id", schema_ref("PeerCorrelationId")),
                    ("status", schema_ref("PeerResponseTerminalStatusWire")),
                    ("result", json_value.clone()),
                ],
                vec!["peer_id", "request_id", "status", "result"],
            ),
        );
        components.insert(
            "RestSetConfigRequest".to_string(),
            serde_json::json!({
                "oneOf": [
                    schema_ref("JsonValue"),
                    object_schema(
                        vec![
                            ("config", schema_ref("JsonValue")),
                            ("expected_generation", integer_schema()),
                        ],
                        vec!["config"],
                    )
                ]
            }),
        );
        components.insert(
            "RestPatchConfigRequest".to_string(),
            serde_json::json!({
                "oneOf": [
                    schema_ref("JsonValue"),
                    object_schema(
                        vec![
                            ("patch", schema_ref("JsonValue")),
                            ("expected_generation", integer_schema()),
                        ],
                        vec!["patch"],
                    )
                ]
            }),
        );
        components.insert(
            "RestScheduleToolCallRequest".to_string(),
            object_schema(
                vec![("name", string_schema()), ("arguments", json_value.clone())],
                vec!["name"],
            ),
        );
        components.insert(
            "RestAuthBindingRequest".to_string(),
            object_schema(
                vec![
                    ("realm_id", string_schema()),
                    ("binding_id", string_schema()),
                    ("profile_id", string_schema()),
                ],
                vec!["realm_id", "binding_id"],
            ),
        );
        components.insert(
            "RestAuthProfileCreateRequest".to_string(),
            serde_json::json!({
                "type": "object",
                "additionalProperties": true,
                "required": ["realm_id", "binding_id", "auth_method"],
            }),
        );
        components.insert(
            "RestAuthLoginStartRequest".to_string(),
            object_schema(
                vec![
                    ("provider", string_schema()),
                    ("redirect_uri", string_schema()),
                    ("realm_id", string_schema()),
                    ("binding_id", string_schema()),
                    ("profile_id", string_schema()),
                ],
                vec!["provider", "redirect_uri", "realm_id", "binding_id"],
            ),
        );
        components.insert(
            "RestMobHelperRequest".to_string(),
            object_schema(
                vec![
                    ("prompt", string_schema()),
                    ("agent_identity", string_schema()),
                    ("role_name", string_schema()),
                    ("runtime_mode", string_schema()),
                    ("backend", string_schema()),
                ],
                vec!["prompt"],
            ),
        );
        components.insert(
            "RestMobForkHelperRequest".to_string(),
            object_schema(
                vec![
                    ("source_member_id", string_schema()),
                    ("prompt", string_schema()),
                    ("agent_identity", string_schema()),
                    ("role_name", string_schema()),
                    ("fork_context", json_value),
                    ("runtime_mode", string_schema()),
                    ("backend", string_schema()),
                ],
                vec!["source_member_id", "prompt"],
            ),
        );
        components.insert(
            "RestMobWaitRequest".to_string(),
            object_schema(
                vec![
                    (
                        "member_ids",
                        serde_json::json!({
                            "type": "array",
                            "items": { "type": "string" }
                        }),
                    ),
                    ("timeout_ms", integer_schema()),
                ],
                vec![],
            ),
        );
        components.insert(
            "RestMobWireMembersBatchRequest".to_string(),
            object_schema(
                vec![(
                    "edges",
                    serde_json::json!({
                        "type": "array",
                        "items": { "$ref": "#/components/schemas/MobWireMembersBatchEdge" }
                    }),
                )],
                vec!["edges"],
            ),
        );
        components
    }

    #[derive(Clone, Copy)]
    struct RestOperationContract {
        request_schema: Option<&'static str>,
        request_required: bool,
        response_schema: &'static str,
        response_content_type: &'static str,
    }

    impl RestOperationContract {
        const fn json(response_schema: &'static str) -> Self {
            Self {
                request_schema: None,
                request_required: false,
                response_schema,
                response_content_type: "application/json",
            }
        }

        const fn with_json_request(
            request_schema: &'static str,
            response_schema: &'static str,
        ) -> Self {
            Self {
                request_schema: Some(request_schema),
                request_required: true,
                response_schema,
                response_content_type: "application/json",
            }
        }

        const fn with_optional_json_request(
            request_schema: &'static str,
            response_schema: &'static str,
        ) -> Self {
            Self {
                request_schema: Some(request_schema),
                request_required: false,
                response_schema,
                response_content_type: "application/json",
            }
        }

        const fn text(response_schema: &'static str) -> Self {
            Self {
                request_schema: None,
                request_required: false,
                response_schema,
                response_content_type: "text/plain",
            }
        }

        const fn event_stream(response_schema: &'static str) -> Self {
            Self {
                request_schema: None,
                request_required: false,
                response_schema,
                response_content_type: "text/event-stream",
            }
        }
    }

    fn rest_operation_contract(path: &str, method: &str) -> RestOperationContract {
        if let Some(response_schema) =
            meerkat_workgraph::workgraph_rest_response_schema(path, method)
        {
            return RestOperationContract::json(response_schema);
        }

        match (path, method) {
            ("/help", "post") => {
                RestOperationContract::with_json_request("HelpRequest", "HelpResponse")
            }
            ("/sessions", "get") => RestOperationContract::json("ListSessionsResponse"),
            ("/sessions", "post") => RestOperationContract::with_json_request(
                "RestCreateSessionRequest",
                "WireRunResult",
            ),
            ("/sessions/{id}", "get") => RestOperationContract::json("SessionDetailsResponse"),
            ("/sessions/{id}", "delete") => RestOperationContract::json("StatusResponse"),
            ("/sessions/{id}/history", "get") => RestOperationContract::json("WireSessionHistory"),
            ("/sessions/{id}/interrupt", "post") => RestOperationContract::json("StatusResponse"),
            ("/sessions/{id}/system_context", "post") => RestOperationContract::with_json_request(
                "RestAppendSystemContextRequest",
                "StatusResponse",
            ),
            ("/sessions/{id}/messages", "post") => RestOperationContract::with_json_request(
                "RestContinueSessionRequest",
                "WireRunResult",
            ),
            ("/sessions/{id}/external-events", "post") => {
                RestOperationContract::with_json_request("JsonValue", "StatusResponse")
            }
            ("/sessions/{id}/peer-response-terminal", "post") => {
                RestOperationContract::with_json_request(
                    "RestPeerResponseTerminalRequest",
                    "StatusResponse",
                )
            }
            ("/sessions/{id}/mcp/add", "post") => {
                RestOperationContract::with_json_request("McpAddParams", "McpLiveOpResponse")
            }
            ("/sessions/{id}/mcp/remove", "post") => {
                RestOperationContract::with_json_request("McpRemoveParams", "McpLiveOpResponse")
            }
            ("/sessions/{id}/mcp/reload", "post") => {
                RestOperationContract::with_json_request("McpReloadParams", "McpLiveOpResponse")
            }
            ("/sessions/{id}/events" | "/mob/{id}/events", "get") => {
                RestOperationContract::event_stream("SseEventStream")
            }
            ("/sessions/{id}/status", "get") => RestOperationContract::json("RuntimeStateResult"),
            ("/schedule/call", "post") => {
                RestOperationContract::with_json_request("RestScheduleToolCallRequest", "JsonValue")
            }
            ("/schedules", "get") => RestOperationContract::json("ScheduleListResult"),
            ("/schedules", "post") => {
                RestOperationContract::with_json_request("JsonValue", "JsonValue")
            }
            ("/schedules/{id}", "get" | "delete")
            | ("/schedules/{id}/pause" | "/schedules/{id}/resume", "post") => {
                RestOperationContract::json("JsonValue")
            }
            ("/schedules/{id}", "patch") => {
                RestOperationContract::with_json_request("JsonValue", "JsonValue")
            }
            ("/schedules/{id}/occurrences", "get") => {
                RestOperationContract::json("ScheduleOccurrencesResult")
            }
            ("/comms/send", "post") => {
                RestOperationContract::with_json_request("CommsSendParams", "CommsSendResult")
            }
            ("/comms/peers", "get") => RestOperationContract::json("CommsPeersResult"),
            ("/config", "get") => RestOperationContract::json("ConfigEnvelope"),
            ("/config", "put") => {
                RestOperationContract::with_json_request("RestSetConfigRequest", "ConfigEnvelope")
            }
            ("/config", "patch") => {
                RestOperationContract::with_json_request("RestPatchConfigRequest", "ConfigEnvelope")
            }
            ("/skills", "get") => RestOperationContract::json("SkillListResponse"),
            ("/capabilities", "get") => RestOperationContract::json("CapabilitiesResponse"),
            ("/runtime/host_info", "get") => RestOperationContract::json("RuntimeHostInfo"),
            ("/runtime/capabilities", "get") => {
                RestOperationContract::json("RuntimeHostCapabilities")
            }
            ("/runtime/health", "get") => RestOperationContract::json("RuntimeHostHealth"),
            ("/models/catalog", "get") => RestOperationContract::json("ModelsCatalogResponse"),
            ("/mob/{id}/spawn-helper", "post") => {
                RestOperationContract::with_json_request("RestMobHelperRequest", "JsonValue")
            }
            ("/mob/{id}/fork-helper", "post") => {
                RestOperationContract::with_json_request("RestMobForkHelperRequest", "JsonValue")
            }
            ("/mob/{id}/wait-kickoff", "post") => {
                RestOperationContract::with_optional_json_request("RestMobWaitRequest", "JsonValue")
            }
            ("/mob/{id}/wire-members-batch", "post") => RestOperationContract::with_json_request(
                "RestMobWireMembersBatchRequest",
                "MobWireMembersBatchResult",
            ),
            ("/mob/{id}/members/{agent_identity}/status", "get")
            | (
                "/mob/{id}/members/{agent_identity}/cancel"
                | "/mob/{id}/members/{agent_identity}/respawn",
                "post",
            ) => RestOperationContract::json("JsonValue"),
            ("/health", "get") => RestOperationContract::text("PlainTextResponse"),
            ("/auth/profiles", "get") => RestOperationContract::json("WireAuthProfilesList"),
            ("/auth/profiles", "post") => RestOperationContract::with_json_request(
                "RestAuthProfileCreateRequest",
                "WireAuthProfileCreated",
            ),
            ("/auth/bindings/{binding_id}", "get") => {
                RestOperationContract::json("WireAuthProfileDetail")
            }
            ("/auth/bindings/{binding_id}", "delete")
            | ("/auth/bindings/{binding_id}/logout", "post") => {
                RestOperationContract::json("WireAuthProfileCleared")
            }
            ("/auth/bindings/{binding_id}/test", "post")
            | ("/auth/bindings/{binding_id}/status", "get") => {
                RestOperationContract::json("WireAuthStatusDetail")
            }
            ("/auth/login/start", "post") => RestOperationContract::with_json_request(
                "RestAuthLoginStartRequest",
                "WireLoginStart",
            ),
            ("/auth/login/complete", "post") => {
                RestOperationContract::with_json_request("LoginCompleteParams", "WireLoginReady")
            }
            ("/auth/login/device/start", "post") => {
                RestOperationContract::with_json_request("DeviceStartParams", "WireDeviceStart")
            }
            ("/auth/login/device/complete", "post") => RestOperationContract::with_json_request(
                "DeviceCompleteParams",
                "WireDeviceCompleteResult",
            ),
            ("/realms", "get") => RestOperationContract::json("WireRealmList"),
            ("/realms/{id}", "get") => RestOperationContract::json("WireRealmConnectionSet"),
            _ => RestOperationContract::json("JsonValue"),
        }
    }

    fn rest_operation_id(method: &str, path: &str) -> String {
        let mut operation_id = method.to_string();
        for segment in path.split('/').filter(|segment| !segment.is_empty()) {
            operation_id.push('_');
            operation_id.push_str(
                &segment
                    .trim_start_matches('{')
                    .trim_end_matches('}')
                    .replace('-', "_"),
            );
        }
        operation_id
    }

    fn rest_path_parameters(path: &str) -> Vec<Value> {
        let mut seen = Vec::new();
        for segment in path.split('/') {
            if let Some(name) = segment
                .strip_prefix('{')
                .and_then(|value| value.strip_suffix('}'))
                && !seen.contains(&name)
            {
                seen.push(name);
            }
        }
        seen.into_iter()
            .map(|name| {
                serde_json::json!({
                    "name": name,
                    "in": "path",
                    "required": true,
                    "schema": { "type": "string" },
                })
            })
            .collect()
    }

    fn rest_responses(contract: RestOperationContract) -> Value {
        serde_json::json!({
            "200": {
                "description": "Successful response",
                "content": media_content(contract.response_content_type, contract.response_schema),
            },
            "default": {
                "description": "Error response",
                "content": media_content("application/json", "WireError"),
            }
        })
    }

    let mut rest_components = rest_manual_components();
    for section in [
        &wire_types,
        &params,
        &errors,
        &capabilities,
        &runtime_host,
        &models,
    ] {
        add_component_section(&mut rest_components, section);
    }

    // REST OpenAPI — generated wire contract for the documented route surface.
    let rest_paths: Map<String, Value> = crate::rest_path_catalog()
        .into_iter()
        .map(|path| {
            let operations = path
                .operations
                .into_iter()
                .map(|operation| {
                    let contract = rest_operation_contract(path.path, operation.method);
                    let mut operation_map = Map::new();
                    operation_map.insert(
                        "operationId".to_string(),
                        Value::String(rest_operation_id(operation.method, path.path)),
                    );
                    operation_map.insert(
                        "summary".to_string(),
                        Value::String(operation.summary.to_string()),
                    );
                    if let Some(description) = operation.description {
                        operation_map.insert(
                            "description".to_string(),
                            Value::String(description.to_string()),
                        );
                    }
                    let parameters = rest_path_parameters(path.path);
                    if !parameters.is_empty() {
                        operation_map.insert("parameters".to_string(), Value::Array(parameters));
                    }
                    if let Some(request_schema) = contract.request_schema {
                        operation_map.insert(
                            "requestBody".to_string(),
                            serde_json::json!({
                                "required": contract.request_required,
                                "content": media_content("application/json", request_schema),
                            }),
                        );
                    }
                    operation_map.insert("responses".to_string(), rest_responses(contract));
                    (operation.method.to_string(), Value::Object(operation_map))
                })
                .collect();
            (path.path.to_string(), Value::Object(operations))
        })
        .collect();
    let rest_components =
        referenced_rest_components(rest_components, &Value::Object(rest_paths.clone()));
    let rest_openapi = serde_json::json!({
        "openapi": "3.1.0",
        "info": {
            "title": "Meerkat REST API",
            "version": crate::version::ContractVersion::CURRENT.to_string(),
        },
        "paths": Value::Object(rest_paths),
        "components": {
            "schemas": Value::Object(rest_components),
        },
    });
    write_pretty_json(output_dir.join("rest-openapi.json"), &rest_openapi)?;

    Ok(())
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
    use std::fs;
    use std::path::PathBuf;
    use std::time::{SystemTime, UNIX_EPOCH};

    use super::*;

    fn temp_output_dir(test_name: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system clock after unix epoch")
            .as_nanos();
        let dir = std::env::temp_dir().join(format!(
            "meerkat-contracts-{test_name}-{}-{nanos}",
            std::process::id()
        ));
        fs::create_dir_all(&dir).expect("create schema output dir");
        dir
    }

    fn assert_schema_accepts(schema: &serde_json::Value, instance: &serde_json::Value) {
        let validator = jsonschema::validator_for(schema).expect("schema compiles");
        let errors = validator
            .iter_errors(instance)
            .map(|error| error.to_string())
            .collect::<Vec<_>>();
        assert!(
            errors.is_empty(),
            "schema should accept {instance}, got errors: {errors:?}"
        );
    }

    fn assert_schema_rejects(schema: &serde_json::Value, instance: &serde_json::Value) {
        let validator = jsonschema::validator_for(schema).expect("schema compiles");
        assert!(
            !validator.is_valid(instance),
            "schema should reject invalid shape {instance}"
        );
    }

    #[test]
    fn emitted_schemas_catalog_auth_status_detail() {
        let output_dir = temp_output_dir("auth-status-detail");
        emit_all_schemas(&output_dir).expect("emit schemas");

        let wire_types: serde_json::Value =
            serde_json::from_slice(&fs::read(output_dir.join("wire-types.json")).unwrap()).unwrap();
        assert!(
            wire_types.get("WireAuthStatus").is_some(),
            "legacy auth status schema must remain emitted"
        );
        let detail = wire_types
            .get("WireAuthStatusDetail")
            .expect("detailed auth status schema must be emitted");
        let detail_props = detail
            .pointer("/properties")
            .expect("detail schema has properties");
        for field in [
            "realm_id",
            "binding_id",
            "auth_binding",
            "profile_id",
            "has_refresh_token",
        ] {
            assert!(
                detail_props.get(field).is_some(),
                "WireAuthStatusDetail schema missing {field}"
            );
        }

        let rpc_methods: serde_json::Value =
            serde_json::from_slice(&fs::read(output_dir.join("rpc-methods.json")).unwrap())
                .unwrap();
        let auth_status = rpc_methods["methods"]
            .as_array()
            .expect("methods array")
            .iter()
            .find(|method| method["name"] == "auth/status/get")
            .expect("auth/status/get catalog entry");
        assert_eq!(
            auth_status["result_type"], "WireAuthStatusDetail",
            "auth/status/get should catalog its concrete detailed response"
        );

        fs::remove_dir_all(&output_dir).unwrap();
    }

    #[test]
    fn emitted_auth_connection_contracts_carry_closed_web_vocabularies() {
        let output_dir = temp_output_dir("auth-connection-contracts");
        emit_all_schemas(&output_dir).expect("emit schemas");

        let contracts: serde_json::Value = serde_json::from_slice(
            &fs::read(output_dir.join("auth-connection-contracts.json")).unwrap(),
        )
        .unwrap();

        assert_eq!(
            contracts["providers"],
            serde_json::json!(["anthropic", "openai", "gemini", "self_hosted"])
        );
        assert!(
            contracts["backend_kinds"]
                .as_array()
                .expect("backend kinds")
                .contains(&serde_json::json!("openai_api")),
            "backend vocabulary must include provider-matrix kinds"
        );
        assert!(
            contracts["auth_methods"]
                .as_array()
                .expect("auth methods")
                .contains(&serde_json::json!("managed_chatgpt_oauth")),
            "auth vocabulary must include provider-matrix methods"
        );
        assert!(
            contracts["credential_source_kinds"]
                .as_array()
                .expect("credential source kinds")
                .contains(&serde_json::json!("external_resolver")),
            "source vocabulary must include CredentialSourceSpec labels"
        );
        assert_eq!(
            contracts["auth_status_states"],
            serde_json::json!([
                "valid",
                "expiring",
                "expired",
                "reauth_required",
                "refresh_failed",
                "unknown"
            ])
        );
        assert_eq!(
            contracts["provider_backend_kinds"]["openai"],
            serde_json::json!(["openai_api", "chatgpt_backend", "azure_openai"])
        );
        assert!(
            contracts["provider_auth_methods"]["openai"]
                .as_array()
                .expect("openai auth methods")
                .contains(&serde_json::json!("azure_api_key")),
            "provider auth relation map must include Azure OpenAI api-key auth"
        );
        assert!(
            contracts["provider_auth_methods"]["openai"]
                .as_array()
                .expect("openai auth methods")
                .contains(&serde_json::json!("managed_chatgpt_oauth")),
            "provider auth relation map must include provider-specific methods"
        );
        assert!(
            !contracts["provider_auth_methods"]["openai"]
                .as_array()
                .expect("openai auth methods")
                .contains(&serde_json::json!("google_oauth")),
            "provider auth relation map must not flatten other provider methods"
        );

        fs::remove_dir_all(&output_dir).unwrap();
    }

    #[test]
    fn emitted_rpc_catalog_carries_typed_auth_and_mob_contracts() {
        let output_dir = temp_output_dir("typed-rpc-catalog");
        emit_all_schemas(&output_dir).expect("emit schemas");

        let rpc_methods: serde_json::Value =
            serde_json::from_slice(&fs::read(output_dir.join("rpc-methods.json")).unwrap())
                .unwrap();
        let methods = rpc_methods["methods"].as_array().expect("methods array");

        for (name, params_type, result_type) in [
            (
                "auth/login/device_complete",
                "DeviceCompleteParams",
                "WireDeviceCompleteResult",
            ),
            (
                "auth/profile/create",
                "CreateProfileParams",
                "WireAuthProfileCreated",
            ),
            (
                "mob/ensure_member",
                "MobEnsureMemberParams",
                "MobEnsureMemberResult",
            ),
            (
                "mob/submit_work",
                "MobSubmitWorkParams",
                "MobSubmitWorkResult",
            ),
            (
                "mob/ingress_interaction",
                "MobIngressInteractionParams",
                "MobIngressInteractionResult",
            ),
            ("mob/spawn", "MobSpawnParams", "MobSpawnResult"),
            ("mob/spawn_many", "MobSpawnManyParams", "MobSpawnManyResult"),
            ("mob/events", "MobEventsParams", "MobEventsResult"),
            (
                "mob/append_system_context",
                "MobAppendSystemContextParams",
                "MobAppendSystemContextResult",
            ),
            (
                "session/stream_open",
                "SessionStreamOpenParams",
                "SessionStreamOpenResult",
            ),
            (
                "session/stream_close",
                "SessionStreamCloseParams",
                "SessionStreamCloseResult",
            ),
            ("comms/send", "CommsSendParams", "CommsSendResult"),
            ("comms/peers", "CommsPeersParams", "CommsPeersResult"),
        ] {
            let method = methods
                .iter()
                .find(|method| method["name"] == name)
                .unwrap_or_else(|| panic!("missing emitted RPC catalog entry for {name}"));
            assert_eq!(
                method["params_type"], params_type,
                "{name} emitted params_type drifted"
            );
            assert_eq!(
                method["result_type"], result_type,
                "{name} emitted result_type drifted"
            );
        }

        let cancel_all_work = methods
            .iter()
            .find(|method| method["name"] == "mob/cancel_all_work")
            .expect("missing emitted RPC catalog entry for mob/cancel_all_work");
        assert_eq!(
            cancel_all_work["params_type"], "MobCancelAllWorkParams",
            "mob/cancel_all_work emitted params_type drifted"
        );
        assert_eq!(
            cancel_all_work["result_type"], "MobCancelAllWorkResult",
            "mob/cancel_all_work emitted result_type drifted"
        );

        fs::remove_dir_all(&output_dir).unwrap();
    }

    #[test]
    fn emitted_comms_and_session_stream_schemas_validate_public_shapes() {
        let output_dir = temp_output_dir("comms-session-stream-contract-shapes");
        emit_all_schemas(&output_dir).expect("emit schemas");

        let params: serde_json::Value =
            serde_json::from_slice(&fs::read(output_dir.join("params.json")).unwrap()).unwrap();
        let wire_types: serde_json::Value =
            serde_json::from_slice(&fs::read(output_dir.join("wire-types.json")).unwrap()).unwrap();

        let session_stream_open = params
            .get("SessionStreamOpenParams")
            .expect("SessionStreamOpenParams schema must be emitted");
        assert_schema_accepts(
            session_stream_open,
            &serde_json::json!({ "session_id": "sid_test" }),
        );
        assert_schema_rejects(
            session_stream_open,
            &serde_json::json!({ "stream_id": "not-open-params" }),
        );

        let session_stream_close = params
            .get("SessionStreamCloseParams")
            .expect("SessionStreamCloseParams schema must be emitted");
        assert_schema_accepts(
            session_stream_close,
            &serde_json::json!({ "stream_id": uuid::Uuid::nil().to_string() }),
        );
        assert_schema_rejects(
            session_stream_close,
            &serde_json::json!({ "stream_id": 42 }),
        );

        let comms_send = params
            .get("CommsSendParams")
            .expect("CommsSendParams schema must be emitted");
        assert_schema_accepts(
            comms_send,
            &serde_json::json!({
                "session_id": "sid_test",
                "kind": "input",
                "body": "hello"
            }),
        );
        assert_schema_rejects(
            comms_send,
            &serde_json::json!({
                "session_id": "sid_test",
                "kind": "input"
            }),
        );
        assert_schema_rejects(
            comms_send,
            &serde_json::json!({
                "session_id": "sid_test",
                "kind": "bogus",
                "body": "hello"
            }),
        );
        assert_schema_rejects(
            comms_send,
            &serde_json::json!({
                "session_id": "sid_test",
                "kind": "input",
                "body": "hello",
                "unexpected": true
            }),
        );

        let comms_peers = params
            .get("CommsPeersParams")
            .expect("CommsPeersParams schema must be emitted");
        assert_schema_accepts(
            comms_peers,
            &serde_json::json!({ "session_id": "sid_test" }),
        );
        assert_schema_rejects(comms_peers, &serde_json::json!({ "session": "sid_test" }));

        assert_schema_accepts(
            wire_types
                .get("SessionStreamOpenResult")
                .expect("SessionStreamOpenResult schema must be emitted"),
            &serde_json::json!({
                "stream_id": uuid::Uuid::nil().to_string(),
                "session_id": "sid_test",
                "opened": true
            }),
        );
        assert_schema_accepts(
            wire_types
                .get("SessionStreamCloseResult")
                .expect("SessionStreamCloseResult schema must be emitted"),
            &serde_json::json!({
                "stream_id": uuid::Uuid::nil().to_string(),
                "closed": true,
                "already_closed": false
            }),
        );
        assert_schema_accepts(
            wire_types
                .get("CommsSendResult")
                .expect("CommsSendResult schema must be emitted"),
            &serde_json::json!({
                "kind": "input_accepted",
                "interaction_id": uuid::Uuid::nil().to_string(),
                "stream_reserved": true
            }),
        );
        assert_schema_accepts(
            wire_types
                .get("CommsPeersResult")
                .expect("CommsPeersResult schema must be emitted"),
            &serde_json::json!({ "peers": [] }),
        );

        fs::remove_dir_all(&output_dir).unwrap();
    }

    #[test]
    fn emitted_mob_spawn_params_expose_advanced_fields_as_concrete_wire_schemas() {
        let output_dir = temp_output_dir("mob-spawn-advanced-slots");
        emit_all_schemas(&output_dir).expect("emit schemas");

        let params: serde_json::Value =
            serde_json::from_slice(&fs::read(output_dir.join("params.json")).unwrap()).unwrap();
        let spawn = params
            .get("MobSpawnParams")
            .expect("MobSpawnParams schema must be emitted");
        let properties = spawn
            .pointer("/properties")
            .and_then(serde_json::Value::as_object)
            .expect("MobSpawnParams schema must expose properties");

        for (field, expected_ref) in [
            ("launch_mode", "WireMemberLaunchMode"),
            ("tool_access_policy", "WireToolAccessPolicy"),
            ("budget_split_policy", "WireBudgetSplitPolicy"),
            ("inherited_tool_filter", "WireToolFilter"),
            ("override_profile", "WireMobProfile"),
        ] {
            let field_schema = properties
                .get(field)
                .unwrap_or_else(|| panic!("MobSpawnParams missing accepted field {field}"));
            let field_schema = serde_json::to_string(field_schema).unwrap();
            assert!(
                field_schema.contains(&format!("#/$defs/{expected_ref}")),
                "MobSpawnParams.{field} must reference concrete {expected_ref} schema, got {field_schema}"
            );
        }
        assert_eq!(
            spawn.get("additionalProperties"),
            Some(&serde_json::Value::Bool(false)),
            "mob/spawn can be closed only when all accepted fields are in the typed schema"
        );

        fs::remove_dir_all(&output_dir).unwrap();
    }

    #[test]
    fn emitted_mob_spawn_override_profile_omits_internal_tool_bundles() {
        let output_dir = temp_output_dir("mob-spawn-public-profile");
        emit_all_schemas(&output_dir).expect("emit schemas");

        let params: serde_json::Value =
            serde_json::from_slice(&fs::read(output_dir.join("params.json")).unwrap()).unwrap();
        let spawn_profile = params
            .pointer("/MobSpawnParams/$defs/WireMobProfile")
            .expect("MobSpawnParams must define WireMobProfile");
        assert_eq!(
            spawn_profile.get("additionalProperties"),
            Some(&serde_json::Value::Bool(false)),
            "mob/spawn override_profile must fail closed on unknown profile fields"
        );
        let spawn_tool_config = params
            .pointer("/MobSpawnParams/$defs/WireMobToolConfig")
            .expect("MobSpawnParams must define WireMobToolConfig");
        assert_eq!(
            spawn_tool_config.get("additionalProperties"),
            Some(&serde_json::Value::Bool(false)),
            "mob/spawn override_profile.tools must fail closed on unknown tool fields"
        );
        assert!(
            spawn_tool_config
                .pointer("/properties/rust_bundles")
                .is_none(),
            "mob/spawn override_profile.tools must not expose internal rust_bundles"
        );

        let wire_types: serde_json::Value =
            serde_json::from_slice(&fs::read(output_dir.join("wire-types.json")).unwrap()).unwrap();
        let public_tool_config = wire_types
            .get("WireMobToolConfig")
            .expect("WireMobToolConfig schema must be emitted");
        assert!(
            public_tool_config
                .pointer("/properties/rust_bundles")
                .is_none(),
            "top-level WireMobToolConfig must not expose internal rust_bundles"
        );

        fs::remove_dir_all(&output_dir).unwrap();
    }

    #[test]
    fn emitted_mob_turn_start_params_expose_typed_prompt_and_known_overrides() {
        let output_dir = temp_output_dir("mob-turn-start-typed");
        emit_all_schemas(&output_dir).expect("emit schemas");

        let params: serde_json::Value =
            serde_json::from_slice(&fs::read(output_dir.join("params.json")).unwrap()).unwrap();
        let turn_start = params
            .get("MobTurnStartParams")
            .expect("MobTurnStartParams schema must be emitted");
        let properties = turn_start
            .pointer("/properties")
            .and_then(serde_json::Value::as_object)
            .expect("MobTurnStartParams schema must expose properties");
        assert_ne!(
            properties.get("prompt"),
            Some(&serde_json::Value::Bool(true)),
            "mob/turn_start prompt must use the canonical content input schema"
        );
        for field in [
            "skill_refs",
            "flow_tool_overlay",
            "additional_instructions",
            "keep_alive",
            "model",
            "provider",
            "max_tokens",
            "system_prompt",
            "output_schema",
            "structured_output_retries",
            "provider_params",
            "clear_provider_params",
            "auth_binding",
            "clear_auth_binding",
        ] {
            assert!(
                properties.contains_key(field),
                "mob/turn_start params missing explicit turn override field {field}"
            );
        }
        assert_eq!(
            turn_start.get("additionalProperties"),
            Some(&serde_json::Value::Bool(false)),
            "mob/turn_start params must fail closed instead of accepting arbitrary flattened overrides"
        );

        fs::remove_dir_all(&output_dir).unwrap();
    }

    #[test]
    fn emitted_rest_openapi_contains_wire_contracts_not_only_paths() {
        let output_dir = temp_output_dir("rest-openapi-contracts");
        emit_all_schemas(&output_dir).expect("emit schemas");

        let rest_openapi: serde_json::Value =
            serde_json::from_slice(&fs::read(output_dir.join("rest-openapi.json")).unwrap())
                .unwrap();
        let components = rest_openapi
            .pointer("/components/schemas")
            .and_then(serde_json::Value::as_object)
            .expect("rest OpenAPI should publish schema components");
        for expected in [
            "RestCreateSessionRequest",
            "RestPeerResponseTerminalRequest",
            "WireRunResult",
            "WireError",
            "ConfigEnvelope",
        ] {
            assert!(
                components.contains_key(expected),
                "rest OpenAPI components missing {expected}"
            );
        }
        let terminal_request = components
            .get("RestPeerResponseTerminalRequest")
            .expect("rest OpenAPI components missing RestPeerResponseTerminalRequest");
        assert_eq!(
            terminal_request.get("additionalProperties"),
            Some(&serde_json::Value::Bool(false)),
            "terminal peer-response request must be closed like RestPeerResponseTerminalBody"
        );
        assert_eq!(
            terminal_request.pointer("/required"),
            Some(&serde_json::json!([
                "peer_id",
                "request_id",
                "status",
                "result"
            ])),
            "terminal peer-response request must require canonical identity and correlation facts"
        );
        let terminal_properties = terminal_request
            .pointer("/properties")
            .and_then(serde_json::Value::as_object)
            .expect("terminal peer-response request must expose object properties");
        assert!(
            terminal_properties.get("peer_name").is_none(),
            "terminal peer-response request must not admit display names as identity"
        );
        assert_eq!(
            terminal_properties
                .get("peer_id")
                .and_then(|schema| schema.pointer("/$ref"))
                .and_then(serde_json::Value::as_str),
            Some("#/components/schemas/PeerId")
        );
        assert_eq!(
            terminal_properties
                .get("display_name")
                .and_then(|schema| schema.pointer("/$ref"))
                .and_then(serde_json::Value::as_str),
            Some("#/components/schemas/PeerName")
        );
        assert_eq!(
            terminal_properties
                .get("request_id")
                .and_then(|schema| schema.pointer("/$ref"))
                .and_then(serde_json::Value::as_str),
            Some("#/components/schemas/PeerCorrelationId")
        );
        assert_eq!(
            terminal_properties
                .get("status")
                .and_then(|schema| schema.pointer("/$ref"))
                .and_then(serde_json::Value::as_str),
            Some("#/components/schemas/PeerResponseTerminalStatusWire")
        );

        let create_session = &rest_openapi["paths"]["/sessions"]["post"];
        assert_eq!(
            create_session
                .pointer("/requestBody/content/application~1json/schema/$ref")
                .and_then(serde_json::Value::as_str),
            Some("#/components/schemas/RestCreateSessionRequest")
        );
        assert_eq!(
            create_session
                .pointer("/responses/200/content/application~1json/schema/$ref")
                .and_then(serde_json::Value::as_str),
            Some("#/components/schemas/WireRunResult")
        );
        assert_eq!(
            create_session
                .pointer("/responses/default/content/application~1json/schema/$ref")
                .and_then(serde_json::Value::as_str),
            Some("#/components/schemas/WireError")
        );

        let get_session = &rest_openapi["paths"]["/sessions/{id}"]["get"];
        assert_eq!(
            get_session
                .pointer("/parameters/0/name")
                .and_then(serde_json::Value::as_str),
            Some("id")
        );
        assert_eq!(
            get_session
                .pointer("/responses/200/content/application~1json/schema/$ref")
                .and_then(serde_json::Value::as_str),
            Some("#/components/schemas/SessionDetailsResponse")
        );

        for descriptor in meerkat_workgraph::workgraph_rest_path_catalog() {
            for catalog_operation in descriptor.operations {
                let operation = &rest_openapi["paths"][descriptor.path][catalog_operation.method];
                let expected =
                    format!("#/components/schemas/{}", catalog_operation.response_schema);
                assert_eq!(
                    operation
                        .pointer("/responses/200/content/application~1json/schema/$ref")
                        .and_then(serde_json::Value::as_str),
                    Some(expected.as_str()),
                    "{} {} must be present in generated REST OpenAPI",
                    catalog_operation.method,
                    descriptor.path
                );
            }
        }

        for retired in [
            "/sessions/{id}/submit",
            "/sessions/{id}/retire",
            "/sessions/{id}/reset",
            "/sessions/{id}/submissions",
            "/sessions/{session_id}/submissions/{submission_id}",
        ] {
            assert!(
                rest_openapi["paths"].get(retired).is_none(),
                "retired REST runtime/session control mirror must not be emitted: {retired}"
            );
        }
        let components_json = serde_json::to_string(components).unwrap();
        for retired_schema in [
            "RuntimeAcceptParams",
            "RuntimeAcceptResult",
            "RuntimeAcceptOutcomeType",
            "RuntimeRetireParams",
            "RuntimeResetParams",
            "RuntimeRetireResult",
            "RuntimeResetResult",
            "InputStateParams",
            "InputListParams",
            "InputStateResult",
            "InputListResult",
            "WireInputLifecycleState",
            "WireInputState",
            "WireInputStateHistoryEntry",
        ] {
            assert!(
                !components_json.contains(retired_schema),
                "old REST runtime/session control schema must not be carried by OpenAPI: {retired_schema}"
            );
        }

        for (path, request_schema) in [
            ("/sessions/{id}/mcp/add", "McpAddParams"),
            ("/sessions/{id}/mcp/remove", "McpRemoveParams"),
            ("/sessions/{id}/mcp/reload", "McpReloadParams"),
        ] {
            let operation = &rest_openapi["paths"][path]["post"];
            let expected_request = format!("#/components/schemas/{request_schema}");
            assert_eq!(
                operation
                    .pointer("/requestBody/content/application~1json/schema/$ref")
                    .and_then(serde_json::Value::as_str),
                Some(expected_request.as_str()),
                "{path} request body contract drifted"
            );
            assert_eq!(
                operation
                    .pointer("/responses/200/content/application~1json/schema/$ref")
                    .and_then(serde_json::Value::as_str),
                Some("#/components/schemas/McpLiveOpResponse"),
                "{path} response contract drifted"
            );
        }

        let wait_kickoff = &rest_openapi["paths"]["/mob/{id}/wait-kickoff"]["post"];
        assert_eq!(
            wait_kickoff
                .pointer("/requestBody/content/application~1json/schema/$ref")
                .and_then(serde_json::Value::as_str),
            Some("#/components/schemas/RestMobWaitRequest")
        );
        assert_eq!(
            wait_kickoff
                .pointer("/requestBody/required")
                .and_then(serde_json::Value::as_bool),
            Some(false)
        );
        let wire_members_batch = &rest_openapi["paths"]["/mob/{id}/wire-members-batch"]["post"];
        assert_eq!(
            wire_members_batch
                .pointer("/requestBody/content/application~1json/schema/$ref")
                .and_then(serde_json::Value::as_str),
            Some("#/components/schemas/RestMobWireMembersBatchRequest")
        );
        assert_eq!(
            wire_members_batch
                .pointer("/responses/200/content/application~1json/schema/$ref")
                .and_then(serde_json::Value::as_str),
            Some("#/components/schemas/MobWireMembersBatchResult")
        );
        assert_eq!(
            wait_kickoff
                .pointer("/responses/200/content/application~1json/schema/$ref")
                .and_then(serde_json::Value::as_str),
            Some("#/components/schemas/JsonValue")
        );

        let body = serde_json::to_string(&rest_openapi).unwrap();
        assert!(
            !body.contains("#/$defs/"),
            "OpenAPI component refs must resolve through #/components/schemas"
        );
        fn collect_openapi_component_refs(
            value: &serde_json::Value,
            refs: &mut std::collections::BTreeSet<String>,
        ) {
            match value {
                serde_json::Value::Object(object) => {
                    if let Some(serde_json::Value::String(reference)) = object.get("$ref")
                        && let Some(name) = reference.strip_prefix("#/components/schemas/")
                    {
                        refs.insert(name.to_string());
                    }
                    for nested in object.values() {
                        collect_openapi_component_refs(nested, refs);
                    }
                }
                serde_json::Value::Array(values) => {
                    for nested in values {
                        collect_openapi_component_refs(nested, refs);
                    }
                }
                _ => {}
            }
        }
        let mut refs = std::collections::BTreeSet::new();
        collect_openapi_component_refs(&rest_openapi, &mut refs);
        for reference in refs {
            assert!(
                components.contains_key(&reference),
                "OpenAPI component ref must resolve: {reference}"
            );
        }

        fs::remove_dir_all(&output_dir).unwrap();
    }

    #[test]
    fn emitted_mob_rpc_contract_names_resolve_to_exported_schemas() {
        let output_dir = temp_output_dir("typed-mob-rpc-catalog-resolution");
        emit_all_schemas(&output_dir).expect("emit schemas");

        let exported_contracts = ["params.json", "wire-types.json"]
            .into_iter()
            .flat_map(|file| {
                let value: serde_json::Value =
                    serde_json::from_slice(&fs::read(output_dir.join(file)).unwrap()).unwrap();
                value
                    .as_object()
                    .expect("schema artifact is an object")
                    .keys()
                    .cloned()
                    .collect::<Vec<_>>()
            })
            .collect::<std::collections::BTreeSet<_>>();
        let rpc_methods: serde_json::Value =
            serde_json::from_slice(&fs::read(output_dir.join("rpc-methods.json")).unwrap())
                .unwrap();

        for method in rpc_methods["methods"].as_array().expect("methods array") {
            let Some(name) = method["name"].as_str() else {
                continue;
            };
            if !name.starts_with("mob/") {
                continue;
            }
            for field in ["params_type", "result_type"] {
                let Some(contract_name) = method.get(field).and_then(serde_json::Value::as_str)
                else {
                    continue;
                };
                assert!(
                    exported_contracts.contains(contract_name),
                    "{name} advertises {field}={contract_name}, but no emitted schema exports that contract"
                );
            }
        }

        fs::remove_dir_all(&output_dir).unwrap();
    }

    #[test]
    fn emitted_auth_rpc_contract_names_resolve_to_exported_schemas() {
        let output_dir = temp_output_dir("typed-auth-rpc-catalog-resolution");
        emit_all_schemas(&output_dir).expect("emit schemas");

        let exported_contracts = ["params.json", "wire-types.json"]
            .into_iter()
            .flat_map(|file| {
                let value: serde_json::Value =
                    serde_json::from_slice(&fs::read(output_dir.join(file)).unwrap()).unwrap();
                value
                    .as_object()
                    .expect("schema artifact is an object")
                    .keys()
                    .cloned()
                    .collect::<Vec<_>>()
            })
            .collect::<std::collections::BTreeSet<_>>();
        let rpc_methods: serde_json::Value =
            serde_json::from_slice(&fs::read(output_dir.join("rpc-methods.json")).unwrap())
                .unwrap();

        for method in rpc_methods["methods"].as_array().expect("methods array") {
            let Some(name) = method["name"].as_str() else {
                continue;
            };
            if !(name.starts_with("auth/") || name.starts_with("realm/")) {
                continue;
            }
            for field in ["params_type", "result_type"] {
                let Some(contract_name) = method.get(field).and_then(serde_json::Value::as_str)
                else {
                    continue;
                };
                assert!(
                    exported_contracts.contains(contract_name),
                    "{name} advertises {field}={contract_name}, but no emitted schema exports that contract"
                );
            }
        }

        fs::remove_dir_all(&output_dir).unwrap();
    }
}
