"""Non-persisted P2HS invoke-capability catalog.

Invariant (do not collapse these):

    capability declared  ≠  capability invocable  ≠  connector action implements it

``invocable: false`` means the verb is valid for planning, composition, requirement
declaration, and future binding. Codegen must not emit a runtime call to it.

``implements`` on a connector action is catalog truth for ``implemented_by``.
When that capability is the Codegen default (``status: implemented``), the
action is also demoted to ``provider_actions`` / escape hatch.

``required_capabilities`` remains an authorization/entitlement list
(e.g. integrations.execute). It is not this catalog.

Manifest convention (all connectors, not email-only):

    connectors.capabilities        default Codegen surface (check invocable)
    connectors.provider_actions    advanced / provider-specific surface (not a compatibility bucket)

A capability becomes ``role: default`` only after the full chain exists:
contract → resolver → invoke path → provider ``implements`` → Codegen exposure.
``implemented_by`` is always ``slugs_implementing()``, never a second list.
"""
from __future__ import annotations

from typing import Any, Literal, TypedDict

from src.domain.connectors.resource_contracts import resource_specs

CapabilityStatus = Literal["implemented", "contract"]


class InvokeCapabilitySpec(TypedDict, total=False):
    id: str
    status: CapabilityStatus
    invocable: bool
    request_type: str | None
    method: str | None
    runtime: str
    codegen: str
    input_schema: dict[str, Any]
    output_schema: dict[str, Any]
    errors: tuple[str, ...]
    adapter_mappings: dict[str, dict[str, Any]]


# Locked: two conversion templates. Email = provider-backed first; messaging = contract-first.
CONVERSION_REFERENCE = {
    "stable_verb": "email.send",
    "implementing_actions": (
        "resend.send_email",
        "mailgun.send_email",
        "ses.send_email",
    ),
    "required_capabilities_change": False,
    "codegen_sees_vendor_actions": "escape_hatch_only",
    "generic_codegen_target": "capability",
    "provider_selection": "runtime_resolver",
    "dashboard_vendor_invoke": True,
    "provider_switch_regenerates_code": False,
}

CAPABILITY_CONVERSION_PATTERN: dict[str, Any] = {
    "implemented_by": "slugs_implementing",
    "provider_actions_role": "advanced_provider_specific",
    "role_default_requires": (
        "contract",
        "resolver",
        "invoke_path",
        "provider_implements",
        "codegen_exposure",
    ),
    "templates": {
        "email.send": "provider_backed_then_codegen_normalize",
        "messaging.send": "contract_then_resolver_invoke_implements_codegen",
        "messaging.sms.send": "provider_backed_then_codegen_normalize",
        "automation.workflow.trigger": "provider_backed_then_codegen_normalize",
        "document.create": "provider_backed_then_codegen_normalize",
        "document.get": "provider_backed_then_codegen_normalize",
        "document.insert_text": "provider_backed_then_codegen_normalize",
        "document.append_text": "provider_backed_then_codegen_normalize",
        "document.replace_text": "provider_backed_then_codegen_normalize",
        "crm.contact.create": "provider_backed_then_codegen_normalize",
        "crm.contact.get": "provider_backed_then_codegen_normalize",
        "crm.contact.update": "provider_backed_then_codegen_normalize",
        "crm.contact.upsert": "provider_backed_then_codegen_normalize",
        "crm.contact.search": "provider_backed_then_codegen_normalize",
        "crm.company.create": "provider_backed_then_codegen_normalize",
        "crm.company.get": "provider_backed_then_codegen_normalize",
        "crm.company.update": "provider_backed_then_codegen_normalize",
        "crm.company.search": "provider_backed_then_codegen_normalize",
        "crm.deal.create": "provider_backed_then_codegen_normalize",
        "crm.deal.get": "provider_backed_then_codegen_normalize",
        "crm.deal.update": "provider_backed_then_codegen_normalize",
        "crm.deal.search": "provider_backed_then_codegen_normalize",
        "crm.association.create": "provider_backed_then_codegen_normalize",
    },
    "decision_rule": {
        "A": (
            "Live provider/runtime path already exists → normalize behind a "
            "stable capability → then move Codegen to the capability"
        ),
        "B": (
            "Stable P2HS verb exists conceptually but no capability runtime exists → "
            "contract → resolver → invoke path → provider implements → Codegen exposure"
        ),
    },
    "opened_families": {
        "email.send": "A",
        "messaging.send": "B",
        "messaging.sms.send": "A",
        "payments.*": "A",
        "automation.workflow.trigger": "A",
        "document.*": "A",
        # Customer provider foundations and public invoke paths are implemented.
        "spreadsheet.*": "A",
        "storage.file.*": "split",
        # Product domain opened for inventory; verbs are not frozen until 6B/6C.
        "crm.*": "canonical_model",
        "crm.contact.*": "A",
        "crm.company.*": "A",
        "crm.deal.*": "A",
        "crm.association.*": "A",
        # Forms opened 7A; 7P composition binding (still no form.* invoke verbs).
        "forms.*": "composition_integration",
    },
    "closed_families": {"crm.*": "complete", "forms.*": "complete"},
    "next_candidates": ("email.templates",),
    "next_candidate_classification": {
        "storage.file.*": "split_A_and_B",
        "crm.*": "split_p2hs_records_and_customer_crm",
        "forms.*": "split_p2hs_form_product_and_customer_post_submit",
    },
    "public_api_boundary": "dashboard_bff",
    "core_is_public_integration_endpoint": False,
    "public_response_owner": "core",
    "bff_strip_role": "defense_in_depth",
    "phase_0_status": "complete",
    "phase_0a_status": "complete",
    "phase_1_status": "1c_complete_1d_zapier_blocked",
    "phase_1d_status": "blocked_until_catalog_and_cloud_path",
    "phase_2_status": "2g_codegen_defaults_complete",
    "phase_2a_status": "frozen",
    "phase_2p_status": "complete",
    "phase_2b_status": "frozen",
    "phase_2c_status": "complete",
    "phase_2d_status": "complete",
    "phase_2e_status": "complete",
    "phase_2f_status": "complete",
    "phase_2g_status": "complete",
    "phase_3_status": "complete",
    "phase_3a_status": "complete",
    "phase_3p_status": "complete",
    "phase_3b_status": "complete",
    "phase_4_status": "complete",
    "phase_5_status": "complete",
    "phase_6_status": "6p_complete",
    "phase_7_status": "complete",
    "forms_domain_status": "complete",
    "phase_8c_status": "complete",
    "phase_8d_status": "complete",
    "phase_8e_status": "complete",
    "phase_8f_status": "complete",
    "phase_8g_status": "complete",
    "phase_8h_status": "complete",
    "phase_8i_status": "complete",
    "phase_8j_status": "complete",
    "phase_8k_status": "complete",
    "next_program_slice": "9_app_block_modernization",
    "automation_codegen_policy": "progressive_after_first_implementation",
    "legacy_alignment": "email_slack_twilio_phase_0a",
    "bff_owns": (
        "session_authentication",
        "project_access",
        "edge_request_validation",
        "csrf_origin_rate_limit",
        "proxying",
        "normalized_public_http_response",
    ),
    "bff_must_not": (
        "capability_resolution",
        "provider_selection",
        "binding_semantics",
        "connector_permissions",
        "execution_planning",
        "provider_authorization_policy",
        "document_alias_resolution",
        "google_payload_normalization",
        "persist_create_binding",
        "stage_map_lookup",
        "pipeline_map_lookup",
        "soql_generation",
        "hubspot_filter_generation",
        "accountid_mapping",
        "association_type_selection",
        "provider_error_translation",
    ),
    "public_route_kinds": {
        "capabilities": "stable_product_runtime",
        "connectors": "provider_admin_advanced",
        "connector_catalog": "provider_admin_advanced",
        "workspace_connector_defaults": "provider_admin_advanced",
        "integrations": "provider_admin_advanced",
    },
    "parked_families": {},
    "current_program_phase": 8,
    "program_order": (
        "phase_0_public_bff_private_core",
        "phase_0a_legacy_capability_alignment",
        "automation.workflow.trigger",
        "document.*",
        "spreadsheet.*",
        "storage.file.*",
        "cloudflare_domains",
        "crm",
        "forms",
        "email.templates",
        "messaging.sms.send",
        "app_block_dependency_waves",
        "cross_catalog_validation",
        "version_api_resilience",
    ),
    "topology": (
        "generated_app_or_dashboard",
        "dashboard_bff_public",
        "core_private",
        "cloud_private_execution",
        "provider_adapter",
        "external_api",
    ),
    "five_surfaces": (
        "catalog",
        "core",
        "cloud",
        "dashboard_bff",
        "codegen",
    ),
    "deferred": (),
    "no_capability_state_until_family_opened": True,
    "operating_sequence": (
        "execution_domain_inventory",
        "open_family",
        "classify_A_B_or_split",
        "freeze_target_capability_contract",
        "run_matching_staged_conversion",
        "move_catalog_invoke_codegen_only_when_stage_is_true",
    ),
    "governing_invariant": (
        "generated app → stable P2HS capability contract → "
        "project-resolved binding/version → provider adapter → external API"
    ),
    "managed_vs_customer_invariant": (
        "Internal use of provider X does not make provider X a customer-facing "
        "connector, resolver candidate, implements entry, or Codegen surface. "
        "Adding provider X as a customer-facing connector does not mean P2HS "
        "internal services should resolve through the customer's connection."
    ),
    "parallel_execution_domain_invariant": (
        "P2HS-owned execution and customer-bound execution are independent "
        "domains. The existence, addition, removal, or selection of a customer "
        "connector must not replace, disable, redirect, or implicitly satisfy "
        "P2HS-owned infrastructure execution—and P2HS-owned provider "
        "infrastructure must not implicitly satisfy a customer-facing capability."
    ),
    "execution_domains_are_not_fallbacks": True,
    "no_parallel_execution_domain_registry": True,
}

# Architectural categories only. Do not add a second capability registry.
# Domain = ownership + resource lifecycle + authorization + purpose,
# not where the secret is stored.
EXECUTION_DOMAINS: dict[str, Any] = {
    "model": (
        "provider",
        "p2hs_owned_execution",
        "customer_bound_execution",
    ),
    "both_means": "independent_parallel_paths_not_choose_one",
    "defined_by": (
        "ownership",
        "resource_lifecycle",
        "authorization_authority",
        "operational_purpose",
    ),
    "not_defined_by": "where_the_secret_is_stored",
    "inventory_labels": ("P", "C", "B"),
    "inventory_label_meanings": {
        "P": "p2hs_owned_path_only",
        "C": "customer_bound_path_only",
        "B": "provider_has_both_independent_paths",
    },
    "p2hs_owned_execution": {
        "credentials_or_resources": "p2hs",
        "provider_visible_to_customer": False,
        "user_binds_provider": False,
        "resolver_may_inspect_customer_connections": False,
        "examples": (
            "generated_repo_github_pipeline",
            "platform_deployment",
            "p2hs_dns_subdomains",
            "p2hs_pages_workers",
            "managed_r2",
            "prompt_attachments",
            "voice_audio_assets",
            "project_specifications",
            "internal_generated_artifacts",
            "platform_email",
            "platform_ai",
            "internal_jobs_workflows",
            "p2hs_telemetry",
        ),
    },
    "customer_bound_execution": {
        "credentials_or_resources": "customer_or_project",
        "provider_visible_to_customer": False,
        "user_binds_provider": True,
        "resolver_may_inspect_p2hs_owned_resources": False,
        "examples": (
            "email.send",
            "messaging.send",
            "automation.workflow.trigger",
            "storage.file.*",
            "document.*",
        ),
    },
    "provider_action": {
        "credentials_or_resources": "customer_or_project",
        "provider_visible_to_customer": True,
        "user_binds_provider": True,
        "examples": (
            "cloudflare_r2.*",
            "amazon_s3.*",
            "google_docs.*",
            "slack.post_message",
        ),
    },
    "never": (
        "customer_connector_as_p2hs_owned_fallback",
        "p2hs_owned_infra_as_customer_capability_fallback",
        "customer_github_for_p2hs_repo_pipeline",
        "customer_cloudflare_for_p2hs_deployment",
        "customer_r2_absent_then_managed_r2_for_storage_file",
    ),
    "same_provider_both_independent": (
        ("github", "B"),
        ("cloudflare", "B"),
        ("storage_r2", "B"),
        ("google", "B"),
        ("email", "B"),
        ("ai", "B"),
        ("automation", "B"),
        ("observability", "B"),
    ),
    "same_provider_different_domain": (
        ("managed_r2", "customer_cloudflare_r2"),
        ("platform_email", "email.send"),
        ("platform_ai", "customer_openai"),
        ("p2hs_github_pipeline", "customer_github"),
        ("p2hs_cloudflare_infra", "customer_cloudflare"),
        ("internal_jobs", "customer_make_n8n"),
    ),
    "managed_r2_does_not_implement_storage_file": True,
    "customer_r2_does_not_replace_managed_r2": True,
    "family_open_step_0": (
        "label_each_existing_provider_path_P_C_or_B",
        "preserve_p2hs_owned_paths",
        "audit_customer_bound_paths_separately",
        "classify_customer_facing_A_B_or_split",
        "freeze_customer_facing_contract",
        "resolver_inspects_only_customer_bound_resources",
        "invoke_implements_follow_runtime_truth",
        "codegen_sees_stable_customer_facing_capability",
        "never_expose_p2hs_owned_provider_topology",
    ),
}

# Phase 4 is planned, not opened. Verbs are candidates, not frozen.
STORAGE_HYPOTHESIZED_VERBS_NOT_FROZEN: tuple[str, ...] = (
    "storage.file.put",
    "storage.file.get",
    "storage.file.delete",
    "storage.file.exists",
    "storage.file.list",
)

STORAGE_FAMILY_PLAN: dict[str, Any] = {
    "status": "complete",
    "family": "storage.file.*",
    "classification_when_opened": "split",
    "opened": True,
    "verbs_frozen": True,
    "hypothesized_verbs_not_frozen": STORAGE_HYPOTHESIZED_VERBS_NOT_FROZEN,
    "providers_when_opened": ("cloudflare_r2", "amazon_s3"),
    "byoc_r2_catalog_exists": True,
    "byoc_s3_catalog_exists": True,
    "managed_r2_in_scope": False,
    "customer_absent_does_not_use_managed_r2": True,
    "phase_4a_in_scope": (
        "cloudflare_r2_user_project_connection",
        "cloudflare_r2_byoc_credentials",
        "cloudflare_r2_catalog_actions",
        "cloudflare_r2_cloud_handlers",
        "amazon_s3_user_project_connection",
        "amazon_s3_byoc_credentials",
        "amazon_s3_catalog_actions",
        "amazon_s3_cloud_handlers",
    ),
    "phase_4a_out_of_scope": (
        "p2hs_managed_r2",
        "prompt_attachments",
        "user_uploaded_images",
        "voice_audio_assets",
        "project_specifications",
        "internal_generated_artifacts",
        "internal_platform_storage",
        "core_cloud_operational_storage",
    ),
    "slices": (
        "4a_external_storage_audit_r2_and_s3",
        "4b_freeze_only_portable_verbs",
        "4c_r2_implements_generic",
        "4d_s3_implements_generic",
        "4e_logical_storage_binding",
        "4f_public_invoke_and_codegen",
    ),
    "working_model": {
        "resource": "logical_storage_alias",
        "example_input": {"storage": "uploads", "key": "customers/123/invoice.pdf"},
        "not_in_generated_code": (
            "bucket",
            "bucket_name",
            "account_id",
            "region",
            "endpoint",
            "access_key_id",
            "secret_access_key",
            "r2_api_method",
            "s3_api_method",
        ),
        "unique_without_alias": "resource_unbound",
        "multiple": "binding_ambiguous; not first-wins",
        "provider_hint": "adapter-family filter only (cloudflare_r2 vs amazon_s3)",
        "r2_only": "provider_actions.cloudflare_r2.*",
        "s3_only": "provider_actions.amazon_s3.*",
        "managed_r2_is_not_an_implementation": True,
    },
    "later_product_decision": (
        "whether_p2hs_offers_managed_application_storage_as_storage_file_impl"
    ),
}

# Slice 5: classification only. No new verbs. Customer DNS/Turnstile/Workers/Rules
# are absent; do not infer 5a from vendor presence.
CLOUDFLARE_DOMAIN_AUDIT: dict[str, Any] = {
    "slice_id": "5_cloudflare_domain_audit",
    "status": "complete",
    "family_opened": False,
    "new_capability_verbs": (),
    "catalog_mutated": False,
    "phase_4_reopened": False,
    "vendor_overall": "B",
    "both_means": "independent_parallel_paths_not_choose_one",
    "customer_connector_keys": ("cloudflare_r2",),
    "absent_customer_connectors": (
        "cloudflare",
        "cloudflare_dns",
        "cloudflare_turnstile",
        "cloudflare_workers",
        "cloudflare_rules",
    ),
    "hypothesized_verbs_not_created": (
        "security.challenge.verify",
        "dns.record.create",
        "dns.record.update",
        "domain.create",
        "deployment.create",
    ),
    "customer_bound_live_actions": (
        "cloudflare_r2.list_buckets",
        "cloudflare_r2.create_bucket",
        "cloudflare_r2.list_objects",
        "cloudflare_r2.presign_put",
        "cloudflare_r2.presign_get",
        "cloudflare_r2.delete_object",
        "cloudflare_r2.import_from_url",
        "cloudflare_r2.extract_zip",
        "cloudflare_r2.head_object",
        "cloudflare_r2.bulk_export",
        "cloudflare_r2.bulk_export_bucket",
        "cloudflare_r2.bulk_delete",
        "cloudflare_r2.put_object",
        "cloudflare_r2.get_object",
    ),
    "storage_file_already_phase_4": (
        "storage.file.put",
        "storage.file.get",
        "storage.file.delete",
        "storage.file.exists",
        "storage.file.list",
    ),
    "provider_specific_r2_actions": (
        "cloudflare_r2.list_buckets",
        "cloudflare_r2.create_bucket",
        "cloudflare_r2.presign_put",
        "cloudflare_r2.presign_get",
        "cloudflare_r2.import_from_url",
        "cloudflare_r2.extract_zip",
        "cloudflare_r2.bulk_export",
        "cloudflare_r2.bulk_export_bucket",
        "cloudflare_r2.bulk_delete",
    ),
    "normalization_candidates_this_slice": (),
    "turnstile_customer_e2e": False,
    "dns_customer_e2e": False,
    "workers_customer_e2e": False,
    "rules_customer_state": "absent",
    "app_block_cloudflare_dependencies": (),
    "feature_pack_cloudflare_dependencies": (),
    "starter_cloudflare_dependencies": (),
    "recommended_next_slice": "6_crm_domain_audit",
    "not_recommended": (
        "5a_turnstile_contract",
        "5a_security_challenge_normalization",
        "5a_dns_record_normalization",
    ),
    "docs": "docs/audits/CLOUDFLARE_DOMAIN_5.md",
}

# Phase 6: first-class CRM product domain. 6A is inventory only.
# Do not treat hypothesized verbs as catalog facts until 6B/6C freeze them.
CRM_CONTACT_FROZEN_VERBS: tuple[str, ...] = (
    "crm.contact.create",
    "crm.contact.get",
    "crm.contact.update",
    "crm.contact.upsert",
    "crm.contact.search",
)

CRM_CONTACT_OUT_OF_SLICE: tuple[str, ...] = (
    "crm.contact.delete",
    "crm.contact.list",
    "crm.contact.batch_create",
    "crm.contact.merge",
    "crm.contact.associate",
    "crm.contact.owner.assign",
)

CRM_HYPOTHESIZED_VERBS_NOT_FROZEN: tuple[str, ...] = (
    "crm.company.upsert",
    "crm.deal.upsert",
    "crm.note.create",
    "crm.task.create",
    "crm.activity.create",
    "crm.search",
    "crm.pipeline.get",
)

CRM_OBJECT_VOCABULARY_CANDIDATES: tuple[str, ...] = (
    "CRMContact",
    "CRMCompany",
    "CRMDeal",
    "CRMNote",
    "CRMTask",
    "CRMActivity",
    "CRMObjectRef",
    "CRMAssociation",
    "CRMPropertyMap",
)

CRM_FAMILY_PLAN: dict[str, Any] = {
    "status": "6p_complete",
    "family": "crm.*",
    "product_domain": True,
    "not_merely_connector_family": True,
    "classification_when_opened": "split",
    "opened": True,
    "verbs_frozen": False,
    "contact_verbs_frozen": True,
    "company_verbs_frozen": True,
    "company_upsert": "PROVIDER_SPECIFIC_FOR_NOW",
    "company_get": True,
    "company_search": "name_exact",
    "company_search_domain": "REJECT",
    "deal_verbs_frozen": True,
    "deal_upsert": "PROVIDER_SPECIFIC_FOR_NOW",
    "deal_get": True,
    "deal_search": "name_or_stage_exact",
    "deal_search_pipeline": "REJECT",
    "deal_search_stage": "stage_exact_logical_alias",
    "stage_binding": "logical_connection_scoped",
    "pipeline_binding": "hubspot_only",
    "salesforce_pipeline": "REJECT",
    "generic_crm_search": "REJECT",
    "note_decision": "DEFER",
    "task_decision": "PROVIDER_SPECIFIC_FOR_NOW",
    "activity_decision": "REJECT_UMBRELLA",
    "activity_verbs_frozen": False,
    "association_verbs_frozen": True,
    "association_contact_deal": "PROVIDER_SPECIFIC_FOR_NOW",
    "object_model_frozen": True,
    "design_rule": (
        "normalize_the_70_to_80_percent_apps_use; "
        "keep_provider_depth_in_provider_actions; "
        "custom_properties_are_part_of_the_generic_model"
    ),
    "isolation": {
        "p2hs_record_crm_is_not_customer_hubspot": True,
        "p2hs_lead_entity_is_not_salesforce_lead": True,
        "salesforce_lead_is_not_crm_contact": True,
        "hubspot_lifecycle_is_not_salesforce_lead": True,
        "missing_customer_crm_does_not_use_p2hs_records": True,
        "p2hs_lead_pipeline_does_not_use_customer_crm_connection": True,
    },
    "inbound_is_not_outbound": True,
    "slices": (
        "6a_crm_domain_audit",
        "6b_crm_canonical_object_model",
        "6c_contact_capabilities",
        "6d_company_account_capabilities",
        "6e_deal_opportunity_capabilities",
        "6f_activities_notes_tasks",
        "6g_associations",
        "6h_search_read_query_normalization",
        "6i_pipeline_stage_resource_bindings",
        "6j_crm_inbound_webhook_event_normalization",
        "6k_hubspot_implementation_completeness",
        "6l_salesforce_implementation_completeness",
        "6m_public_bff_runtime_truth",
        "6n_codegen_exposure",
        "6o_app_block_feature_pack_integration",
        "6p_end_to_end_crm_application_tests",
    ),
    "hypothesized_verbs_not_frozen": CRM_HYPOTHESIZED_VERBS_NOT_FROZEN,
    "object_vocabulary_candidates": CRM_OBJECT_VOCABULARY_CANDIDATES,
    "canonical_model": "src.domain.connectors.crm_canonical",
    "contact_runtime": "src.domain.connectors.crm_contact",
    "company_runtime": "src.domain.connectors.crm_company",
    "deal_runtime": "src.domain.connectors.crm_deal",
    "association_runtime": "src.domain.connectors.crm_association",
    "inbound_events_runtime": "src.domain.connectors.crm_events",
    "resolver": "src.domain.connectors.crm_resolve",
    "inbound_events_frozen": True,
    "inbound_dispatch_to_project_workflow": True,
    "next_slice": "forms_first_class_product_domain",
}

CRM_CONTACT_PHASE_6C: dict[str, Any] = {
    "status": "complete",
    "slice": "6c_contact_capabilities",
    "family": "crm.contact.*",
    "execution_domain": "C",
    "eligible_providers": ("hubspot", "salesforce"),
    "salesforce_lead_is_not_crm_contact": True,
    "p2hs_records_are_not_resolver_candidates": True,
    "verbs": CRM_CONTACT_FROZEN_VERBS,
    "out_of_slice": CRM_CONTACT_OUT_OF_SLICE,
    "upsert_match_by": "email",
    "search_predicates": ("email",),
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "public_bff_capability_route": False,
    "core_private_invoke_http": True,
    "enqueue": "integration.execute",
    "resolver_eligibility_is_not_execution_authorization": True,
    "hubspot_null_clear": "empty_string_crm_v3",
    "next_slice": "6d_company_account_capabilities",
    "docs": "docs/audits/CRM_CONTACT_6C.md",
}

CRM_COMPANY_FROZEN_VERBS: tuple[str, ...] = (
    "crm.company.create",
    "crm.company.get",
    "crm.company.update",
    "crm.company.search",
)
CRM_COMPANY_OUT_OF_SLICE: tuple[str, ...] = (
    "crm.company.upsert",
    "crm.company.delete",
    "crm.company.list",
)
CRM_COMPANY_PHASE_6D: dict[str, Any] = {
    "status": "complete",
    "slice": "6d_company_account_capabilities",
    "family": "crm.company.*",
    "execution_domain": "C",
    "eligible_providers": ("hubspot", "salesforce"),
    "hubspot_object": "company",
    "salesforce_object": "Account",
    "salesforce_website_is_not_domain": True,
    "verbs": ("crm.company.create", "crm.company.update"),
    "out_of_slice": (
        "crm.company.get",
        "crm.company.upsert",
        "crm.company.search",
        "crm.company.delete",
        "crm.company.list",
    ),
    "upsert_classification": "PROVIDER_SPECIFIC_FOR_NOW",
    "upsert_reason": (
        "HubSpot upserts by canonical domain; Salesforce upserts by Website, "
        "Name, or a provider external id. Those are not the same identity."
    ),
    "get_search": "missing_on_both_providers",
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "public_bff_capability_route": False,
    "core_private_invoke_http": True,
    "enqueue": "integration.execute",
    "resolver": "src.domain.connectors.crm_resolve",
    "next_slice": "6e_deal_opportunity_capabilities",
    "docs": "docs/audits/CRM_COMPANY_6D.md",
}

CRM_DEAL_FROZEN_VERBS: tuple[str, ...] = (
    "crm.deal.create",
    "crm.deal.get",
    "crm.deal.update",
    "crm.deal.search",
)
CRM_DEAL_OUT_OF_SLICE: tuple[str, ...] = (
    "crm.deal.upsert",
    "crm.deal.delete",
    "crm.deal.list",
)
CRM_DEAL_PHASE_6E: dict[str, Any] = {
    "status": "complete",
    "slice": "6e_deal_opportunity_capabilities",
    "family": "crm.deal.*",
    "execution_domain": "C",
    "eligible_providers": ("hubspot", "salesforce"),
    "hubspot_object": "deal",
    "salesforce_object": "Opportunity",
    "salesforce_pipeline_is_not_record_type": True,
    "no_canonical_currency": True,
    "stage_values_not_portable": True,
    "verbs": ("crm.deal.create", "crm.deal.update"),
    "out_of_slice": (
        "crm.deal.get",
        "crm.deal.upsert",
        "crm.deal.search",
        "crm.deal.delete",
        "crm.deal.list",
    ),
    "upsert_classification": "PROVIDER_SPECIFIC_FOR_NOW",
    "upsert_reason": (
        "HubSpot upsert_deal requires deal_id / hs_object_id. Salesforce "
        "upsert_opportunity requires a configured Opportunity external-id field. "
        "Those are not the same identity. Name, amount, and stage are not unique keys."
    ),
    "get_search": "missing_on_both_providers",
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "public_bff_capability_route": False,
    "core_private_invoke_http": True,
    "enqueue": "integration.execute",
    "resolver": "src.domain.connectors.crm_resolve",
    "next_slice": "6f_crm_activities_notes_tasks",
    "docs": "docs/audits/CRM_DEAL_6E.md",
}

CRM_ACTIVITIES_PHASE_6F: dict[str, Any] = {
    "status": "complete",
    "slice": "6f_crm_activities_notes_tasks",
    "execution_domain": "C",
    "new_capability_verbs": (),
    "canonical_types_frozen": (),
    "object_ref_amended": False,
    "object_types_remain": ("contact", "company", "deal"),
    "crm_property_map_on_note_task": False,
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "public_bff_capability_route": False,
    "core_private_invoke_http": False,
    "generic_sobject_does_not_create_canonical": True,
    "association_boundary": "6g",
    "decisions": {
        "CRMNote": "DEFER",
        "CRMTask": "PROVIDER_SPECIFIC_FOR_NOW",
        "CRMActivity": "REJECT_UMBRELLA",
    },
    "decision_reasons": {
        "CRMNote": (
            "HubSpot catalog has no note CRUD; notes appear only as associate_objects "
            "enum values. Salesforce has no Note/ContentNote action. "
            "upsert_record/query_soql do not invent crm.note.*."
        ),
        "CRMTask": (
            "salesforce.create_task is a live Task SObject create with a fields bag. "
            "HubSpot has no create_task; tasks appear only in associate_objects enums. "
            "Those are not the same contract."
        ),
        "CRMActivity": (
            "Activity is an umbrella (note/task/call/meeting/email/event/timeline). "
            "salesforce.create_event is Event, not a generic activity. HubSpot has no "
            "engagements/calls/meetings/emails actions. Reject the umbrella."
        ),
    },
    "hubspot_note_actions": (),
    "hubspot_task_actions": (),
    "hubspot_activity_actions": (),
    "hubspot_association_object_types": (
        "contacts",
        "companies",
        "deals",
        "tickets",
        "notes",
        "tasks",
    ),
    "salesforce_note_actions": (),
    "salesforce_task_actions": ("salesforce.create_task",),
    "salesforce_event_actions": ("salesforce.create_event",),
    "salesforce_objects_not_actions": (
        "Note",
        "ContentNote",
        "EmailMessage",
        "ActivityHistory",
        "OpenActivity",
        "FeedItem",
    ),
    "provider_actions_remain": (
        "salesforce.create_task",
        "salesforce.create_event",
        "hubspot.associate_objects",
        "salesforce.associate_record",
        "salesforce.upsert_record",
        "salesforce.query_soql",
    ),
    "hypothesized_verbs_remain": (
        "crm.note.create",
        "crm.task.create",
        "crm.activity.create",
    ),
    "next_slice": "6g_crm_associations",
    "docs": "docs/audits/CRM_ACTIVITIES_6F.md",
}

CRM_ASSOCIATION_FROZEN_VERBS: tuple[str, ...] = ("crm.association.create",)
CRM_PUBLIC_INVOKE_VERBS: tuple[str, ...] = (
    *CRM_CONTACT_FROZEN_VERBS,
    *CRM_COMPANY_FROZEN_VERBS,
    *CRM_DEAL_FROZEN_VERBS,
    *CRM_ASSOCIATION_FROZEN_VERBS,
)
CRM_INVOKE_METHODS: dict[str, str] = {
    verb: f"POST /api/projects/{{project_id}}/capabilities/{verb}"
    for verb in CRM_PUBLIC_INVOKE_VERBS
}
CRM_PUBLIC_RUNTIME_PHASE_6M: dict[str, Any] = {
    "status": "complete",
    "slice": "6m_crm_public_bff_runtime_truth",
    "not_a_new_canonical_phase": True,
    "public_route": "POST /api/projects/{project_id}/capabilities/{capability}",
    "no_crm_dispatcher": True,
    "bff_is_proxy_only": True,
    "invoke_count": 14,
    "inbound_events_invocable": False,
    "catalog_invocable": True,
    "implements_attached": True,
    "implemented_by": "slugs_implementing",
    "role": "default",
    "codegen_default": False,
    "public_bff_capability_route": True,
    "core_private_invoke_http": True,
    "enqueue": "integration.execute",
    "salesforce_association": (
        "salesforce.update_contact",
        "salesforce.update_opportunity",
    ),
    "no_salesforce_associate_objects": True,
    "next_slice": "6n_crm_codegen_exposure",
    "docs": "docs/audits/CRM_PUBLIC_RUNTIME_6M.md",
}
CRM_CODEGEN_PHASE_6N: dict[str, Any] = {
    "status": "complete",
    "slice": "6n_crm_codegen_exposure",
    "not_a_new_canonical_phase": True,
    "runtime_unchanged": True,
    "catalog_invocable_unchanged": True,
    "implements_unchanged": True,
    "implemented_by": "slugs_implementing",
    "role_unchanged": True,
    "method_unchanged": True,
    "codegen_default": True,
    "consumes_catalog_metadata": True,
    "no_parallel_crm_codegen_registry": True,
    "inbound_events_unchanged": True,
    "app_blocks_unchanged": True,
    "feature_packs_unchanged": True,
    "generic_intent": "crm.*",
    "provider_actions": "explicit_semantics_only",
    "installed_connector_does_not_change_contract": True,
    "public_path": "POST /api/projects/{project_id}/capabilities/{capability}",
    "stage": "logical_aliases_only",
    "pipeline": "portability_aware_no_salesforce_fabrication",
    "search": "frozen_exact_canonical_subset",
    "associations": "frozen_6g_pairs_only",
    "next_slice": "6o_crm_app_block_feature_pack_integration",
    "docs": "docs/audits/CRM_CODEGEN_6N.md",
}
CRM_COMPOSITION_PHASE_6O: dict[str, Any] = {
    "status": "complete",
    "slice": "6o_crm_app_block_feature_pack_integration",
    "crm_lead": "local_p2hs_product",
    "forms_contact": "optional_crm_contact_upsert",
    "lead_lifecycle": "local_by_default_explicit_stage_sync",
    "crm_pipeline_alias": "KEEP_AS_LOCAL",
    "crm_external_sync": "composition_marker_expands_to_crm_contact_upsert",
    "generic_blocks_provider_neutral": True,
    "provider_selection_never_by_blocks": True,
    "capability_dependencies_from_catalog": True,
    "local_id_not_external_object_ref": True,
    "object_ref_persistence": "GAP_DOCUMENTED",
    "crm_failure_isolated": True,
    "inbound_events": "DEFER",
    "codegen_composes_blocks_plus_crm": True,
    "no_parallel_readiness_flags": True,
    "next_slice": "6p_crm_end_to_end_application_tests",
    "docs": "docs/audits/CRM_COMPOSITION_6O.md",
}
CRM_END_TO_END_PHASE_6P: dict[str, Any] = {
    "status": "complete",
    "slice": "6p_crm_end_to_end_application_tests",
    "no_new_verbs": True,
    "no_new_packs_or_blocks": True,
    "durable_object_ref": "DEFERRED",
    "bidirectional_mirror": False,
    "inbound_block_composition": "DEFER",
    "legacy_crm_sync_function": "not_used_by_new_composition",
    "cross_provider_portability": True,
    "next_slice": "forms_first_class_product_domain",
    "docs": "docs/audits/CRM_END_TO_END_6P.md",
}
CRM_ASSOCIATION_OUT_OF_SLICE: tuple[str, ...] = (
    "crm.association.delete",
    "crm.association.get",
    "crm.association.list",
)
CRM_ASSOCIATION_PHASE_6G: dict[str, Any] = {
    "status": "complete",
    "slice": "6g_crm_associations",
    "family": "crm.association.*",
    "execution_domain": "C",
    "eligible_providers": ("hubspot", "salesforce"),
    "verbs": CRM_ASSOCIATION_FROZEN_VERBS,
    "out_of_slice": CRM_ASSOCIATION_OUT_OF_SLICE,
    "supported_pairs": (
        "contact->company",
        "deal->company",
    ),
    "canonical_directions_only": True,
    "contact_deal": "PROVIDER_SPECIFIC_FOR_NOW",
    "contact_deal_reason": (
        "HubSpot can associate contacts and deals. Salesforce uses "
        "OpportunityContactRole with role/primary semantics, not a bare link."
    ),
    "cardinality": "primary_company_relationship",
    "create_means": "ensure_relationship_exists",
    "salesforce_account_id_replaces_primary": True,
    "hubspot_association_is_additive": True,
    "no_cross_provider": True,
    "object_types": ("contact", "company", "deal"),
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "public_bff_capability_route": False,
    "core_private_invoke_http": True,
    "enqueue": "integration.execute",
    "resolver": "src.domain.connectors.crm_resolve",
    "next_slice": "6h_crm_search_read_query_normalization",
    "docs": "docs/audits/CRM_ASSOCIATIONS_6G.md",
}

CRM_SEARCH_READ_PHASE_6H: dict[str, Any] = {
    "status": "complete",
    "slice": "6h_crm_search_read_query_normalization",
    "execution_domain": "C",
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "public_bff_capability_route": False,
    "core_private_invoke_http": True,
    "enqueue": "integration.execute",
    "resolver": "src.domain.connectors.crm_resolve",
    "search_match": "exact_equality",
    "cursor": "opaque_hubspot_after_only",
    "salesforce_search_cursor": False,
    "client_field_projection": False,
    "generic_crm_search": "REJECT",
    "company_search_domain": "REJECT",
    "deal_search_pipeline": "REJECT",
    "deal_search_stage": "DEFER_TO_6I",
    "foundations_added": (
        "hubspot.get_company",
        "hubspot.search_companies",
        "hubspot.get_deal",
        "hubspot.search_deals",
        "salesforce.get_account",
        "salesforce.search_accounts",
        "salesforce.get_opportunity",
        "salesforce.search_opportunities",
    ),
    "not_used_as_canonical": (
        "salesforce.query_soql",
        "salesforce.upsert_record",
        "hubspot.search filter_groups on public request",
    ),
    "matrix": {
        "crm.company.get": "FREEZE",
        "crm.company.search(name)": "FREEZE",
        "crm.company.search(domain)": "REJECT",
        "crm.deal.get": "FREEZE",
        "crm.deal.search(name)": "FREEZE",
        "crm.deal.search(stage)": "DEFER_TO_6I",
        "crm.search": "REJECT",
    },
    "next_slice": "6i_crm_pipeline_stage_resource_bindings",
    "docs": "docs/audits/CRM_SEARCH_READ_6H.md",
}

CRM_PIPELINE_STAGE_PHASE_6I: dict[str, Any] = {
    "status": "complete",
    "slice": "6i_crm_pipeline_stage_resource_bindings",
    "contract_evolution": "option_a_logical_aliases",
    "storage": "connector_connection.config_json",
    "no_new_persistence": True,
    "stage_binding": "FREEZE",
    "pipeline_binding": "hubspot_only",
    "salesforce_pipeline": "REJECT",
    "salesforce_pipeline_not_record_type": True,
    "raw_provider_fallback": False,
    "unknown_read_stage": "omit",
    "bidirectional": True,
    "search_stage": "exact_logical_alias",
    "search_name_and_stage": "and_exact",
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "public_bff_capability_route": False,
    "core_private_invoke_http": True,
    "discovery_actions": "admin_config_not_runtime_capabilities",
    "stage_pipeline_membership": "provider_authoritative",
    "next_slice": "6j_crm_inbound_webhook_event_normalization",
    "docs": "docs/audits/CRM_PIPELINE_STAGE_6I.md",
}

CRM_INBOUND_EVENTS_PHASE_6J: dict[str, Any] = {
    "status": "complete",
    "slice": "6j_crm_inbound_webhook_event_normalization",
    "plane": "event_not_invoke",
    "envelope": "object_ref_plus_changed_fields",
    "hydrate_via_get": False,
    "raw_fallback": False,
    "bidirectional_stage": True,
    "unknown_stage": "omit",
    "hubspot_transport": "webhook_v3",
    "salesforce_transport": "cdc_not_hubspot_webhook",
    "dedupe": "provider_event_id_connection_scoped",
    "loop_protection": "fingerprint_and_provider_origin",
    "delete": "DEFER",
    "association_changed": "DEFER",
    "generic_object_changed": "REJECT",
    "automation_workflow_trigger": False,
    "invoke_capabilities_rows": False,
    "catalog_invocable": False,
    "public_bff_capability_route": False,
    "dispatch": "canonical_event_then_project_outbox",
    "storage": "cloud_webhook_receipts_and_event_outbox",
    "next_slice": "6k_hubspot_implementation_completeness",
    "docs": "docs/audits/CRM_INBOUND_EVENTS_6J.md",
}

CRM_HUBSPOT_COMPLETENESS_PHASE_6K: dict[str, Any] = {
    "status": "complete",
    "slice": "6k_hubspot_implementation_completeness",
    "not_a_new_canonical_phase": True,
    "canonical_gap": "none_for_frozen_6c_6j",
    "null_clear": "empty_string_crm_v3",
    "create_email": "optional_provider_permits",
    "create_is_not_upsert": True,
    "oauth": "connect_refresh_revoke_reauthorize",
    "scope_missing": "reauthorization_required",
    "contacts": "complete",
    "companies": "complete",
    "deals": "complete",
    "stage_pipeline": "6i_semantics",
    "associations": "6g_frozen_pairs_only",
    "inbound_events": "six_6j_events",
    "advanced_hubspot": "provider_specific",
    "no_new_crm_verbs": True,
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "public_bff_capability_route": False,
    "next_slice": "6l_salesforce_implementation_completeness",
    "docs": "docs/audits/CRM_HUBSPOT_COMPLETENESS_6K.md",
}

CRM_SALESFORCE_COMPLETENESS_PHASE_6L: dict[str, Any] = {
    "status": "complete",
    "slice": "6l_salesforce_implementation_completeness",
    "not_a_new_canonical_phase": True,
    "canonical_gap": "none_for_frozen_6c_6j",
    "instance_url": "required_runtime_host",
    "login_hosts_are_oauth_only": True,
    "org_id": "inbound_attribution",
    "null_clear": "json_null",
    "upsert_email": "search_then_create_or_update_ambiguous_rejected",
    "external_id_upsert": "PROVIDER_SPECIFIC",
    "company_domain": "crm_field_unsupported",
    "pipeline": "REJECT",
    "soql": "PROVIDER_SPECIFIC",
    "lead_task_event_case": "PROVIDER_SPECIFIC",
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "public_bff_capability_route": False,
    "next_slice": "6m_crm_public_bff_runtime_truth",
    "docs": "docs/audits/CRM_SALESFORCE_COMPLETENESS_6L.md",
}

CRM_DOMAIN_AUDIT: dict[str, Any] = {
    "slice_id": "6a_crm_domain_audit",
    "status": "complete",
    "family_opened": True,
    "verbs_frozen": False,
    "catalog_mutated": False,
    "new_capability_verbs": (),
    "vendor_overall": "B",
    "customer_connector_keys": ("hubspot", "salesforce"),
    "other_crm_catalogs": (),
    "p2hs_record_entities": (
        "lead",
        "contact_submission",
        "crm_contact",
        "crm_deal",
        "case_record",
    ),
    "hubspot_live_actions": (
        "hubspot.create_contact",
        "hubspot.update_contact",
        "hubspot.upsert_contact",
        "hubspot.get_contact",
        "hubspot.search_contacts",
        "hubspot.create_company",
        "hubspot.update_company",
        "hubspot.upsert_company",
        "hubspot.get_company",
        "hubspot.search_companies",
        "hubspot.create_deal",
        "hubspot.update_deal",
        "hubspot.upsert_deal",
        "hubspot.get_deal",
        "hubspot.search_deals",
        "hubspot.associate_objects",
    ),
    "salesforce_live_actions": (
        "salesforce.create_lead",
        "salesforce.update_lead",
        "salesforce.upsert_lead",
        "salesforce.get_lead",
        "salesforce.search_leads",
        "salesforce.create_contact",
        "salesforce.update_contact",
        "salesforce.upsert_contact",
        "salesforce.get_contact",
        "salesforce.search_contacts",
        "salesforce.create_account",
        "salesforce.update_account",
        "salesforce.get_account",
        "salesforce.search_accounts",
        "salesforce.upsert_account",
        "salesforce.create_opportunity",
        "salesforce.update_opportunity",
        "salesforce.get_opportunity",
        "salesforce.search_opportunities",
        "salesforce.upsert_opportunity",
        "salesforce.create_case",
        "salesforce.update_case",
        "salesforce.create_task",
        "salesforce.create_event",
        "salesforce.associate_record",
        "salesforce.upsert_record",
        "salesforce.query_soql",
    ),
    "implements_attached": False,
    "public_bff_crm_capabilities": (),
    "inbound_webhook_verify_and_normalize": True,
    "inbound_dispatch_to_project_workflow": True,
    "turnstile_irrelevant": True,
    "app_blocks_existing": ("crm.lead",),
    "feature_packs_existing": (
        "contact-capture",
        "lead-lifecycle",
        "crm-pipeline",
        "salesforce-order-sync",
        "case-lifecycle",
    ),
    "composition_gaps": (
        "crm.external_sync_is_pack_metadata_not_an_invoke_capability",
        "crm.pipeline_app_block_alias_points_at_lead-lifecycle_not_crm-pipeline",
        "forms.contact_optional_crm_sync_has_no_capability_runtime",
    ),
    "normalize_candidates_after_6b": (
        "crm.contact.*",
        "crm.company.*",
        "crm.deal.*",
        "crm.association.create",
    ),
    "keep_provider_specific": (
        "salesforce.lead.*",
        "salesforce.query_soql",
        "salesforce.upsert_record",
        "salesforce.create_case",
        "hubspot.search_contacts_filter_groups",
        "hubspot.associate_objects.association_type_id",
        "salesforce.associate_record.lookup_field",
    ),
    "incomplete_for_generic_until_both_providers": (
        "crm.note.*",
        "crm.task.*",
        "crm.activity.*",
        "crm.pipeline.*",
        "crm.search",
    ),
    "recommended_next_slice": "6b_crm_canonical_object_model",
    "docs": "docs/audits/CRM_DOMAIN_6A.md",
}

FORMS_OBJECT_VOCABULARY_CANDIDATES: tuple[str, ...] = (
    "FormDefinition",
    "FormField",
    "FormValidationRule",
    "FormSubmission",
    "FormAction",
    "FormStep",
    "FormCondition",
    "FormFileField",
    "FormSpamPolicy",
)

FORMS_HYPOTHESIZED_VERBS_NOT_FROZEN: tuple[str, ...] = (
    "form.submit",
    "form.create",
    "form.definition.get",
    "form.submission.get",
    "form.submission.list",
)

FORMS_FAMILY_PLAN: dict[str, Any] = {
    "status": "complete",
    "family": "forms.*",
    "product_domain": True,
    "not_a_connector": True,
    "classification_when_opened": "split",
    "opened": True,
    "verbs_frozen": False,
    "object_model_frozen": True,
    "validation_semantics_frozen": True,
    "runtime_acceptance_frozen": True,
    "persistence_contract_frozen": True,
    "file_runtime_frozen": True,
    "anti_abuse_frozen": True,
    "post_submit_frozen": True,
    "crm_composition_frozen": True,
    "email_composition_frozen": True,
    "automation_webhook_composition_frozen": True,
    "conditional_logic_frozen": True,
    "multi_step_frozen": True,
    "public_runtime_frozen": True,
    "codegen_exposure_frozen": True,
    "composition_integration_frozen": True,
    "e2e_frozen": True,
    "forms_contact_is_generic_domain": False,
    "forms_contact_role": "contact_form_app_block",
    "isolation": {
        "form_definition_is_not_rendered_ui": True,
        "submission_is_not_crm_object": True,
        "submission_is_not_job_id": True,
        "email_field_does_not_make_crm_form": True,
        "registration_form_is_not_auth": True,
        "payment_form_is_not_payment_execution": True,
        "platform_turnstile_is_not_customer_cloudflare": True,
        "email_send_is_not_email_template": True,
        "p_is_never_fallback_for_c": True,
    },
    "slices": (
        "7a_forms_domain_audit",
        "7b_canonical_form_model",
        "7c_form_validation_semantics",
        "7d_submission_runtime",
        "7e_persistence_and_retrieval",
        "7f_file_uploads",
        "7g_anti_abuse",
        "7h_post_submit_action_model",
        "7i_crm_composition",
        "7j_email_composition",
        "7k_automation_webhook_composition",
        "7l_conditional_logic",
        "7m_multi_step_forms",
        "7n_public_bff_runtime_truth",
        "7o_codegen_exposure",
        "7p_app_block_feature_pack_starter",
        "7q_end_to_end_application_tests",
    ),
    "hypothesized_verbs_not_frozen": FORMS_HYPOTHESIZED_VERBS_NOT_FROZEN,
    "object_vocabulary_candidates": FORMS_OBJECT_VOCABULARY_CANDIDATES,
    "canonical_model": "src.domain.forms.canonical",
    "validation": "src.domain.forms.validation",
    "runtime": "src.domain.forms.runtime",
    "persistence": "src.domain.forms.persistence",
    "files": "src.domain.forms.files",
    "anti_abuse": "src.domain.forms.anti_abuse",
    "actions": "src.domain.forms.actions",
    "crm_composition": "src.domain.forms.crm",
    "email_composition": "src.domain.forms.email",
    "automation_composition": "src.domain.forms.automation",
    "http_forward_composition": "src.domain.forms.forward",
    "conditions": "src.domain.forms.conditions",
    "steps": "src.domain.forms.steps",
    "public_runtime": "src.domain.forms.public",
    "forms_codegen": "src.domain.codegen.forms_codegen",
    "forms_composition": "src.domain.forms.composition",
    "forms_e2e": "src.domain.forms.e2e",
    "next_slice": "9_app_block_modernization",
}

FORMS_DOMAIN_AUDIT: dict[str, Any] = {
    "slice_id": "7a_forms_domain_audit",
    "status": "complete",
    "family_opened": True,
    "verbs_frozen": False,
    "catalog_mutated": False,
    "new_capability_verbs": (),
    "implements_attached": False,
    "public_bff_form_capabilities": (),
    "codegen_truth_flip": False,
    "schema_migrations": False,
    "vendor_overall": "B",
    "forms_contact_classification": "A_specific_contact_form_app_block",
    "forms_quote_request_classification": "specific_quote_form_app_block",
    "forms_public_submission_alias": "contact-capture",
    "core_form_primitives": ("form.public_create",),
    "pack_provide_ids": ("form.public_submission", "contact.capture"),
    "app_blocks": ("forms.contact", "forms.quote_request"),
    "starter_contracts": ("form.standard", "form.field", "form.section"),
    "generated_contact_entity": "contact_submission",
    "platform_marketing_table": "contact_inquiries",
    "cloud_job_form_forward": True,
    "hubspot_form_catalog_actions": (),
    "salesforce_form_catalog_actions": (),
    "p2hs_owned": (
        "forms.contact",
        "forms.quote_request",
        "contact-capture",
        "form.public_create",
        "p2hs-contact.submitContact",
        "p2hs-forms.submitFormEntry",
        "starter.form.standard",
        "core.contact_inquiries",
        "platform_turnstile",
    ),
    "customer_bound": (
        "crm.contact.upsert",
        "email.send",
        "automation.workflow.trigger",
        "form.forward",
    ),
    "composition_gaps": (
        "forms.public_submission_alias_is_contact_capture_not_generic",
        "quote_request_block_requires_contact_capture_pack",
        "form.public_create_has_no_turnstile_or_honeypot",
        "submitContact_uses_name_not_first_name_last_name",
        "block_event_forms.contact.submitted_vs_pack_event_contact.submitted",
        "legacy_contact_capture_crm_sync_unused_by_new_composition",
    ),
    "failure_policies_inventoried": (
        "BEST_EFFORT",
        "QUEUE_AND_RETURN_SUCCESS",
        "BLOCK_USER",
        "UNKNOWN",
    ),
    "upload_runtime": False,
    "multi_step_runtime": False,
    "conditional_logic_runtime": False,
    "recommended_next_slice": "7b_canonical_form_model",
    "docs": "docs/audits/FORMS_DOMAIN_7A.md",
}

# Locked: invocable false is a Codegen prohibition, not a missing catalog row.
INVOCABLE_FALSE_MEANS = (
    "Valid for planning, composition, requirement declaration, and future binding. "
    "Codegen must not emit a runtime call. Fall back to an invocable capability "
    "(e.g. email.send), an explicit provider action if the requirement is vendor-specific, "
    "or a capability/configuration gap. Never invent POST /api/jobs/<capability>."
)

MESSAGING_SEND_FORBIDDEN_METADATA_KEYS: tuple[str, ...] = (
    "blocks",
    "attachments",
    "buttons",
    "mrkdwn",
    "unfurl_links",
    "icon_emoji",
    "channel",
    "thread_ts",
)

MESSAGING_SEND_INPUT_SCHEMA: dict[str, Any] = {
    "type": "object",
    "additionalProperties": False,
    "required": ["destination", "text"],
    "properties": {
        "destination": {
            "type": "string",
            "minLength": 1,
            "description": (
                "Logical destination (e.g. ops, user:123, support-team). "
                "Not a Slack channel/user ID (C0123 / U0123). The runtime resolver maps "
                "logical names to provider addresses; do not treat this field "
                "as a raw provider identifier."
            ),
            "x-p2hs": {
                "kind": "logical_destination",
                "resolution": "runtime_resolver",
                "not_provider_address": True,
            },
        },
        "text": {
            "type": "string",
            "minLength": 1,
            "description": "Plain message body. Not mrkdwn, Block Kit, or attachments.",
        },
        "thread_ref": {
            "type": "string",
            "description": "Opaque thread id from a prior messaging.send result. Empty starts a new thread.",
        },
        "metadata": {
            "type": "object",
            "additionalProperties": True,
            "description": (
                "Opaque application/audit context. MUST NOT affect provider rendering "
                "semantics. Not a side door for blocks, attachments, buttons, formatting, "
                "or provider options. Rendering extensions need a capability version "
                "or a provider escape hatch."
            ),
            "x-p2hs": {
                "kind": "audit_context",
                "must_not_affect_provider_rendering": True,
                "forbidden_keys": list(MESSAGING_SEND_FORBIDDEN_METADATA_KEYS),
            },
        },
    },
}

MESSAGING_SEND_OUTPUT_SCHEMA: dict[str, Any] = {
    "type": "object",
    "additionalProperties": False,
    "required": ["message_id", "status"],
    "properties": {
        "message_id": {"type": "string"},
        "thread_ref": {"type": "string"},
        "status": {"type": "string", "enum": ["sent", "queued", "rejected"]},
    },
}

MESSAGING_SEND_ERRORS: tuple[str, ...] = (
    "capability_not_invocable",
    "capability_not_bound",
    "binding_ambiguous",
    "connection_inactive",
    "credentials_missing",
    "destination_invalid",
    "destination_unbound",
    "text_required",
    "provider_unavailable",
    "provider_rejected",
)

MESSAGING_SEND_INVOKE_METHOD = "POST /api/projects/{project_id}/capabilities/messaging.send"

# Invoke path is live. Slack implements messaging.send. Codegen default is the capability.
MESSAGING_SEND_RESOLVER_DESIGN: dict[str, Any] = {
    "status": "codegen_default",
    "capability": "messaging.send",
    "catalog_invocable": True,
    "implements_attached": True,
    "public_result": "capability_shaped",
    "codegen_default_unchanged": False,
    "codegen_default": True,
    "slack_demoted_to_provider_actions": True,
    "no_platform_default": True,
    "not_email_first_wins": True,
    "returns": "execution_plan",
    "dispatch": "integration.execute",
    "resolver_eligibility_is_not_execution_authorization": True,
    "flip_order": (
        "invocable_true_after_public_entry_dispatches_plan",
        "implements_on_slack_post_message",
        "implemented_by_from_catalog_slugs_implementing",
        "codegen_default_messaging_send",
        "demote_slack_post_message_to_provider_actions",
    ),
    "evaluation_order": (
        "capability_exists",
        "capability_invocable",
        "validate_input",
        "discover_candidate_actions",
        "load_project_connections",
        "filter_state_credentials_entitlement_permission",
        "resolve_connection_ambiguity",
        "resolve_logical_destination",
        "return_execution_plan",
    ),
    "provider_hint": "disambiguation among eligible candidates; does not create or persist a binding",
    "unique_connection_errors": {
        "connection_inactive": "only when an explicit binding identifies one connection",
        "credentials_missing": "only when an explicit binding identifies one connection",
        "no_binding_no_eligible": "capability_not_bound (rejected-candidate reasons stay in diagnostics)",
    },
    "candidate_eligibility": {
        "connection_status": "active",
        "secrets": "required when the connector catalog declares secret_fields",
        "entitlement": "integrations.execute (required_capabilities; do not overload)",
        "permission": "allow, or ask with a valid approval token",
        "adapter": (
            "Catalog action that will implement messaging.send "
            "(planned: slack.post_message only). Not every communication connector: "
            "Twilio SMS is messaging.sms.send, not messaging.send."
        ),
        "excludes": (
            "inactive/error/pending connections",
            "client-side-only connectors",
            "connections whose only chat action is Slack-specific Block Kit",
        ),
    },
    "selection": {
        "zero": "capability_not_bound",
        "one": "use that connection",
        "multiple": (
            "Do not first-wins (email.send may; Slack vs a future Teams inbox "
            "are not substitutable for destination ops). Require a unique "
            "project capability binding, else a unique destination binding, "
            "else a provider_hint that matches exactly one candidate. "
            "Otherwise binding_ambiguous."
        ),
        "explicit_binding_when_exists": "UNIQUE(project_id, capability_key) — not a table yet",
    },
    "destination_binding": {
        "input": "logical name (ops, user:123, support-team)",
        "order": (
            "destination map on the project/capability binding",
            "named destination on the selected connection config",
            "never treat destination as a raw Slack C/U id",
        ),
        "provider_shaped_id": "destination_invalid — use slack.post_message escape hatch",
        "unknown_logical_name": "destination_unbound",
        "empty": "destination_invalid",
        "maps_to": "provider address (Slack channel) only after a unique connection is selected",
    },
    "failure_semantics": {
        "capability_not_invocable": "Call attempted while invocable is false.",
        "capability_not_bound": "Zero eligible connections. Not a silent email.send substitution.",
        "binding_ambiguous": "Multiple eligible connections and no unique binding/hint.",
        "connection_inactive": "Selected binding points at a non-active connection.",
        "credentials_missing": "Eligible connector lacks required secrets.",
        "destination_invalid": "Empty or provider-shaped identifier in the generic contract.",
        "destination_unbound": "Logical name with no mapping on the selected binding.",
        "text_required": "Contract validation before provider call.",
        "provider_unavailable": "Execution: adapter/transport missing.",
        "provider_rejected": "Execution: provider API rejected the mapped send.",
    },
}
NOTIFICATION_SMS_FORBIDDEN_METADATA_KEYS: tuple[str, ...] = (
    "media_url",
    "from",
    "messaging_service_sid",
    "status_callback",
    "validity_period",
    "smart_encoded",
    "shorten_urls",
    "content_sid",
    "content_variables",
    "twilio_sid",
    "from_number",
)

NOTIFICATION_SMS_PROVIDER_INPUT_KEYS: tuple[str, ...] = (
    "to",
    "body",
    "from",
    "media_url",
    "messaging_service_sid",
    "status_callback",
    "validity_period",
    "smart_encoded",
    "shorten_urls",
    "content_sid",
    "content_variables",
)

NOTIFICATION_SMS_INPUT_SCHEMA: dict[str, Any] = {
    "type": "object",
    "additionalProperties": False,
    "required": ["destination", "text"],
    "properties": {
        "destination": {
            "type": "string",
            "minLength": 1,
            "description": (
                "E.164 recipient phone number (e.g. +14155550123). "
                "Not a Twilio Messaging Service SID, WhatsApp address, or logical alias. "
                "Sender identity is resolved from the bound connection, not this field."
            ),
            "x-p2hs": {
                "kind": "e164_recipient",
                "normalization": "e164",
                "not_logical_alias": True,
                "not_sender": True,
            },
        },
        "text": {
            "type": "string",
            "minLength": 1,
            "description": (
                "Plain SMS body. Not MMS media, WhatsApp templates, or Twilio Content SID payloads."
            ),
        },
        "metadata": {
            "type": "object",
            "additionalProperties": True,
            "description": (
                "Opaque application/audit context. MUST NOT affect provider rendering "
                "or Twilio send options. Not a side door for media_url, From, Messaging "
                "Service SID, status callbacks, or Content SID."
            ),
            "x-p2hs": {
                "kind": "audit_context",
                "must_not_affect_provider_rendering": True,
                "forbidden_keys": list(NOTIFICATION_SMS_FORBIDDEN_METADATA_KEYS),
            },
        },
    },
}

NOTIFICATION_SMS_OUTPUT_SCHEMA: dict[str, Any] = {
    "type": "object",
    "additionalProperties": False,
    "required": ["status"],
    "properties": {
        "message_id": {"type": "string"},
        "status": {"type": "string", "enum": ["queued", "rejected"]},
    },
}

NOTIFICATION_SMS_ERRORS: tuple[str, ...] = (
    "capability_not_invocable",
    "capability_not_bound",
    "binding_ambiguous",
    "connection_inactive",
    "credentials_missing",
    "destination_invalid",
    "text_required",
    "sender_unbound",
    "provider_fields_forbidden",
    "provider_unavailable",
    "provider_rejected",
)

NOTIFICATION_SMS_INVOKE_METHOD = "POST /api/projects/{project_id}/capabilities/messaging.sms.send"

# Twilio implements messaging.sms.send. Public invoke supports BYOK and the
# provider-neutral P2HS-owned fallback.
NOTIFICATION_SMS_RESOLVER_DESIGN: dict[str, Any] = {
    "status": "contract_and_current_path_mapping",
    "capability": "messaging.sms.send",
    "template": "A",
    "catalog_invocable": True,
    "implements_attached": True,
    "public_result": "capability_shaped",
    "codegen_default": False,
    "codegen_default_unchanged": True,
    "twilio_remains_catalog_action": True,
    "not_messaging_send": True,
    "no_platform_default": True,
    "not_email_first_wins": True,
    "returns": "execution_plan",
    "dispatch": "integration.execute",
    "existing_runtime_path": "twilio.send_sms",
    "resolver_eligibility_is_not_execution_authorization": True,
    "public_topology": "dashboard_bff_then_private_core",
    "no_installed_app_jobs_route": True,
    "flip_order": (
        "freeze_notification_sms_contract",
        "map_twilio_send_sms_generic_vs_provider_fields",
        "define_sender_resolution_from_connection",
        "define_candidate_binding_rules",
        "core_private_resolver_execution_plan",
        "core_private_invoke_http",
        "dashboard_bff_public_proxy",
        "invocable_true",
        "implements_on_twilio_send_sms",
        "implemented_by_from_catalog_slugs_implementing",
        "codegen_default_notification_sms_bff_path",
        "demote_twilio_send_sms_to_provider_actions",
    ),
    "evaluation_order": (
        "capability_exists",
        "capability_invocable",
        "validate_input",
        "normalize_e164_destination",
        "discover_candidate_actions",
        "load_project_connections",
        "filter_state_credentials_entitlement_permission",
        "resolve_connection_ambiguity",
        "resolve_sender_from_connection_config",
        "return_execution_plan",
    ),
    "provider_hint": (
        "Disambiguation among eligible connector keys only; two Twilio accounts "
        "still need an explicit project capability binding. Does not persist preference."
    ),
    "unique_connection_errors": {
        "connection_inactive": "only when an explicit binding identifies one connection",
        "credentials_missing": "only when an explicit binding identifies one connection",
        "no_binding_no_eligible": (
            "capability_not_bound (rejected-candidate reasons stay in diagnostics)"
        ),
    },
    "candidate_eligibility": {
        "connection_status": "active",
        "secrets": "required when the connector catalog declares secret_fields",
        "entitlement": "integrations.execute (required_capabilities; do not overload)",
        "permission": "allow, or ask with a valid approval token",
        "adapter": (
            "Catalog action that will implement messaging.sms.send "
            "(planned: twilio.send_sms only). Not Slack: messaging.send is a "
            "different capability and must not share this resolver."
        ),
        "excludes": (
            "inactive/error/pending connections",
            "client-side-only connectors",
            "Slack / chat messaging connections",
            "WhatsApp-only or MMS-only intent (use twilio.send_sms escape hatch)",
        ),
    },
    "selection": {
        "zero": "capability_not_bound",
        "one": "use that connection",
        "multiple": (
            "Do not first-wins. Two Twilio accounts can differ by sender, region, "
            "compliance registration, brand, and billing. Require a unique project "
            "capability binding, else a unique connection-id binding. provider_hint "
            "of 'twilio' does not disambiguate two Twilio connections. "
            "Otherwise binding_ambiguous."
        ),
        "explicit_binding_when_exists": "UNIQUE(project_id, capability_key) — not a table yet",
    },
    "destination_binding": {
        "input": "E.164 recipient (+14155550123)",
        "order": (
            "normalize whitespace/separators",
            "require leading + and ITU-T E.164 digit length",
            "reject WhatsApp addresses, Messaging Service SIDs, and logical aliases",
        ),
        "logical_alias": "not required; destination_invalid (no user/contact binding layer)",
        "provider_shaped_id": "Messaging Service SID / WhatsApp prefix → destination_invalid",
        "empty": "destination_invalid",
        "maps_to": "Twilio Messages API To (recipient). Never From.",
    },
    "sender_resolution": {
        "source": "bound Twilio connection configuration, not Codegen and not capability input",
        "order": (
            "messaging_service_sid on the connection config",
            "from_phone on the connection config",
        ),
        "capability_input_from": (
            "forbidden — generated apps must not hardcode Twilio From or Messaging Service SID"
        ),
        "cloud_adapter": (
            "Existing twilio.send_sms already reads From / MessagingServiceSid from "
            "connection config when omitted on input. Capability plans pass only to/body."
        ),
        "unbound": "sender_unbound — connection has neither from_phone nor messaging_service_sid",
    },
    "failure_semantics": {
        "capability_not_invocable": "Call attempted while invocable is false.",
        "capability_not_bound": (
            "Zero eligible Twilio connections. Not a silent messaging.send "
            "or email.send substitution."
        ),
        "binding_ambiguous": "Multiple eligible Twilio connections and no unique binding.",
        "connection_inactive": "Selected binding points at a non-active connection.",
        "credentials_missing": "Eligible connector lacks required secrets.",
        "destination_invalid": "Empty, non-E.164, WhatsApp, or SID-shaped recipient.",
        "text_required": "Contract validation before provider call.",
        "sender_unbound": "Selected connection has no From number and no Messaging Service SID.",
        "provider_fields_forbidden": "Twilio-only fields appeared on the generic capability input.",
        "provider_unavailable": "Execution: adapter/transport missing.",
        "provider_rejected": "Execution: provider API rejected the mapped send.",
    },
}

TWILIO_NOTIFICATION_SMS_MAPPING: dict[str, Any] = {
    "provider_action": "twilio.send_sms",
    "status": "current_path_audit",
    "live_runtime": "integration.execute → pc-processor twilio.send_sms",
    "fields": {
        "destination": "to",
        "text": "body",
    },
    "sender": {
        "capability_field": None,
        "connection_config": ("messaging_service_sid", "from_phone"),
        "twilio_request": ("MessagingServiceSid", "From"),
    },
    "unmapped_provider_fields": (
        "from",
        "media_url",
        "messaging_service_sid",
        "status_callback",
        "validity_period",
        "smart_encoded",
        "shorten_urls",
        "content_sid",
        "content_variables",
    ),
    "unmapped_provider_output": (
        "twilio_sid",
        "sid",
        "messaging_service_sid",
        "from_number",
        "from",
    ),
    "escape_hatch": (
        "Use twilio.send_sms when the user explicitly requires Twilio Messages API "
        "semantics (MMS media_url, WhatsApp, Content SID, status callbacks, "
        "Messaging Service override, From override). Those fields are not part of "
        "messaging.sms.send. Do not overload messaging.sms.send for MMS; that is a "
        "future messaging.mms.send or the Twilio escape hatch."
    ),
}

SLACK_MESSAGING_SEND_MAPPING: dict[str, Any] = {
    "provider_action": "slack.post_message",
    "status": "runtime_adapter",
    "fields": {
        "destination": "channel",
        "text": "text",
        "thread_ref": "thread_ts",
    },
    "unmapped_provider_fields": (
        "blocks",
        "attachments",
        "unfurl_links",
        "mrkdwn",
        "icon_emoji",
    ),
    "escape_hatch": (
        "Use slack.post_message when the user explicitly requires Slack Block Kit "
        "or other chat.postMessage semantics. Those fields are not part of messaging.send."
    ),
}

AUTOMATION_WORKFLOW_TRIGGER_FORBIDDEN_METADATA_KEYS: tuple[str, ...] = (
    "scenario_id",
    "workflow_id",
    "webhook_url",
    "hook_url",
    "zap_id",
    "make_scenario_id",
    "n8n_workflow_id",
)

AUTOMATION_WORKFLOW_TRIGGER_PROVIDER_INPUT_KEYS: tuple[str, ...] = (
    "scenario_id",
    "workflow_id",
    "webhook_url",
    "hook_url",
    "zap_id",
    "payload",
)

AUTOMATION_WORKFLOW_TRIGGER_INPUT_SCHEMA: dict[str, Any] = {
    "type": "object",
    "additionalProperties": False,
    "required": ["workflow", "input"],
    "properties": {
        "workflow": {
            "type": "string",
            "minLength": 1,
            "pattern": "^[a-z][a-z0-9-]{0,62}$",
            "description": (
                "Logical workflow name (e.g. lead-intake). Not a Make scenario ID, "
                "n8n workflow ID, Zapier zap ID, or webhook URL. Runtime binding "
                "resolves this name to a project connection."
            ),
            "x-p2hs": {
                "kind": "logical_workflow",
                "resolution": "runtime_resolver",
                "not_provider_id": True,
            },
        },
        "input": {
            "type": "object",
            "additionalProperties": True,
            "description": (
                "JSON object posted as the workflow payload. Not Make scenario "
                "variables, n8n execution options, or a webhook URL."
            ),
        },
        "metadata": {
            "type": "object",
            "additionalProperties": True,
            "description": (
                "Opaque application/audit context. MUST NOT select the provider "
                "workflow or alter Make/n8n/Zapier execution semantics."
            ),
            "x-p2hs": {
                "kind": "audit_context",
                "must_not_affect_provider_execution": True,
                "forbidden_keys": list(AUTOMATION_WORKFLOW_TRIGGER_FORBIDDEN_METADATA_KEYS),
            },
        },
    },
}

AUTOMATION_WORKFLOW_TRIGGER_OUTPUT_SCHEMA: dict[str, Any] = {
    "type": "object",
    "additionalProperties": False,
    "required": ["capability", "status"],
    "properties": {
        "capability": {
            "type": "string",
            "const": "automation.workflow.trigger",
        },
        "status": {"type": "string", "enum": ["queued", "rejected"]},
        "execution_ref": {
            "type": "string",
            "description": (
                "Opaque platform reference for the queued job. Not a Make scenario ID, "
                "n8n execution ID, webhook URL, or provider operation URL."
            ),
            "x-p2hs": {"kind": "opaque_execution_ref", "not_provider_id": True},
        },
        "reason_code": {"type": "string"},
        "message": {"type": "string"},
    },
}

AUTOMATION_WORKFLOW_TRIGGER_ERRORS: tuple[str, ...] = (
    "capability_not_invocable",
    "capability_not_bound",
    "binding_ambiguous",
    "workflow_invalid",
    "workflow_unbound",
    "input_required",
    "provider_fields_forbidden",
    "connection_inactive",
    "credentials_missing",
    "provider_unavailable",
    "provider_rejected",
)

AUTOMATION_WORKFLOW_TRIGGER_INVOKE_METHOD = (
    "POST /api/projects/{project_id}/capabilities/automation.workflow.trigger"
)

MAKE_AUTOMATION_WORKFLOW_MAPPING: dict[str, Any] = {
    "provider_action": "make_com.trigger_scenario",
    "status": "runtime_adapter",
    "cloud_handler": "make_com.trigger_scenario",
    "fields": {"input": "payload"},
    "workflow": {
        "capability_field": "workflow",
        "connection_config": ("workflow",),
        "provider_address": "webhook_url",
    },
    "unmapped_provider_fields": (
        "webhook_url",
        "scenario_id",
        "api_base_url",
    ),
    "escape_hatch": (
        "Use make_com.trigger_scenario for Make-specific scenario variables, "
        "API-run-by-id, or webhook URL semantics."
    ),
}

N8N_AUTOMATION_WORKFLOW_MAPPING: dict[str, Any] = {
    "provider_action": "n8n.trigger_workflow",
    "status": "runtime_adapter",
    "cloud_handler": "n8n.trigger_workflow",
    "fields": {"input": "payload"},
    "workflow": {
        "capability_field": "workflow",
        "connection_config": ("workflow",),
        "provider_address": "webhook_url",
    },
    "unmapped_provider_fields": (
        "webhook_url",
        "workflow_id",
        "instance_base_url",
    ),
    "escape_hatch": (
        "Use n8n.trigger_workflow for n8n-specific execution semantics, "
        "REST execute-by-id, or webhook URL semantics."
    ),
}

ZAPIER_AUTOMATION_WORKFLOW_MAPPING: dict[str, Any] = {
    "provider_action": None,
    "status": "blocked",
    "cloud_handler": None,
    "implements": False,
    "resolver_candidate": False,
    "notes": (
        "No Zapier connector action and no Cloud execute path. Absent from "
        "capability adapter_mappings and resolver candidates until both exist. "
        "Event-subscription destination 'zapier' is lifecycle, not this invoke verb."
    ),
}

AUTOMATION_WORKFLOW_TRIGGER_RESOLVER_DESIGN: dict[str, Any] = {
    "status": "codegen_default",
    "capability": "automation.workflow.trigger",
    "template": "A",
    "phase": "1c",
    "phase_status": "n8n_implements_complete",
    "next_slice": "1d_zapier_blocked",
    "phase_1d_status": "blocked_until_catalog_and_cloud_path",
    "catalog_invocable": True,
    "implements_attached": True,
    "runtime_adapter": ("make_com", "n8n"),
    "n8n_runtime": True,
    "public_result": "capability_shaped",
    "public_response_owner": "core",
    "public_success": {
        "capability": "automation.workflow.trigger",
        "status": "queued",
        "execution_ref": "opaque-platform-reference",
    },
    "public_success_forbidden": (
        "webhook_url",
        "scenario_id",
        "workflow_id",
        "zap_id",
        "provider_execution_url",
        "make_operation_id",
        "n8n_execution_id",
        "connection_id",
        "action",
        "run_id",
    ),
    "codegen_default": True,
    "codegen_default_unchanged": True,
    "codegen_exposure_policy": "progressive_after_first_implementation",
    "make_demoted_to_provider_actions": True,
    "n8n_demoted_to_provider_actions": True,
    "existing_runtime_path": (
        "make_com.trigger_scenario",
        "n8n.trigger_workflow",
    ),
    "dispatch": "integration.execute",
    "zapier": "blocked_until_catalog_and_cloud_path",
    "resolver_eligibility_is_not_execution_authorization": True,
    "flip_order": (
        "freeze_automation_workflow_trigger_contract",
        "map_make_and_n8n_payload_vs_provider_ids",
        "define_logical_workflow_binding_rules",
        "verify_cloud_webhook_execute_paths",
        "1b_make_private_invoke_and_bff_proxy",
        "1b_invocable_true",
        "1b_implements_on_make_com_trigger_scenario",
        "1b_implemented_by_from_catalog_slugs_implementing",
        "1b_codegen_default_automation_workflow_trigger",
        "1b_demote_make_com_trigger_scenario_to_provider_actions",
        "1c_n8n_implements_without_codegen_change",
        "1d_zapier_blocked_until_catalog_and_cloud_path",
    ),
    "provider_hint": (
        "Adapter-family filter only (make_com vs n8n). Does not select a "
        "scenario, workflow ID, or webhook. Two Make connections still need "
        "an explicit workflow map or connection alias."
    ),
    "disambiguation_axes": {
        "provider_hint": "provider_family",
        "workflow": "logical_resource",
    },
    "workflow_binding": {
        "input": "logical name (lead-intake), never provider IDs",
        "order": (
            "explicit_project_capability_binding",
            "workflow_map[logical_name]",
            "connection.config.workflow exact match",
            "unique eligible connection with matching workflow binding",
            "otherwise workflow_unbound or binding_ambiguous",
        ),
        "alias": "connection.config.workflow",
        "map": "workflow_map logical_name → connection_id",
        "provider_address": "connection.config.webhook_url (Make/n8n)",
        "unique_without_alias": "workflow_unbound",
        "zero": "capability_not_bound",
        "multiple": "binding_ambiguous; not first-wins",
    },
    "candidate_eligibility": {
        "adapter": {
            "make_com": "runtime candidate via make_com.trigger_scenario",
            "n8n": "runtime candidate via n8n.trigger_workflow",
        },
        "connection_status": "active",
        "secrets": "not required for webhook trigger when catalog secrets are optional",
        "entitlement": "integrations.execute",
        "permission": "allow, or ask with a valid approval token",
    },
    "failure_semantics": AUTOMATION_WORKFLOW_TRIGGER_ERRORS,
}

GOOGLE_DOCS_PROVIDER_ACTIONS: tuple[str, ...] = (
    "google_docs.create_document",
    "google_docs.get_document",
    "google_docs.insert_text",
    "google_docs.append_text",
    "google_docs.replace_all_text",
)

DOCUMENT_FROZEN_VERBS: tuple[str, ...] = (
    "document.create",
    "document.get",
    "document.insert_text",
    "document.append_text",
    "document.replace_text",
)

# Phase 2A audit; 2P filled the customer-bound live path. 2B froze document.* verbs.
DOCUMENT_LIVE_PATH_AUDIT: dict[str, Any] = {
    "family": "document.*",
    "first_intended_provider": "google_docs",
    "execution_domain": "C",
    "core_catalog_connector": "google_docs",
    "core_catalog_actions": GOOGLE_DOCS_PROVIDER_ACTIONS,
    "tools_yaml": "tools/connectors/google_docs.yaml",
    "cloud_handlers": GOOGLE_DOCS_PROVIDER_ACTIONS,
    "invoke_capability_ids": DOCUMENT_FROZEN_VERBS,
    "feature_packs": (),
    "app_blocks": (),
    "public_bff_capability_routes": DOCUMENT_FROZEN_VERBS,
    "codegen_default": True,
    "implements": GOOGLE_DOCS_PROVIDER_ACTIONS,
    "sibling_google_connectors_not_docs": (
        "google_calendar",
        "google_ads",
        "google_analytics",
        "google_tag_manager",
    ),
    "not_this_family": (
        "google_calendar",
        "cloudflare_r2",
        "spreadsheet.*",
    ),
    "p2hs_owned_google_path": False,
}

GOOGLE_DOCS_DOCUMENT_MAPPING: dict[str, Any] = {
    "provider_action": GOOGLE_DOCS_PROVIDER_ACTIONS,
    "status": "resolver_mapped",
    "cloud_handler": GOOGLE_DOCS_PROVIDER_ACTIONS,
    "implements": True,
    "resolver_candidate": True,
    "execution_domain": "C",
    "notes": (
        "Phase 2C resolves customer-bound google_docs connections into "
        "internal execution plans. Catalog invocable and google_docs implements "
        "are runtime truth (2F). Codegen default stays off until 2G. "
        "P2HS-owned Google is not a resolver candidate."
    ),
}

DOCUMENT_HYPOTHESIZED_VERBS_NOT_FROZEN: tuple[str, ...] = (
    "document.export",
)

DOCUMENT_VERB_CLASSIFICATION: dict[str, str] = {
    "document.create": "A",
    "document.get": "A",
    "document.insert_text": "A",
    "document.append_text": "A",
    "document.replace_text": "A",
}

DOCUMENT_FORBIDDEN_METADATA_KEYS: tuple[str, ...] = (
    "document_id",
    "revision_id",
    "google_document_id",
    "docs_document_id",
    "index",
    "match_case",
    "requests",
    "batchUpdate",
    "documents.batchUpdate",
    "documents.get",
    "drive.files",
)

DOCUMENT_PROVIDER_INPUT_KEYS: tuple[str, ...] = (
    "document_id",
    "revision_id",
    "google_document_id",
    "docs_document_id",
    "index",
    "match_case",
    "requests",
    "batchUpdate",
)

DOCUMENT_LOGICAL_FIELD: dict[str, Any] = {
    "type": "string",
    "minLength": 1,
    "description": (
        "Logical document alias (e.g. contract-template). Not a Google "
        "document ID, revision ID, or Drive file ID."
    ),
    "x-p2hs": {
        "kind": "logical_document",
        "not_provider_id": True,
    },
}

DOCUMENT_METADATA_FIELD: dict[str, Any] = {
    "type": "object",
    "additionalProperties": True,
    "description": (
        "Opaque application/audit context. MUST NOT select the Google "
        "document or carry Docs API fields."
    ),
    "x-p2hs": {
        "kind": "audit_context",
        "must_not_affect_provider_execution": True,
        "forbidden_keys": list(DOCUMENT_FORBIDDEN_METADATA_KEYS),
    },
}

DOCUMENT_CREATE_INPUT_SCHEMA: dict[str, Any] = {
    "type": "object",
    "additionalProperties": False,
    "properties": {
        "title": {
            "type": "string",
            "description": "Optional document title. Provider document IDs stay on the binding.",
        },
        "metadata": DOCUMENT_METADATA_FIELD,
    },
}

DOCUMENT_GET_INPUT_SCHEMA: dict[str, Any] = {
    "type": "object",
    "additionalProperties": False,
    "required": ["document"],
    "properties": {
        "document": DOCUMENT_LOGICAL_FIELD,
        "metadata": DOCUMENT_METADATA_FIELD,
    },
}

DOCUMENT_INSERT_TEXT_INPUT_SCHEMA: dict[str, Any] = {
    "type": "object",
    "additionalProperties": False,
    "required": ["document", "text"],
    "properties": {
        "document": DOCUMENT_LOGICAL_FIELD,
        "text": {"type": "string", "minLength": 1},
        "position": {
            "type": "string",
            "enum": ["start"],
            "default": "start",
            "description": (
                "Abstract insertion point. start maps to the beginning of the "
                "document body. Numeric Google Docs indexes are not part of "
                "this contract; use google_docs.insert_text for index. "
                "Appending at the end is document.append_text."
            ),
            "x-p2hs": {
                "kind": "abstract_position",
                "not_google_index": True,
                "maps_to_google_docs": {"start": 1},
            },
        },
        "metadata": DOCUMENT_METADATA_FIELD,
    },
}

DOCUMENT_APPEND_TEXT_INPUT_SCHEMA: dict[str, Any] = {
    "type": "object",
    "additionalProperties": False,
    "required": ["document", "text"],
    "properties": {
        "document": DOCUMENT_LOGICAL_FIELD,
        "text": {"type": "string", "minLength": 1},
        "metadata": DOCUMENT_METADATA_FIELD,
    },
}

DOCUMENT_REPLACE_TEXT_INPUT_SCHEMA: dict[str, Any] = {
    "type": "object",
    "additionalProperties": False,
    "required": ["document", "find"],
    "properties": {
        "document": DOCUMENT_LOGICAL_FIELD,
        "find": {"type": "string", "minLength": 1},
        "replace": {"type": "string"},
        "metadata": DOCUMENT_METADATA_FIELD,
    },
}

DOCUMENT_OUTPUT_SCHEMA: dict[str, Any] = {
    "type": "object",
    "additionalProperties": False,
    "required": ["capability", "status"],
    "properties": {
        "capability": {"type": "string"},
        "status": {"type": "string", "enum": ["queued", "rejected", "succeeded"]},
        "document": {
            "type": "object",
            "additionalProperties": False,
            "description": (
                "Normalized document content for document.get succeeded results. "
                "Not a Google document payload."
            ),
            "properties": {
                "title": {"type": "string"},
                "body_text": {"type": "string"},
            },
        },
        "reason_code": {"type": "string"},
        "message": {"type": "string"},
    },
}

DOCUMENT_ERRORS: tuple[str, ...] = (
    "capability_not_invocable",
    "capability_not_bound",
    "binding_ambiguous",
    "document_invalid",
    "document_unbound",
    "text_required",
    "find_required",
    "position_invalid",
    "provider_fields_forbidden",
    "connection_inactive",
    "credentials_missing",
    "permission_denied",
    "approval_required",
    "provider_unavailable",
    "provider_rejected",
)

DOCUMENT_INVOKE_METHODS: dict[str, str] = {
    verb: f"POST /api/projects/{{project_id}}/capabilities/{verb}"
    for verb in DOCUMENT_FROZEN_VERBS
}

DOCUMENT_CODEGEN_DEFAULT = (
    "Use document.* for generic document work. Emit "
    "POST /api/projects/{project_id}/capabilities/<verb> on the public "
    "Dashboard/BFF path. Never invent POST /api/jobs/document.* . Never "
    "construct a Core host. document is a logical alias, not a Google "
    "document_id. Do not put Google document IDs, revision IDs, Docs API "
    "method names, or Drive paths in generated application code. "
    "google_docs.insert_text is a provider_actions escape hatch for a "
    "numeric Google Docs index. google_docs.replace_all_text is an escape "
    "hatch for match_case. Other google_docs.* actions are escape hatches "
    "for explicit Google Docs API semantics. Switching the connected Docs "
    "adapter must not change generated application code."
)

DOCUMENT_CREATE_CODEGEN = (
    DOCUMENT_CODEGEN_DEFAULT
    + " document.create does not persist a binding to a logical alias. "
    "Do not assume document.create { title: \"Proposal\" } makes "
    "document.append_text { document: \"proposal\" } work. Subsequent "
    "get/insert/append/replace require an already-bound logical document. "
    "Create-binding is deferred; do not invent a binding write."
)

DOCUMENT_CODEGEN_UNTIL_DEFAULT = DOCUMENT_CODEGEN_DEFAULT

GOOGLE_DOCS_CREATE_MAPPING: dict[str, Any] = {
    "provider_action": "google_docs.create_document",
    "status": "runtime_adapter",
    "fields": {"title": "title"},
    "unmapped_provider_fields": (),
    "document": "created_resource_bound_later",
    "escape_hatch": (
        "Use google_docs.create_document only for explicit Google Docs API "
        "create semantics (provider document_id return, Drive parent)."
    ),
}

GOOGLE_DOCS_GET_MAPPING: dict[str, Any] = {
    "provider_action": "google_docs.get_document",
    "status": "runtime_adapter",
    "fields": {},
    "document": "logical_alias_to_connection_document_id",
    "unmapped_provider_fields": ("document_id",),
    "escape_hatch": (
        "Use google_docs.get_document only for explicit Google document_id "
        "or revision payloads."
    ),
}

GOOGLE_DOCS_INSERT_TEXT_MAPPING: dict[str, Any] = {
    "provider_action": "google_docs.insert_text",
    "status": "runtime_adapter",
    "fields": {"text": "text", "position": "index_via_abstract_start_only"},
    "position_map": {"start": 1},
    "unmapped_provider_fields": ("index", "document_id"),
    "document": "logical_alias_to_connection_document_id",
    "escape_hatch": (
        "Use google_docs.insert_text when the caller needs a numeric Google "
        "Docs index. document.insert_text only maps position=start."
    ),
}

GOOGLE_DOCS_APPEND_TEXT_MAPPING: dict[str, Any] = {
    "provider_action": "google_docs.append_text",
    "status": "runtime_adapter",
    "fields": {"text": "text"},
    "unmapped_provider_fields": ("document_id",),
    "document": "logical_alias_to_connection_document_id",
    "escape_hatch": (
        "Use google_docs.append_text only for explicit Google Docs API "
        "append semantics."
    ),
}

GOOGLE_DOCS_REPLACE_TEXT_MAPPING: dict[str, Any] = {
    "provider_action": "google_docs.replace_all_text",
    "status": "runtime_adapter",
    "fields": {"find": "find", "replace": "replace"},
    "unmapped_provider_fields": ("match_case", "document_id"),
    "document": "logical_alias_to_connection_document_id",
    "escape_hatch": (
        "Use google_docs.replace_all_text when the caller needs match_case "
        "or other Google replace-all API semantics."
    ),
}

DOCUMENT_FAMILY_RESOLVER_DESIGN: dict[str, Any] = {
    "status": "core_private_resolver_execution_plan",
    "template": "A",
    "phase": "2g",
    "catalog_invocable": True,
    "implements_attached": True,
    "codegen_default": True,
    "resolver": True,
    "public_bff_capability_route": True,
    "core_private_invoke_http": True,
    "dispatch": "integration.execute",
    "existing_runtime_path": "google_docs.* via integration.execute",
    "execution_domain": "C",
    "resolver_eligibility_is_not_execution_authorization": True,
    "insert_text_position": "abstract_start_only",
    "replace_text_portable_fields": ("find", "replace"),
    "not_frozen": ("document.export",),
    "document_ref_emitted": False,
    "create_does_not_persist_binding": True,
    "async_enqueue_status": "queued",
    "succeeded_requires_provider_neutral_result": True,
    "google_docs_demoted_to_provider_actions": True,
    "next_slice": "3a_spreadsheet_live_path_audit",
    "flip_order": (
        "2b_freeze_document_contracts",
        "2b_map_google_docs_generic_vs_provider_fields",
        "2c_core_private_resolver_execution_plan",
        "2d_core_private_invoke_http",
        "2e_dashboard_bff_public_proxy",
        "2f_invocable_true_and_implements",
        "2g_codegen_defaults",
    ),
    "evaluation_order": (
        "capability_exists",
        "capability_invocable_gate",
        "validate_input",
        "discover_customer_bound_google_docs",
        "filter_state_credentials_entitlement_permission",
        "explicit_project_binding",
        "document_map_alias",
        "connection_config_document_exact_match",
        "unique_eligible_matching_or_unbound_ambiguous",
        "map_provider_neutral_plan",
        "return_execution_plan_without_authorize",
    ),
    "workflow_binding_reused_as": "logical_document",
    "unique_without_alias": "document_unbound",
    "create_unique_eligible_connection": "use_that_connection_no_document_resource",
    "multiple": "binding_ambiguous; not first-wins",
    "provider_hint": (
        "adapter-family filter only; google_docs does not select among "
        "two google_docs connections"
    ),
}

DOCUMENT_CREATE_POST_CREATE_BINDING: dict[str, Any] = {
    "status": "deferred",
    "blocks_2c": False,
    "question": (
        "After document.create, how does a logical alias such as proposal "
        "become bound to the provider document_id Google returns?"
    ),
    "not_in_2c": (
        "Do not write the returned document_id into connection config. "
        "Do not invent a binding table during the resolver pass."
    ),
    "required_before": "document.create as a generated-app default",
}

DOCUMENT_FAMILY_WORKING_MODEL: dict[str, Any] = {
    "family": "document.*",
    "template_when_a_live_path_exists": "A",
    "template_until_then": "B_unfrozen",
    "first_provider_when_live": "google_docs",
    "resource": "logical_document_alias",
    "provider_document_id": "connection_or_project_binding",
    "provider_revision_id": "connection_or_project_binding",
    "not_in_generated_code": (
        "document_id",
        "revision_id",
        "google_document_id",
        "docs_document_id",
        "google_docs_api_method",
        "documents.batchUpdate",
        "documents.get",
        "drive.files",
    ),
    "generic_vs_provider": {
        "generic": "logical document resource + frozen document.* verb",
        "google_specific": "google_docs.* catalog actions until later provider_actions demotion",
    },
    "no_giant_google_workspace_capability": True,
    "spreadsheet_family_closed": False,
    "binding_order_when_runtime_opens": (
        "explicit_project_capability_binding",
        "document_map[logical_name]",
        "connection.config.document exact match",
        "unique eligible connection with matching document binding",
        "otherwise document_unbound or binding_ambiguous",
    ),
    "unique_without_alias": "document_unbound",
    "multiple": "binding_ambiguous; not first-wins",
    "provider_hint": "adapter-family filter only; does not select a Google document",
}

DOCUMENT_FAMILY_PHASE_2A: dict[str, Any] = {
    "status": "frozen_audit",
    "phase": "2a",
    "family": "document.*",
    "classification": "audit",
    "verbs_frozen": False,
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "resolver": False,
    "public_bff_capability_route": False,
    "live_path": DOCUMENT_LIVE_PATH_AUDIT,
    "google_docs": GOOGLE_DOCS_DOCUMENT_MAPPING,
    "working_model": DOCUMENT_FAMILY_WORKING_MODEL,
    "hypothesized_verbs_not_frozen": DOCUMENT_HYPOTHESIZED_VERBS_NOT_FROZEN,
    "verb_classification": DOCUMENT_VERB_CLASSIFICATION,
    "next_slice": "2f_document_runtime_truth",
    "phase_2p": (
        "user_facing_google_docs_catalog",
        "byoc_google_credentials",
        "cloud_execute_handlers",
        "no_document_verb_freeze",
        "not_p2hs_managed_drive",
    ),
    "phase_2b_sequence": (
        "classify_only_proven_google_docs_actions",
        "freeze_only_verbs_those_actions_can_support",
        "then_resolver_invoke_implements_codegen_in_later_stages",
    ),
    "flip_order": (
        "2a_open_document_family_audit_only",
        "2a_inventory_live_google_docs_paths",
        "2a_refuse_to_freeze_verbs_without_executable_reality",
        "2a_record_logical_document_binding_model",
        "2p_google_docs_provider_foundation_complete",
        "2b_document_contract_frozen",
        "2c_document_resolver_complete",
        "spreadsheet_closed_as_phase_3",
    ),
}

DOCUMENT_FAMILY_PHASE_2P: dict[str, Any] = {
    "status": "complete",
    "phase": "2p",
    "execution_domain": "C",
    "connector": "google_docs",
    "actions": GOOGLE_DOCS_PROVIDER_ACTIONS,
    "implements": False,
    "invocable_document_star": False,
    "codegen_default": False,
    "public_bff_capability_route": False,
    "p2hs_owned_google_path": False,
    "next_slice": "2f_document_runtime_truth",
}

DOCUMENT_FAMILY_PHASE_2B: dict[str, Any] = {
    "status": "frozen",
    "phase": "2b",
    "family": "document.*",
    "classification": "A",
    "verbs_frozen": DOCUMENT_FROZEN_VERBS,
    "verb_classification": DOCUMENT_VERB_CLASSIFICATION,
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "resolver": False,
    "public_bff_capability_route": False,
    "insert_text_position": "abstract_start_only",
    "not_frozen": DOCUMENT_HYPOTHESIZED_VERBS_NOT_FROZEN,
    "next_slice": "2f_document_runtime_truth",
    "follow_on": (
        "2c_core_private_resolver_execution_plan",
        "2d_core_private_invoke_http",
        "2e_dashboard_bff_public_proxy",
        "2f_invocable_true_and_implements",
        "2g_codegen_defaults",
    ),
}

DOCUMENT_FAMILY_PHASE_2C: dict[str, Any] = {
    "status": "complete",
    "phase": "2c",
    "family": "document.*",
    "resolver": True,
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "public_bff_capability_route": False,
    "core_private_invoke_http": False,
    "execution_domain": "C",
    "create_does_not_persist_binding": True,
    "resolver_eligibility_is_not_execution_authorization": True,
    "next_slice": "2f_document_runtime_truth",
}

DOCUMENT_FAMILY_PHASE_2D: dict[str, Any] = {
    "status": "complete",
    "phase": "2d",
    "family": "document.*",
    "resolver": True,
    "core_private_invoke_http": True,
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "public_bff_capability_route": False,
    "document_ref_emitted": False,
    "create_does_not_persist_binding": True,
    "enqueue": "integration.execute",
    "next_slice": "2f_document_runtime_truth",
}

DOCUMENT_FAMILY_PHASE_2E: dict[str, Any] = {
    "status": "complete",
    "phase": "2e",
    "family": "document.*",
    "public_bff_capability_route": True,
    "bff_role": "proxy_only",
    "core_owns_semantics": True,
    "core_private_invoke_http": True,
    "catalog_invocable": False,
    "implements_attached": False,
    "codegen_default": False,
    "catalog_method_unset": True,
    "document_ref_emitted": False,
    "create_does_not_persist_binding": True,
    "next_slice": "2f_document_runtime_truth",
}

DOCUMENT_FAMILY_PHASE_2F: dict[str, Any] = {
    "status": "complete",
    "phase": "2f",
    "family": "document.*",
    "catalog_invocable": True,
    "implements_attached": True,
    "implemented_by": "slugs_implementing",
    "codegen_default": False,
    "catalog_method_unset": True,
    "status_remains_contract": True,
    "public_bff_capability_route": True,
    "core_private_invoke_http": True,
    "enqueue": "integration.execute",
    "document_ref_emitted": False,
    "create_does_not_persist_binding": True,
    "no_export": True,
    "no_spreadsheet": True,
    "p2hs_owned_google_path": False,
    "next_slice": "2g_document_codegen_defaults",
}

DOCUMENT_FAMILY_PHASE_2G: dict[str, Any] = {
    "status": "complete",
    "phase": "2g",
    "family": "document.*",
    "catalog_invocable": True,
    "implements_attached": True,
    "implemented_by": "slugs_implementing",
    "codegen_default": True,
    "catalog_method_unset": False,
    "status_implemented": True,
    "google_docs_demoted_to_provider_actions": True,
    "public_bff_capability_route": True,
    "create_does_not_persist_binding": True,
    "create_binding_codegen_warning": True,
    "no_export": True,
    "no_spreadsheet": True,
    "p2hs_owned_google_path": False,
    "next_slice": "3a_spreadsheet_live_path_audit",
}

CRM_CONTACT_ERRORS: tuple[str, ...] = (
    "crm_unbound",
    "binding_ambiguous",
    "crm_contact_not_found",
    "crm_duplicate_match",
    "crm_validation_failed",
    "crm_property_invalid",
    "crm_permission_denied",
    "reauthorization_required",
    "crm_rate_limited",
    "crm_provider_unavailable",
    "provider_fields_forbidden",
    "capability_not_invocable",
)

CRM_CONTACT_CODEGEN = (
    "Default for generic CRM contact work. Public path is "
    "POST /api/projects/{project_id}/capabilities/crm.contact.<verb>. "
    "create = new contact; upsert = create-or-update / sync / ensure-exists by email "
    "(payload {contact:{email,first_name,last_name,phone}}); do not choose upsert merely "
    "because email is present. Search is exact email only. Get/update use "
    "CRMObjectRef {object_type,external_id} — opaque connection-scoped id, not a P2HS "
    "record id. Example contact: {email:'customer@example.com',first_name:'Jane',"
    "last_name:'Smith'}. Never emit hubspot.create_contact, salesforce.create_contact, "
    "hs_object_id, External ID, or provider_hint for generic intent. Salesforce Lead "
    "and the crm.lead App Block are not CRMContact."
)

CRM_CONTACT_CREATE_INPUT_SCHEMA = {
    "type": "object",
    "properties": {"contact": {"type": "object"}},
    "required": ["contact"],
    "additionalProperties": False,
}
CRM_CONTACT_GET_INPUT_SCHEMA = {
    "type": "object",
    "properties": {"contact": {"type": "object"}},
    "required": ["contact"],
    "additionalProperties": False,
}
CRM_CONTACT_UPDATE_INPUT_SCHEMA = {
    "type": "object",
    "properties": {
        "contact": {"type": "object"},
        "changes": {"type": "object"},
    },
    "required": ["contact", "changes"],
    "additionalProperties": False,
}
CRM_CONTACT_UPSERT_INPUT_SCHEMA = {
    "type": "object",
    "properties": {"contact": {"type": "object"}},
    "required": ["contact"],
    "additionalProperties": False,
}
CRM_CONTACT_SEARCH_INPUT_SCHEMA = {
    "type": "object",
    "properties": {
        "email": {"type": "string"},
        "cursor": {"type": "string"},
    },
    "required": ["email"],
    "additionalProperties": False,
}
CRM_CONTACT_OUTPUT_SCHEMA = {
    "type": "object",
    "properties": {
        "capability": {"type": "string"},
        "status": {"type": "string"},
        "contact": {"type": "object"},
        "items": {"type": "array"},
        "next_cursor": {"type": "string"},
        "reason_code": {"type": "string"},
        "message": {"type": "string"},
    },
}

HUBSPOT_CONTACT_MAPPINGS = {
    "crm.contact.create": {"provider_action": "hubspot.create_contact"},
    "crm.contact.get": {"provider_action": "hubspot.get_contact"},
    "crm.contact.update": {"provider_action": "hubspot.update_contact"},
    "crm.contact.upsert": {"provider_action": "hubspot.upsert_contact"},
    "crm.contact.search": {"provider_action": "hubspot.search_contacts"},
}
SALESFORCE_CONTACT_MAPPINGS = {
    "crm.contact.create": {"provider_action": "salesforce.create_contact"},
    "crm.contact.get": {"provider_action": "salesforce.get_contact"},
    "crm.contact.update": {"provider_action": "salesforce.update_contact"},
    "crm.contact.upsert": {"provider_action": "salesforce.upsert_contact"},
    "crm.contact.search": {"provider_action": "salesforce.search_contacts"},
}

CRM_COMPANY_ERRORS: tuple[str, ...] = (
    "crm_unbound",
    "binding_ambiguous",
    "crm_company_not_found",
    "crm_duplicate_match",
    "crm_validation_failed",
    "crm_property_invalid",
    "crm_field_unsupported",
    "crm_permission_denied",
    "reauthorization_required",
    "crm_rate_limited",
    "crm_provider_unavailable",
    "provider_fields_forbidden",
    "capability_not_invocable",
)
CRM_COMPANY_CODEGEN = (
    "Default for generic CRM company work. Public path is "
    "POST /api/projects/{project_id}/capabilities/crm.company.<verb>. "
    "Canonical fields: name, domain, phone. For portable HubSpot+Salesforce generation, "
    "do not require domain (Salesforce may reject it). Search is exact name only — not "
    "domain. Example: {name:'Acme Corp',phone:'555-0100'}. Never emit "
    "hubspot.create_company, salesforce.create_account, or AccountId."
)
CRM_COMPANY_CREATE_INPUT_SCHEMA = {
    "type": "object",
    "properties": {"company": {"type": "object"}},
    "required": ["company"],
    "additionalProperties": False,
}
CRM_COMPANY_GET_INPUT_SCHEMA = {
    "type": "object",
    "properties": {"company": {"type": "object"}},
    "required": ["company"],
    "additionalProperties": False,
}
CRM_COMPANY_UPDATE_INPUT_SCHEMA = {
    "type": "object",
    "properties": {
        "company": {"type": "object"},
        "changes": {"type": "object"},
    },
    "required": ["company", "changes"],
    "additionalProperties": False,
}
CRM_COMPANY_SEARCH_INPUT_SCHEMA = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "cursor": {"type": "string"},
    },
    "required": ["name"],
    "additionalProperties": False,
}
CRM_COMPANY_OUTPUT_SCHEMA = {
    "type": "object",
    "properties": {
        "capability": {"type": "string"},
        "status": {"type": "string"},
        "company": {"type": "object"},
        "items": {"type": "array"},
        "next_cursor": {"type": "string"},
        "reason_code": {"type": "string"},
        "message": {"type": "string"},
    },
}
HUBSPOT_COMPANY_MAPPINGS = {
    "crm.company.create": {"provider_action": "hubspot.create_company"},
    "crm.company.get": {"provider_action": "hubspot.get_company"},
    "crm.company.update": {"provider_action": "hubspot.update_company"},
    "crm.company.search": {"provider_action": "hubspot.search_companies"},
}
SALESFORCE_COMPANY_MAPPINGS = {
    "crm.company.create": {"provider_action": "salesforce.create_account"},
    "crm.company.get": {"provider_action": "salesforce.get_account"},
    "crm.company.update": {"provider_action": "salesforce.update_account"},
    "crm.company.search": {"provider_action": "salesforce.search_accounts"},
}

CRM_DEAL_ERRORS: tuple[str, ...] = (
    "crm_unbound",
    "binding_ambiguous",
    "crm_deal_not_found",
    "crm_duplicate_match",
    "crm_validation_failed",
    "crm_property_invalid",
    "crm_field_unsupported",
    "crm_pipeline_unsupported",
    "crm_pipeline_unbound",
    "crm_stage_unbound",
    "crm_permission_denied",
    "reauthorization_required",
    "crm_rate_limited",
    "crm_provider_unavailable",
    "provider_fields_forbidden",
    "capability_not_invocable",
)
CRM_DEAL_CODEGEN = (
    "Default for generic CRM deal/opportunity work. Public path is "
    "POST /api/projects/{project_id}/capabilities/crm.deal.<verb>. "
    "stage is a project logical alias only — never dealstage, StageName, "
    "appointmentscheduled, closedwon, or Closed Won. Do not invent pipeline:'default'; "
    "omit pipeline unless the requirement explicitly needs it (HubSpot-only; Salesforce "
    "pipeline is rejected and is not RecordType). Search is exact name and/or stage (AND). "
    "Example: {name:'Enterprise Renewal',amount:25000,stage:'qualified'} only when "
    "qualified is a configured alias. Amount has no canonical currency."
)
CRM_DEAL_CREATE_INPUT_SCHEMA = {
    "type": "object",
    "properties": {"deal": {"type": "object"}},
    "required": ["deal"],
    "additionalProperties": False,
}
CRM_DEAL_GET_INPUT_SCHEMA = {
    "type": "object",
    "properties": {"deal": {"type": "object"}},
    "required": ["deal"],
    "additionalProperties": False,
}
CRM_DEAL_UPDATE_INPUT_SCHEMA = {
    "type": "object",
    "properties": {
        "deal": {"type": "object"},
        "changes": {"type": "object"},
    },
    "required": ["deal", "changes"],
    "additionalProperties": False,
}
CRM_DEAL_SEARCH_INPUT_SCHEMA = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "stage": {"type": "string"},
        "cursor": {"type": "string"},
    },
    "additionalProperties": False,
}
CRM_DEAL_OUTPUT_SCHEMA = {
    "type": "object",
    "properties": {
        "capability": {"type": "string"},
        "status": {"type": "string"},
        "deal": {"type": "object"},
        "items": {"type": "array"},
        "next_cursor": {"type": "string"},
        "reason_code": {"type": "string"},
        "message": {"type": "string"},
    },
}
HUBSPOT_DEAL_MAPPINGS = {
    "crm.deal.create": {"provider_action": "hubspot.create_deal"},
    "crm.deal.get": {"provider_action": "hubspot.get_deal"},
    "crm.deal.update": {"provider_action": "hubspot.update_deal"},
    "crm.deal.search": {"provider_action": "hubspot.search_deals"},
}
SALESFORCE_DEAL_MAPPINGS = {
    "crm.deal.create": {"provider_action": "salesforce.create_opportunity"},
    "crm.deal.get": {"provider_action": "salesforce.get_opportunity"},
    "crm.deal.update": {"provider_action": "salesforce.update_opportunity"},
    "crm.deal.search": {"provider_action": "salesforce.search_opportunities"},
}

CRM_ASSOCIATION_ERRORS: tuple[str, ...] = (
    "crm_unbound",
    "binding_ambiguous",
    "crm_association_unsupported",
    "crm_association_invalid",
    "crm_relationship_conflict",
    "crm_validation_failed",
    "crm_permission_denied",
    "reauthorization_required",
    "crm_rate_limited",
    "crm_provider_unavailable",
    "provider_fields_forbidden",
    "capability_not_invocable",
)
CRM_ASSOCIATION_CODEGEN = (
    "Default for generic CRM primary links. Public path is "
    "POST /api/projects/{project_id}/capabilities/crm.association.create. "
    "Supported pairs: contact→company and deal→company using CRMObjectRef "
    "{object_type,external_id}. Do not generate company→contact, company→deal, or "
    "contact↔deal. Do not emit association_type_id, AccountId, WhoId, WhatId, or "
    "OpportunityContactRole; those are provider_actions when explicitly required."
)
CRM_ASSOCIATION_CREATE_INPUT_SCHEMA = {
    "type": "object",
    "properties": {"association": {"type": "object"}},
    "required": ["association"],
    "additionalProperties": False,
}
CRM_ASSOCIATION_OUTPUT_SCHEMA = {
    "type": "object",
    "properties": {
        "capability": {"type": "string"},
        "status": {"type": "string"},
        "association": {"type": "object"},
        "reason_code": {"type": "string"},
        "message": {"type": "string"},
    },
}
HUBSPOT_ASSOCIATION_MAPPINGS = {
    "crm.association.create": {"provider_action": "hubspot.associate_objects"},
}
SALESFORCE_ASSOCIATION_MAPPINGS = {
    "crm.association.create": {
        "contact_company": "salesforce.update_contact",
        "deal_company": "salesforce.update_opportunity",
    },
}

INVOKE_CAPABILITIES: dict[str, InvokeCapabilitySpec] = {
    "email.send": {
        "id": "email.send",
        "status": "implemented",
        "request_type": "email.send",
        "method": "POST /api/jobs/email.send",
        "runtime": "queued",
        "codegen": (
            "Use this for generic transactional email. Runtime chooses platform email "
            "or a connected Resend/Mailgun/SES adapter. Do not call vendor action slugs. "
            "Switching the connected provider must not change generated application code. "
            "Generated apps call the installed same-origin BFF POST /api/jobs/email.send. "
            "Dashboard/capability invoke uses "
            "POST /api/projects/{project_id}/capabilities/email.send. "
            "Never construct a Core host (P2HS_BACKEND_URL, CORE_API_BASE_URL, beapipie)."
        ),
    },
    "messaging.send": {
        "id": "messaging.send",
        "status": "implemented",
        "invocable": True,
        "request_type": None,
        "method": MESSAGING_SEND_INVOKE_METHOD,
        "runtime": "integration.execute",
        "input_schema": MESSAGING_SEND_INPUT_SCHEMA,
        "output_schema": MESSAGING_SEND_OUTPUT_SCHEMA,
        "errors": MESSAGING_SEND_ERRORS,
        "adapter_mappings": {"slack": SLACK_MESSAGING_SEND_MAPPING},
        "codegen": (
            "Use this for generic operational/chat messages. Runtime resolves the "
            "project binding (Slack today; a future Teams connection must not change "
            "generated code). Do not emit POST /api/jobs/messaging.send. Do not call "
            "slack.post_message or api.slack.com for generic sends. "
            "slack.post_message is a provider_actions escape hatch only for Slack Block Kit, "
            "attachments, or Slack-specific API semantics. metadata is audit-only and "
            "must not carry blocks/attachments/formatting. "
            "Twilio SMS is messaging.sms.send, not messaging.send. "
            "Emit only the platform-relative public path; never a Core host."
        ),
    },
    "messaging.sms.send": {
        "id": "messaging.sms.send",
        "status": "implemented",
        "invocable": True,
        "request_type": "integration.execute",
        "method": "POST /api/projects/{project_id}/capabilities/messaging.sms.send",
        "runtime": "Core resolver + Cloud integration.execute",
        "input_schema": NOTIFICATION_SMS_INPUT_SCHEMA,
        "output_schema": NOTIFICATION_SMS_OUTPUT_SCHEMA,
        "errors": NOTIFICATION_SMS_ERRORS,
        "adapter_mappings": {"twilio": TWILIO_NOTIFICATION_SMS_MAPPING},
        "codegen": (
            "Emit only POST /api/projects/{project_id}/capabilities/messaging.sms.send — never a Core "
            "host. Do not fold SMS into messaging.send. Sender numbers and Messaging "
            "Service SIDs belong on the Twilio connection, not in generated app code."
        ),
    },
    "automation.workflow.trigger": {
        "id": "automation.workflow.trigger",
        "status": "implemented",
        "invocable": True,
        "request_type": None,
        "method": AUTOMATION_WORKFLOW_TRIGGER_INVOKE_METHOD,
        "runtime": "integration.execute",
        "input_schema": AUTOMATION_WORKFLOW_TRIGGER_INPUT_SCHEMA,
        "output_schema": AUTOMATION_WORKFLOW_TRIGGER_OUTPUT_SCHEMA,
        "errors": AUTOMATION_WORKFLOW_TRIGGER_ERRORS,
        "adapter_mappings": {
            "make_com": MAKE_AUTOMATION_WORKFLOW_MAPPING,
            "n8n": N8N_AUTOMATION_WORKFLOW_MAPPING,
        },
        "codegen": (
            "Use this for generic workflow triggers. Runtime resolves the project "
            "binding (Make or n8n). Switching the connected adapter must not change "
            "generated code. Do not emit POST /api/jobs/automation.workflow.trigger. "
            "workflow is a logical alias (e.g. lead-intake), never a Make scenario ID, "
            "n8n workflow ID, or webhook URL. make_com.trigger_scenario and "
            "n8n.trigger_workflow are provider_actions escape hatches for vendor "
            "webhook/API semantics. Public path is Dashboard/BFF only; never a Core "
            "host. Success returns only capability, status, and opaque execution_ref."
        ),
    },
    "document.create": {
        "id": "document.create",
        "status": "implemented",
        "invocable": True,
        "request_type": None,
        "method": DOCUMENT_INVOKE_METHODS["document.create"],
        "runtime": "integration.execute",
        "input_schema": DOCUMENT_CREATE_INPUT_SCHEMA,
        "output_schema": DOCUMENT_OUTPUT_SCHEMA,
        "errors": DOCUMENT_ERRORS,
        "adapter_mappings": {"google_docs": GOOGLE_DOCS_CREATE_MAPPING},
        "codegen": DOCUMENT_CREATE_CODEGEN,
    },
    "document.get": {
        "id": "document.get",
        "status": "implemented",
        "invocable": True,
        "request_type": None,
        "method": DOCUMENT_INVOKE_METHODS["document.get"],
        "runtime": "integration.execute",
        "input_schema": DOCUMENT_GET_INPUT_SCHEMA,
        "output_schema": DOCUMENT_OUTPUT_SCHEMA,
        "errors": DOCUMENT_ERRORS,
        "adapter_mappings": {"google_docs": GOOGLE_DOCS_GET_MAPPING},
        "codegen": DOCUMENT_CODEGEN_DEFAULT,
    },
    "document.insert_text": {
        "id": "document.insert_text",
        "status": "implemented",
        "invocable": True,
        "request_type": None,
        "method": DOCUMENT_INVOKE_METHODS["document.insert_text"],
        "runtime": "integration.execute",
        "input_schema": DOCUMENT_INSERT_TEXT_INPUT_SCHEMA,
        "output_schema": DOCUMENT_OUTPUT_SCHEMA,
        "errors": DOCUMENT_ERRORS,
        "adapter_mappings": {"google_docs": GOOGLE_DOCS_INSERT_TEXT_MAPPING},
        "codegen": DOCUMENT_CODEGEN_DEFAULT,
    },
    "document.append_text": {
        "id": "document.append_text",
        "status": "implemented",
        "invocable": True,
        "request_type": None,
        "method": DOCUMENT_INVOKE_METHODS["document.append_text"],
        "runtime": "integration.execute",
        "input_schema": DOCUMENT_APPEND_TEXT_INPUT_SCHEMA,
        "output_schema": DOCUMENT_OUTPUT_SCHEMA,
        "errors": DOCUMENT_ERRORS,
        "adapter_mappings": {"google_docs": GOOGLE_DOCS_APPEND_TEXT_MAPPING},
        "codegen": DOCUMENT_CODEGEN_DEFAULT,
    },
    "document.replace_text": {
        "id": "document.replace_text",
        "status": "implemented",
        "invocable": True,
        "request_type": None,
        "method": DOCUMENT_INVOKE_METHODS["document.replace_text"],
        "runtime": "integration.execute",
        "input_schema": DOCUMENT_REPLACE_TEXT_INPUT_SCHEMA,
        "output_schema": DOCUMENT_OUTPUT_SCHEMA,
        "errors": DOCUMENT_ERRORS,
        "adapter_mappings": {"google_docs": GOOGLE_DOCS_REPLACE_TEXT_MAPPING},
        "codegen": DOCUMENT_CODEGEN_DEFAULT,
    },
    "crm.contact.create": {
        "id": "crm.contact.create",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_CONTACT_CREATE_INPUT_SCHEMA,
        "output_schema": CRM_CONTACT_OUTPUT_SCHEMA,
        "errors": CRM_CONTACT_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_CONTACT_MAPPINGS["crm.contact.create"],
            "salesforce": SALESFORCE_CONTACT_MAPPINGS["crm.contact.create"],
        },
        "codegen": CRM_CONTACT_CODEGEN,
    },
    "crm.contact.get": {
        "id": "crm.contact.get",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_CONTACT_GET_INPUT_SCHEMA,
        "output_schema": CRM_CONTACT_OUTPUT_SCHEMA,
        "errors": CRM_CONTACT_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_CONTACT_MAPPINGS["crm.contact.get"],
            "salesforce": SALESFORCE_CONTACT_MAPPINGS["crm.contact.get"],
        },
        "codegen": CRM_CONTACT_CODEGEN,
    },
    "crm.contact.update": {
        "id": "crm.contact.update",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_CONTACT_UPDATE_INPUT_SCHEMA,
        "output_schema": CRM_CONTACT_OUTPUT_SCHEMA,
        "errors": CRM_CONTACT_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_CONTACT_MAPPINGS["crm.contact.update"],
            "salesforce": SALESFORCE_CONTACT_MAPPINGS["crm.contact.update"],
        },
        "codegen": CRM_CONTACT_CODEGEN,
    },
    "crm.contact.upsert": {
        "id": "crm.contact.upsert",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_CONTACT_UPSERT_INPUT_SCHEMA,
        "output_schema": CRM_CONTACT_OUTPUT_SCHEMA,
        "errors": CRM_CONTACT_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_CONTACT_MAPPINGS["crm.contact.upsert"],
            "salesforce": SALESFORCE_CONTACT_MAPPINGS["crm.contact.upsert"],
        },
        "codegen": CRM_CONTACT_CODEGEN,
    },
    "crm.contact.search": {
        "id": "crm.contact.search",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_CONTACT_SEARCH_INPUT_SCHEMA,
        "output_schema": CRM_CONTACT_OUTPUT_SCHEMA,
        "errors": CRM_CONTACT_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_CONTACT_MAPPINGS["crm.contact.search"],
            "salesforce": SALESFORCE_CONTACT_MAPPINGS["crm.contact.search"],
        },
        "codegen": CRM_CONTACT_CODEGEN,
    },
    "crm.company.create": {
        "id": "crm.company.create",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_COMPANY_CREATE_INPUT_SCHEMA,
        "output_schema": CRM_COMPANY_OUTPUT_SCHEMA,
        "errors": CRM_COMPANY_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_COMPANY_MAPPINGS["crm.company.create"],
            "salesforce": SALESFORCE_COMPANY_MAPPINGS["crm.company.create"],
        },
        "codegen": CRM_COMPANY_CODEGEN,
    },
    "crm.company.get": {
        "id": "crm.company.get",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_COMPANY_GET_INPUT_SCHEMA,
        "output_schema": CRM_COMPANY_OUTPUT_SCHEMA,
        "errors": CRM_COMPANY_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_COMPANY_MAPPINGS["crm.company.get"],
            "salesforce": SALESFORCE_COMPANY_MAPPINGS["crm.company.get"],
        },
        "codegen": CRM_COMPANY_CODEGEN,
    },
    "crm.company.update": {
        "id": "crm.company.update",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_COMPANY_UPDATE_INPUT_SCHEMA,
        "output_schema": CRM_COMPANY_OUTPUT_SCHEMA,
        "errors": CRM_COMPANY_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_COMPANY_MAPPINGS["crm.company.update"],
            "salesforce": SALESFORCE_COMPANY_MAPPINGS["crm.company.update"],
        },
        "codegen": CRM_COMPANY_CODEGEN,
    },
    "crm.company.search": {
        "id": "crm.company.search",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_COMPANY_SEARCH_INPUT_SCHEMA,
        "output_schema": CRM_COMPANY_OUTPUT_SCHEMA,
        "errors": CRM_COMPANY_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_COMPANY_MAPPINGS["crm.company.search"],
            "salesforce": SALESFORCE_COMPANY_MAPPINGS["crm.company.search"],
        },
        "codegen": CRM_COMPANY_CODEGEN,
    },
    "crm.deal.create": {
        "id": "crm.deal.create",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_DEAL_CREATE_INPUT_SCHEMA,
        "output_schema": CRM_DEAL_OUTPUT_SCHEMA,
        "errors": CRM_DEAL_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_DEAL_MAPPINGS["crm.deal.create"],
            "salesforce": SALESFORCE_DEAL_MAPPINGS["crm.deal.create"],
        },
        "codegen": CRM_DEAL_CODEGEN,
    },
    "crm.deal.get": {
        "id": "crm.deal.get",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_DEAL_GET_INPUT_SCHEMA,
        "output_schema": CRM_DEAL_OUTPUT_SCHEMA,
        "errors": CRM_DEAL_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_DEAL_MAPPINGS["crm.deal.get"],
            "salesforce": SALESFORCE_DEAL_MAPPINGS["crm.deal.get"],
        },
        "codegen": CRM_DEAL_CODEGEN,
    },
    "crm.deal.update": {
        "id": "crm.deal.update",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_DEAL_UPDATE_INPUT_SCHEMA,
        "output_schema": CRM_DEAL_OUTPUT_SCHEMA,
        "errors": CRM_DEAL_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_DEAL_MAPPINGS["crm.deal.update"],
            "salesforce": SALESFORCE_DEAL_MAPPINGS["crm.deal.update"],
        },
        "codegen": CRM_DEAL_CODEGEN,
    },
    "crm.deal.search": {
        "id": "crm.deal.search",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_DEAL_SEARCH_INPUT_SCHEMA,
        "output_schema": CRM_DEAL_OUTPUT_SCHEMA,
        "errors": CRM_DEAL_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_DEAL_MAPPINGS["crm.deal.search"],
            "salesforce": SALESFORCE_DEAL_MAPPINGS["crm.deal.search"],
        },
        "codegen": CRM_DEAL_CODEGEN,
    },
    "crm.association.create": {
        "id": "crm.association.create",
        "status": "contract",
        "invocable": False,
        "request_type": None,
        "method": None,
        "runtime": "integration.execute",
        "input_schema": CRM_ASSOCIATION_CREATE_INPUT_SCHEMA,
        "output_schema": CRM_ASSOCIATION_OUTPUT_SCHEMA,
        "errors": CRM_ASSOCIATION_ERRORS,
        "adapter_mappings": {
            "hubspot": HUBSPOT_ASSOCIATION_MAPPINGS["crm.association.create"],
            "salesforce": SALESFORCE_ASSOCIATION_MAPPINGS["crm.association.create"],
        },
        "codegen": CRM_ASSOCIATION_CODEGEN,
    },
    "payments.checkout.create": {
        "id": "payments.checkout.create",
        "status": "implemented",
        "invocable": True,
        "request_type": "payment.checkout.create",
        "method": "POST /api/projects/{project_id}/capabilities/payments.checkout.create",
        "runtime": "queued",
        "codegen": (
            "Use this for provider-neutral checkout. Runtime resolves the bound "
            "Stripe or PayPal connection. Do not call stripe.create_checkout or "
            "paypal.create_order. Do not send amounts from the browser; items "
            "reference trusted Cloud price records. Generated apps may keep "
            "POST /api/jobs/payment.checkout.create as a compatibility port. "
            "Dashboard invoke uses POST /api/projects/{project_id}/capabilities/"
            "payments.checkout.create. Never construct a Core host."
        ),
    },
    "payments.order.capture": {
        "id": "payments.order.capture",
        "status": "implemented",
        "invocable": True,
        "request_type": "payment.capture",
        "method": "POST /api/projects/{project_id}/capabilities/payments.order.capture",
        "runtime": "queued",
        "codegen": (
            "Capture an authorized checkout order. Runtime resolves Stripe or PayPal. "
            "Do not call provider capture actions. Generated apps may keep "
            "POST /api/jobs/payment.capture as a compatibility port. Never a Core host."
        ),
    },
    "payments.refund.create": {
        "id": "payments.refund.create",
        "status": "implemented",
        "invocable": True,
        "request_type": "payment.refund",
        "method": "POST /api/projects/{project_id}/capabilities/payments.refund.create",
        "runtime": "queued",
        "codegen": (
            "Refund a trusted checkout order. Runtime resolves Stripe or PayPal. "
            "Refund ceiling comes from trusted order state, not a browser amount as "
            "authority. Generated apps may keep POST /api/jobs/payment.refund. "
            "Never a Core host."
        ),
    },
    "payments.catalog.upsert": {
        "id": "payments.catalog.upsert",
        "status": "implemented",
        "invocable": True,
        "request_type": "payment.catalog.upsert",
        "method": "POST /api/projects/{project_id}/capabilities/payments.catalog.upsert",
        "runtime": "queued",
        "codegen": (
            "Upsert trusted product/price records. Stripe creates Product+Price; "
            "PayPal stores Cloud unit_amount only. Do not call stripe.create_product "
            "for generic catalog writes. Generated apps may keep "
            "POST /api/jobs/payment.catalog.upsert. Never a Core host."
        ),
    },
}

PROVIDER_ACTION_WHEN: dict[str, str] = {
    "resend.send_email": (
        "Only when the user explicitly requires Resend API semantics "
        "(tags, templates, Resend scheduled_at, batch)."
    ),
    "mailgun.send_email": (
        "Only when the user explicitly requires Mailgun API semantics "
        "(recipient-variables, Mailgun templates, tracking flags)."
    ),
    "ses.send_email": (
        "Only when the user explicitly requires Amazon SES API semantics "
        "(configuration sets, SES-specific tags)."
    ),
    "slack.post_message": (
        "Only when the user explicitly requires Slack API semantics "
        "(Block Kit, attachments, mrkdwn, unfurl_links, icon_emoji)."
    ),
    "make_com.trigger_scenario": (
        "Only when the user explicitly requires Make webhook/API semantics "
        "(scenario IDs, scenario variables, Make API run-by-id)."
    ),
    "n8n.trigger_workflow": (
        "Only when the user explicitly requires n8n webhook/API semantics "
        "(workflow IDs, Header Auth details, REST execute-by-id)."
    ),
    "google_docs.create_document": (
        "Only when the user explicitly requires Google Docs API create "
        "semantics (provider document_id return, Drive parent)."
    ),
    "google_docs.get_document": (
        "Only when the user explicitly requires Google document_id or "
        "revision payloads."
    ),
    "google_docs.insert_text": (
        "Only when the user explicitly requires a numeric Google Docs index."
    ),
    "google_docs.append_text": (
        "Only when the user explicitly requires Google Docs API append "
        "semantics."
    ),
    "google_docs.replace_all_text": (
        "Only when the user explicitly requires match_case or other Google "
        "replace-all API semantics."
    ),
    "hubspot.create_contact": (
        "Only when the user explicitly requires HubSpot Contacts API semantics, "
        "not generic CRM contact create."
    ),
    "hubspot.update_contact": (
        "Only when the user explicitly requires HubSpot-specific contact properties "
        "such as lifecycle stage; generic updates use crm.contact.update."
    ),
    "hubspot.upsert_contact": (
        "Only when the user explicitly requires HubSpot upsert/id semantics; "
        "generic create-or-update by email is crm.contact.upsert."
    ),
    "hubspot.search_contacts": (
        "Only when the user explicitly requires HubSpot filterGroups, sorts, or "
        "complex search; generic email lookup is crm.contact.search."
    ),
    "hubspot.associate_objects": (
        "Only when the user explicitly requires HubSpot association labels, "
        "association_type_id, or pairs outside contact→company and deal→company."
    ),
    "hubspot.upsert_company": (
        "Only when the user explicitly requires HubSpot company upsert semantics."
    ),
    "hubspot.upsert_deal": (
        "Only when the user explicitly requires HubSpot deal upsert semantics."
    ),
    "salesforce.create_contact": (
        "Only when the user explicitly requires Salesforce Contact API semantics, "
        "not generic CRM contact create."
    ),
    "salesforce.upsert_contact": (
        "Only when the user explicitly requires Salesforce External ID upsert; "
        "generic create-or-update by email is crm.contact.upsert."
    ),
    "salesforce.search_contacts": (
        "Only when the user explicitly requires Salesforce-native contact query "
        "beyond exact email."
    ),
    "salesforce.query_soql": (
        "Only when the user explicitly requires Salesforce SOQL."
    ),
    "salesforce.create_lead": (
        "Only when the user explicitly requires Salesforce Lead; it is not crm.contact."
    ),
    "salesforce.create_task": (
        "Only when the user explicitly requires Salesforce Task."
    ),
    "salesforce.create_event": (
        "Only when the user explicitly requires Salesforce Event."
    ),
    "salesforce.upsert_record": (
        "Only when the user explicitly requires Salesforce External ID / custom SObject upsert."
    ),
    "salesforce.associate_record": (
        "Only when the user explicitly requires Salesforce OpportunityContactRole "
        "or other non-canonical association semantics."
    ),
    "salesforce.create_opportunity": (
        "Only when the user explicitly requires Salesforce Opportunity API semantics, "
        "not generic crm.deal.create."
    ),
}


INVOKE_CAPABILITIES.update(resource_specs())

for _crm_verb in CRM_PUBLIC_INVOKE_VERBS:
    _crm_spec = INVOKE_CAPABILITIES[_crm_verb]
    _crm_spec["status"] = "implemented"
    _crm_spec["invocable"] = True
    _crm_spec["method"] = CRM_INVOKE_METHODS[_crm_verb]


def is_implemented(capability_id: str) -> bool:
    spec = INVOKE_CAPABILITIES.get(capability_id) or {}
    return spec.get("status") == "implemented"


def capability_surface(capability_id: str, implemented_by: list[str]) -> dict[str, Any]:
    spec = INVOKE_CAPABILITIES[capability_id]
    implemented = spec.get("status") == "implemented"
    if "invocable" in spec:
        invocable = bool(spec["invocable"])
    else:
        invocable = implemented and bool(spec.get("method") or spec.get("request_type"))
    surface: dict[str, Any] = {
        "id": spec["id"],
        "status": spec.get("status") or "contract",
        "invocable": invocable,
        "request_type": spec.get("request_type"),
        "method": spec.get("method"),
        "runtime": spec.get("runtime") or "none",
        "role": "default" if implemented else "contract",
        "implemented_by": list(implemented_by),
        "codegen": spec["codegen"],
    }
    if not surface["invocable"]:
        surface["invocable_means"] = INVOCABLE_FALSE_MEANS
    if spec.get("input_schema"):
        surface["input_schema"] = spec["input_schema"]
    if spec.get("output_schema"):
        surface["output_schema"] = spec["output_schema"]
    if spec.get("errors"):
        surface["errors"] = list(spec["errors"])
    if spec.get("adapter_mappings"):
        surface["adapter_mappings"] = spec["adapter_mappings"]
    from src.domain.connectors.runtime_surfaces import invoke_spec_metadata

    meta = invoke_spec_metadata(capability_id)
    for key in ("surface", "boundary", "resolution", "storage_scope", "platform_fallback"):
        if key in meta:
            surface[key] = meta[key]
    return surface


def catalog_consistency_issues() -> tuple[str, ...]:
    """Platform-wide catalog/runtime invariants. Not CRM-only."""
    from src.domain.connectors.catalog import CONNECTORS, slugs_implementing
    from src.domain.connectors.public_gateway import (
        is_public_bff_proxy_capability,
        invoke_path_has_host,
        public_capability_invoke_method,
    )

    issues: list[str] = []
    known = set(INVOKE_CAPABILITIES)
    for capability_id, spec in INVOKE_CAPABILITIES.items():
        slugs = list(slugs_implementing(capability_id))
        surface = capability_surface(capability_id, slugs)
        method = spec.get("method")
        if surface["invocable"]:
            if not method:
                issues.append(f"{capability_id}: invocable=true without public method")
            elif invoke_path_has_host(str(method)):
                issues.append(f"{capability_id}: public method includes a host")
            if not is_public_bff_proxy_capability(capability_id):
                issues.append(f"{capability_id}: invocable=true missing BFF allowlist")
            if capability_id in CRM_PUBLIC_INVOKE_VERBS:
                expected = public_capability_invoke_method(capability_id)
                if str(method) != expected:
                    issues.append(
                        f"{capability_id}: method {method!r} is not {expected!r}"
                    )
        if surface["role"] == "default" and not surface["invocable"]:
            issues.append(f"{capability_id}: role=default requires invocable=true")
        if spec.get("status") == "implemented" and not slugs:
            issues.append(f"{capability_id}: implemented without provider implements")
        if slugs != list(slugs_implementing(capability_id)):
            issues.append(f"{capability_id}: implemented_by is not derived")
    for connector in CONNECTORS.values():
        for action in connector.actions:
            for capability_id in action.implements:
                if capability_id not in known:
                    issues.append(
                        f"{action.slug}: implements unknown capability {capability_id}"
                    )
    from src.domain.connectors.runtime_surfaces import catalog_invariants

    issues.extend(catalog_invariants())
    return tuple(issues)
