#!/usr/bin/env bash
set -euo pipefail

ROOT="${AGENTTEAM_MESSAGE_AUTHOR_SOURCE_GUARDRAIL_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
cd "$ROOT"

failures=0

fail() {
  printf 'FAIL: %s\n' "$1" >&2
  failures=$((failures + 1))
}

require_contains() {
  local file="$1"
  local pattern="$2"
  local message="$3"
  if [[ ! -f "$file" ]]; then
    fail "$message (missing $file)"
    return
  fi
  if ! rg -U -q -P -- "$pattern" "$file"; then
    fail "$message"
  fi
}

require_not_contains() {
  local file="$1"
  local pattern="$2"
  local message="$3"
  if [[ ! -f "$file" ]]; then
    fail "$message (missing $file)"
    return
  fi
  if rg -U -q -P -- "$pattern" "$file"; then
    fail "$message"
  fi
}

require_file_absent() {
  local file="$1"
  local message="$2"
  if [[ -e "$file" ]]; then
    fail "$message ($file must not exist)"
  fi
}

check_pattern_matches_examples() {
  local pattern="$1"
  local description="$2"
  shift 2

  local example
  for example in "$@"; do
    if ! printf '%s\n' "$example" | rg -q -P -- "$pattern"; then
      fail "$description regex must match common spelling: $example"
    fi
  done
}

check_no_product_core_sender_types() {
  local pattern matches unexpected
  pattern="[\"'\\x60]?(?:sender(?:_type|Type)|SenderType|author(?:_type|Type)|AuthorType)[\"'\\x60]?\\s*[=:]\\s*[\"'\\x60]?(workflow|bot|provider|trigger|app)\\b"

  check_pattern_matches_examples "$pattern" "core sender/author type denylist" \
    'sender_type = "app"' \
    'senderType: "app"' \
    '"senderType": "app"' \
    '"sender_type": "trigger"' \
    'SenderType = "bot"' \
    'author_type="provider"' \
    'authorType: "trigger"' \
    '"authorType": "provider"' \
    '"author_type": "bot"' \
    'AuthorType = "workflow"'

  matches="$(rg -n -P \
    --glob '*.{go,ts,tsx,md,sql,sh}' \
    -- "$pattern" \
    internal web desktop specs guardrails 2>/dev/null || true)"
  unexpected="$(printf '%s\n' "$matches" | sed '/^$/d' \
    | grep -v -E '^specs/app/app-system.md:.*does not introduce.*(sender_?type|author_?type)[[:space:]]*=[[:space:]]*app' \
    | grep -v '^guardrails/message/author-source-substrate' \
    || true)"
  if [[ -n "$unexpected" ]]; then
    fail "workflow/bot/app/provider/trigger must not become core sender_type or author_type values"
    printf '%s\n' "$unexpected" >&2
  fi
}

check_no_future_service_kind_reservations() {
  local pattern matches unexpected
  pattern="(?:[\"'\\x60]?(?:service(?:_kind|Kind)|ServiceKind|senderServiceKind|SenderServiceKind)[\"'\\x60]?\\s*[=:]\\s*[\"'\\x60]?bot\\b|(?:ServiceKind|SenderServiceKind)Bot\\b)"

  check_pattern_matches_examples "$pattern" "future service-kind reservation denylist" \
    'service_kind = "bot"' \
    'serviceKind: "bot"' \
    '"service_kind": "bot"' \
    'ServiceKind = "bot"' \
    'ServiceKindBot' \
    'SenderServiceKindBot'

  matches="$(rg -n -P \
    --glob '*.{go,ts,tsx,md,sql,sh}' \
    -- "$pattern" \
    internal web desktop specs guardrails 2>/dev/null || true)"
  unexpected="$(printf '%s\n' "$matches" | sed '/^$/d' \
    | grep -v '^specs/collaboration/source-projection-delivery.md:' \
    | grep -v '^guardrails/message/author-source-substrate' \
    || true)"
  if [[ -n "$unexpected" ]]; then
    fail "bot service_kind values must not be pre-reserved without a complete product implementation"
    printf '%s\n' "$unexpected" >&2
  fi
}

# The automation service author contract: workflow and schedule are
# first-class AuthorProfile service kinds that identify the visible author of a
# workflow_run/schedule_run message. The frozen Human member stays the separate
# authorization/audit principal (author_member_id, notify/idempotency subjects,
# permission fences). This check codifies author != execution authority: a
# automation service author profile must never be used to authorize, attribute
# unread/notify to, or self-suppress for the authorizing Human.
check_automation_service_author_contract() {
  local pattern matches unexpected
  pattern="(?:AuthorServiceKind(?:Workflow|Schedule)|AuthorServiceKindWorkflow|AuthorServiceKindSchedule|serviceKind:\\s*[\"'\\x60]?(workflow|schedule)|serviceKind\\s*=\\s*[\"'\\x60]?(workflow|schedule))"

  check_pattern_matches_examples "$pattern" "canonical automation service kind wiring" \
    'AuthorServiceKindWorkflow' \
    'AuthorServiceKindSchedule' \
    'serviceKind: "workflow"' \
    'serviceKind: "schedule"'

  require_contains \
    internal/store/message/automation_service_author.go \
    'func AutomationServiceAuthorForSource' \
    "automation service author must be derived from the typed contribution source"

  require_contains \
    internal/store/message/automation_service_author.go \
    'ApplyAutomationServiceAuthorToAppMessage' \
    "automation service author must be overlaid on AppMessage read models"

  require_contains \
    internal/store/message/automation_service_author.go \
    'ApplyAutomationServiceAuthorToHistoryMessage' \
    "automation service author must be overlaid on HistoryMessage read models"

  # author != execution authority: the visible automation author must not leak
  # into Human authorization/audit attribution surfaces.
  require_not_contains \
    internal/store/message/automation_service_author.go \
    'author_member_id\s*=\s*|AuthorMemberID:\s*.*Workflow|NotifyPrincipalMemberID:\s*.*AuthorServiceKind' \
    "automation service author must not replace the frozen Human authorization/audit principal"

  require_contains \
    desktop/internal/types/session.go \
    'func ResolveAuthorIdentityWithAutomation' \
    "desktop canonical author resolution must apply the automation source fallback"

  # Reply-thread-root previews must route through the source resolver and the
  # canonical automation-author overlay, never serialize the frozen Human from
  # the raw ref author member directly.
  require_contains \
    internal/store/message/scope_api.go \
    'replyThreadRootMetadataFromVisibleRef' \
    "reply-thread-root metadata projection must build from the canonical AppMessage projection"

  require_contains \
    internal/store/message/scope_api.go \
    'appMessagesFromVisibleRefs\(ctx, workspaceID, \[\]VisibleMessageRef\{ref\}\)' \
    "reply-thread-root metadata must route through the source-attached AppMessage projection"

  require_contains \
    internal/store/message/scope_api.go \
    'contributionSourceMetadata\(appMessage\.Source\)' \
    "reply-thread-root metadata must carry the typed source as provenance"

  require_contains \
    desktop/internal/types/session.go \
    'root.Source' \
    "desktop ReplyThreadRoot must parse and carry the typed source"
}

check_no_web_message_sender_truth_paths() {
  local pattern matches unexpected
  pattern="(?:message|m|mention|reaction|sourceMessage)\\.sender(?:Type|Id)"

  matches="$(rg -n -P \
    --glob '*.ts' \
    --glob '*.tsx' \
    --glob '!**/*.test.ts' \
    --glob '!**/*.test.tsx' \
    -- "$pattern" \
    web/src 2>/dev/null || true)"
  unexpected="$(printf '%s\n' "$matches" | sed '/^$/d' \
    | grep -v '^web/src/agentteam/workspace/conversation/state/messageAuthorProfile\.ts:' \
    | grep -v '^web/src/agentteam/workspace/conversation/state/ownerPrivateDelivery\.ts:' \
    || true)"
  if [[ -n "$unexpected" ]]; then
    fail "web message author/source behavior must use the canonical author resolver instead of direct message/mention/reaction/sourceMessage sender fields"
    printf '%s\n' "$unexpected" >&2
  fi
}

check_no_web_message_sender_display_truth_paths() {
  local pattern matches unexpected
  pattern="(?:message|m|mention|reaction|sourceMessage)\\.sender(?:Name|DisplayName)"

  matches="$(rg -n -P \
    --glob '*.ts' \
    --glob '*.tsx' \
    --glob '!**/*.test.ts' \
    --glob '!**/*.test.tsx' \
    -- "$pattern" \
    web/src 2>/dev/null || true)"
  unexpected="$(printf '%s\n' "$matches" | sed '/^$/d' \
    | grep -v '^web/src/agentteam/workspace/conversation/state/messageAuthorProfile\.ts:' \
    | grep -v '^web/src/agentteam/workspace/conversation/state/ownerPrivateDelivery\.ts:' \
    | grep -v '^web/src/agentteam/workspace/shared/queryCache\.ts:' \
    || true)"
  if [[ -n "$unexpected" ]]; then
    fail "web message author display behavior must use canonical author resolver/helpers instead of direct message senderName/senderDisplayName fields"
    printf '%s\n' "$unexpected" >&2
  fi
}

check_no_web_sender_legacy_naming() {
  local matches unexpected
  matches="$(rg -n -P \
    --glob '*.ts' \
    --glob '*.tsx' \
    --glob '!**/*.test.ts' \
    --glob '!**/*.test.tsx' \
    -- 'sender|Sender|privateAgentSender|messageSenderNavigation' \
    web/src 2>/dev/null || true)"
  unexpected="$(printf '%s\n' "$matches" | sed '/^$/d' || true)"
  if [[ -n "$unexpected" ]]; then
    fail "web production code must not keep legacy sender naming after browser/API/socket sender wire deletion"
    printf '%s\n' "$unexpected" >&2
  fi
}

check_private_agent_contribution_capability_naming() {
  local matches
  matches="$(rg -n -P \
    --glob '*.{go,ts,tsx,md}' \
    -- 'PrivateAgentContextSenderCapability|privateAgentSenderCapability|PrivateAgentSenderCapability|private-agent-sender-capability|sender capability|sender_capability' \
    internal web specs/task 2>/dev/null || true)"
  if [[ -n "$(printf '%s\n' "$matches" | sed '/^$/d')" ]]; then
    fail "private-agent context API must use contribution capability naming, not sender capability"
    printf '%s\n' "$matches" >&2
  fi
}

check_desktop_author_identity_paths() {
  require_contains \
    desktop/internal/types/session.go \
    'func ResolveAuthorIdentity' \
    "desktop message author identity must be centralized in types.ResolveAuthorIdentity"

  require_contains \
    desktop/internal/types/metadata.go \
    'AuthorTargetID\(\) string' \
    "desktop metadata normalization must expose author target semantics"

  require_not_contains \
    desktop/internal/types/metadata.go \
    'SenderID|FromUserID|SenderType\s+string' \
    "desktop metadata normalization must not expose sender-named author fields"

  require_contains \
    desktop/internal/dispatcher/dispatcher.go \
    'normalized\.AuthorTargetID' \
    "dispatcher session binding must derive ordinary authors from normalized AuthorTargetID"

  require_not_contains \
    desktop/internal/dispatcher/dispatcher.go \
    'normalized\.SenderID' \
    "dispatcher session binding must not consume sender-named metadata normalization"

  require_not_contains \
    desktop/internal/channel/agentteam/handler.go \
    'SenderType:\s+dm\.SenderType|FromUserID|\"sender\"' \
    "AgentTeam receive adapter must not write sender-named fields into AgentTeamMeta"

  require_contains \
    desktop/internal/promptcompiler/text.go \
    'msg\.AuthorIdentity\(\)\.ActorType' \
    "desktop prompt compiler must branch on canonical AuthorIdentity actor type"

  require_contains \
    desktop/internal/promptcompiler/text.go \
    'root\.AuthorIdentity\(\)' \
    "desktop prompt compiler must render reply-root authors through AuthorIdentity"

  require_contains \
    desktop/internal/promptcompiler/model.go \
    'AuthorDisplayName\s+string\s+`json:"authorDisplayName,omitempty"`' \
    "desktop delivery envelope must carry canonical author display naming, not sender naming"

  require_contains \
    desktop/internal/promptcompiler/model.go \
    'AuthorIdentity\s+types\.AuthorIdentity' \
    "desktop delivery compiler input must accept resolved AuthorIdentity from the adapter boundary"

  require_not_contains \
    desktop/internal/promptcompiler/model.go \
    'Sender(Type|Name)|Sender\s+string\s+`json:"sender' \
    "desktop prompt compiler delivery model must not define legacy sender fields"

  require_contains \
    desktop/internal/agent/process_delivery.go \
    'AuthorIdentity:\s+author' \
    "desktop process delivery must resolve message AuthorIdentity before entering the prompt compiler"

  require_contains \
    desktop/internal/agent/process_delivery.go \
    'AuthorIdentity:\s+msg\.AuthorIdentity\(\)' \
    "desktop stdin delivery fallback must pass resolved AuthorIdentity into the prompt compiler"

  require_not_contains \
    desktop/internal/agent/process_delivery.go \
    'Sender(Type|Name):\s+strings\.TrimSpace\(msg\.Sender(Type|Name)\)' \
    "desktop process delivery must not pass legacy sender fields into the prompt compiler"

  require_not_contains \
    desktop/internal/promptcompiler/compiler_test.go \
    'Sender(Type|Name)|sender(Type|Name)' \
    "prompt compiler tests must construct normal messages with AuthorProfile/AuthorIdentity, not legacy sender fields"

  require_not_contains \
    desktop/internal/promptcompiler/compiler_golden_test.go \
    'Sender(Type|Name)|sender(Type|Name)' \
    "prompt compiler golden tests must construct normal messages with AuthorProfile/AuthorIdentity, not legacy sender fields"

  require_not_contains \
    desktop/internal/promptcompiler/text_test.go \
    'Sender(Type|Name)|sender(Type|Name)' \
    "prompt compiler text tests must construct normal messages with AuthorProfile, not legacy sender fields"

  require_not_contains \
    desktop/internal/dispatcher/prompt_compiler_test.go \
    'Sender(Type|Name):|sender_(type|name)' \
    "dispatcher prompt compiler tests must construct normal messages with AuthorProfile, not legacy sender fields"

  require_not_contains \
    desktop/internal/dispatcher/process_launch_test.go \
    'Sender(Type|Name):|sender_(type|name)' \
    "dispatcher launch tests must construct normal messages with AuthorProfile, not legacy sender fields"

  require_not_contains \
    desktop/internal/dispatcher/deferred_continuation.go \
    'type deferredContinuationMessage struct \{[^}]*Sender(Type|Name)' \
    "deferred continuation payloads must write canonical AuthorProfile, not legacy sender fields"

  require_contains \
    desktop/internal/dispatcher/deferred_continuation.go \
    'oldDaemonDeferredPayloadCompat' \
    "deferred continuation may retain only an explicitly named old-daemon compatibility reader"

  require_contains \
    specs/daemon/prompt-compiler-architecture.md \
    'AuthorDisplayName string' \
    "prompt compiler architecture spec must name delivery authors canonically"

  require_not_contains \
    specs/daemon/prompt-compiler-architecture.md \
    'Sender\s+string|EvidenceClass.*Sender|`Sender`' \
    "prompt compiler architecture spec must not model delivery envelope authors as legacy sender fields"

  require_not_contains \
    desktop/prompts/channel/agentteam/reference.md \
    '\bsender\b|\bSender\b' \
    "runtime AgentTeam prompt reference must describe message authors, not sender legacy wording"

  require_not_contains \
    desktop/internal/channel/agentteam/testdata/turn_context_agentteam.rendered_turn.md \
    '\bsender\b|\bSender\b' \
    "runtime AgentTeam prompt golden must describe message authors, not sender legacy wording"

  require_not_contains \
    specs/message/scope-model.md \
    'sender effective|private sender|owner private sender|same-owner private sender' \
    "scope model must describe private visibility through author/member intervals, not sender wording"

  require_not_contains \
    specs/product/overview.md \
    'sender_(type|name)|senderServiceKind|senderServiceSlug' \
    "product overview must describe canonical authorProfile instead of legacy sender wire"

  require_not_contains \
    specs/identity/identity.md \
    'sender(Name|DisplayName|ServiceKind|ServiceSlug|_name)|message sender label' \
    "identity spec must describe author/profile labels and keep legacy sender only at named compat boundaries"

  require_not_contains \
    specs/message/actions.md \
    'sender (group|header)' \
    "message actions spec must name author grouping/header, not sender wording"

  require_not_contains \
    specs/message/search.md \
    'sender/source intervals' \
    "message search spec must describe author/source visibility intervals"

  require_not_contains \
    specs/storage/storage.md \
    'sender/source intervals' \
    "storage spec must describe author/source visibility intervals"

  require_contains \
    desktop/internal/channel/agentteam/mcp_format.go \
    'msg\.AuthorIdentity\(\)' \
    "desktop MCP message formatting must render authors through AuthorIdentity"

  require_contains \
    desktop/internal/channel/agentteam/mcp_format.go \
    'root\.AuthorIdentity\(\)' \
    "desktop MCP root formatting must render authors through AuthorIdentity"

  require_contains \
    desktop/internal/channel/agentteam/mcp_tools_chat.go \
    'message\.AuthorIdentity\(\)' \
    "desktop MCP chat history must render authors through AuthorIdentity"

  require_contains \
    desktop/internal/dispatcher/dispatcher.go \
    'h\.AuthorIdentity\(\)\.ActorType' \
    "desktop dispatcher history policy must branch on canonical AuthorIdentity actor type"

  require_contains \
    desktop/internal/dispatcher/dispatcher.go \
    'msg\.AuthorIdentity\(\)\.DisplayName' \
    "desktop dispatcher owner-context formatting must render authors through AuthorIdentity"
}

check_reaction_receive_message_author_profile() {
  require_contains \
    internal/store/message/reactions.go \
    'AuthorProfile\s+AuthorProfile' \
    "reaction receive message must carry canonical AuthorProfile"

  require_contains \
    internal/store/reaction/helpers.go \
    'AuthorProfile:\s+builtMessage\.AuthorProfile' \
    "reaction ReceiveMessage builder must pass canonical AuthorProfile into active agent wake payloads"

  require_contains \
    desktop/internal/channel/agentteam/agentteam_test.go \
    'from: @Profile Reactor' \
    "desktop reaction receive formatting test must prove AuthorProfile overrides legacy senderName"
}

check_reaction_reactor_author_profile() {
  require_contains \
    internal/api/dto/agent_messages.go \
    'type MessageReactionReactor struct \{\s*AuthorProfile \*AuthorProfile' \
    "reaction aggregate reactor DTO must expose canonical AuthorProfile"

  require_contains \
    desktop/internal/channel/agentteam/mcp_tools_reactions.go \
    'AuthorProfile types\.AuthorProfile `json:"authorProfile"`' \
    "desktop MCP reaction aggregate must decode canonical AuthorProfile"

  require_not_contains \
    desktop/internal/channel/agentteam/mcp_tools_reactions.go \
    'Sender(Type|Name)|MemberID\s+string `json:"memberId"`|reactor\.(SenderName|MemberID)' \
    "desktop MCP reaction summary must not decode or consume legacy reactor sender/member fields"

  require_contains \
    desktop/internal/channel/agentteam/agentteam_test.go \
    'reactor names omitted' \
    "desktop MCP reaction tool must keep reaction summaries count-only instead of legacy reactor sender formatting"

  require_not_contains \
    internal/api/dto/agent_messages.go \
    'MessageReactionReactor struct \{[^}]*Sender(Type|Name)' \
    "reaction aggregate reactor DTO must not expose legacy sender fields"

  require_contains \
    internal/api/dto/message_responses.go \
    'type MessageReaction struct \{[^}]*AuthorProfile \*AuthorProfile' \
    "reaction realtime DTO must expose canonical AuthorProfile"

  require_not_contains \
    internal/api/dto/message_responses.go \
    'Reactor(Sender|Member)' \
    "reaction realtime DTO must not expose legacy reactor sender/member fields"

  require_contains \
    web/src/types.ts \
    'export type MessageReactionReactor = \{\s*authorProfile: AuthorProfile' \
    "web reaction aggregate reactor type must be profile-based"

  require_contains \
    web/src/types.ts \
    'export type MessageReactionRecord = \{[^}]*authorProfile: AuthorProfile' \
    "web reaction realtime record type must be profile-based"

  require_not_contains \
    web/src/types.ts \
    'reactor(Sender|Member)' \
    "web reaction realtime/aggregate types must not expose legacy reactor sender/member fields"

  require_contains \
    web/src/agentteam/workspace/shared/queryCache.ts \
    'authorProfile:\s*reaction\.authorProfile' \
    "web reaction cache must derive reactors from canonical reaction authorProfile"
}

check_trigger_metadata_deleted() {
  require_contains \
    internal/tidb/migrations/00162_remove_trigger_metadata_from_scope_messages.sql \
    'JSON_REMOVE.*\$\.trigger' \
    "trigger metadata cleanup migration must remove retained scope_messages metadata.trigger"

  # TiDB migration regression coverage moved to the dedicated coverage
  # repository; this repo no longer ships the internal/tidb test suite.
}

check_receive_message_source_propagation() {
  require_contains \
    internal/store/privateagent/conversation_write.go \
    'Source:\s*options\.Source' \
    "canonical conversation receive payloads must propagate contribution source"

  require_contains \
    internal/store/app/app_messages.go \
    'Source:\s*delivery\.Message\.Source' \
    "app message DTO construction must preserve delivery contribution source"
}

check_store_runtime_sender_cutover() {
  require_not_contains \
    internal/store/message/api_types.go \
    'type ReceiveMessage struct \{[^}]*Sender(Type|Name)' \
    "store/runtime ReceiveMessage must not carry old daemon sender fields; derive them in the DTO adapter"

  require_not_contains \
    internal/store/message/api_types.go \
    'type HistoryMessage struct \{[^}]*Sender(Type|Name)' \
    "store/runtime HistoryMessage must not carry old daemon sender fields; derive them in the DTO adapter"

  require_contains \
    internal/api/dto/converter/message.go \
    'legacyDaemonSenderFromAuthorProfile' \
    "old daemon sender wire fields must be derived from canonical AuthorProfile at the API DTO boundary"

  require_contains \
    internal/store/app/app_service_messages.go \
    'attachAppMessageToDelivery\(&delivery, &appMessage\)' \
    "service App deliveries must attach the canonical service AuthorProfile"

  require_contains \
    internal/store/app/app_messages.go \
    'delivery\.Message\.AuthorProfile = appMessage\.EffectiveAuthorProfile\(\)' \
    "service app message deliveries must update ReceiveMessage from the canonical service AuthorProfile"

  require_not_contains \
    desktop/internal/dispatcher/dispatcher.go \
    'msg\.SenderName|unknown sender' \
    "desktop runtime status details must render urgent authors through AuthorIdentity, not legacy senderName"

  local matches unexpected
  matches="$(rg -n -P \
    --glob '*.go' \
    -- 'Sender(Type|Name):|\.Sender(Type|Name)\b' \
    internal/store internal/api internal/service cmd e2e 2>/dev/null || true)"
  unexpected="$(printf '%s\n' "$matches" | sed '/^$/d' \
    | grep -v '^internal/archive/' \
    | grep -v '^internal/store/message/history\.go:' \
    | grep -v '^internal/store/message/reply_threads_store\.go:' \
    | grep -v '^internal/store/app/reply_threads_archive\.go:' \
    | grep -v '^internal/store/message/history_cold_integration_test\.go:' \
    | grep -v '^internal/store/app/reply_threads_archive_test\.go:' \
    | grep -v '^internal/api/channel/api_channels_threads_integration_test\.go:' \
    | grep -v '^internal/api/dto/' \
    | grep -v '^internal/api/agent/api_agent_compat_integration_test\.go:' \
    || true)"
  if [[ -n "$unexpected" ]]; then
    fail "canonical store/runtime/API implementation must not set or read old daemon sender fields outside named compat/archive/DTO boundaries"
    printf '%s\n' "$unexpected" >&2
  fi
}

check_provider_watch_message_metadata_deleted() {
  require_file_absent \
    internal/store/app/provider_watch_webhook_render.go \
    "legacy provider-watch webhook render path must remain deleted; service-event delivery render is canonical"

  require_not_contains \
    internal/store/app/service_event_delivery_render.go \
    'providerWatchWebhookMessageMetadata|provider_watch.*map\[string\]any|"provider_watch"\s*:' \
    "service-event visible messages must not write legacy provider_watch metadata"

  require_contains \
    internal/tidb/migrations/00163_remove_provider_watch_message_metadata.sql \
    'JSON_REMOVE.*\$\.provider_watch' \
    "provider watch metadata cleanup migration must remove retained scope_messages metadata.provider_watch"

  # TiDB migration regression coverage moved to the dedicated coverage
  # repository; this repo no longer ships the internal/tidb test suite.
}

check_browser_legacy_sender_wire_deleted() {
  require_not_contains \
    internal/api/dto/agent_messages.go \
    'type AppMessage struct \{[^}]*Sender(Type|ID|Name|DisplayName|AvatarURL|ServiceKind|ServiceSlug)' \
    "browser-facing AppMessage DTO must not expose legacy sender fields"

  require_not_contains \
    web/src/types.ts \
    'export type (Message|MessageMentionEvent|ReactionNotificationEvent) = \{[^}]*sender(Type|Id|Name|DisplayName|AvatarUrl|ServiceKind|ServiceSlug)' \
    "web browser message/notification types must not expose legacy sender fields"

  require_not_contains \
    web/src/agentteam/api/messages.ts \
    'sender(Type|Id|Name|DisplayName|AvatarUrl|ServiceKind|ServiceSlug)\s*:' \
    "web API message schema must not accept legacy sender fields"

  require_not_contains \
    web/src/agentteam/pages/workspace-shell/socketPayloadGuards.ts \
    'sender(Type|Id|Name|DisplayName|AvatarUrl|ServiceKind|ServiceSlug)' \
    "web socket guards must not require or validate legacy sender fields"

  require_contains \
    web/src/agentteam/pages/workspace-shell/socketPayloadGuards.test.ts \
    'rejects payloads without canonical author profile' \
    "web socket guard must require canonical authorProfile messages after sender wire deletion"

  require_contains \
    web/src/agentteam/workspace/conversation/components/ThreadMessages.tsx \
    'authorProfile:\s*\{\s*actorType:\s*"human"' \
    "local pending message construction must use canonical authorProfile instead of legacy sender fields"

  require_contains \
    e2e/daemon_e2e_test.go \
    'AuthorProfile apiAuthorProfile `json:"authorProfile"`' \
    "daemon e2e browser message response must assert canonical authorProfile"

  require_not_contains \
    e2e/daemon_e2e_test.go \
    'Sender(Type|ID|Id|Name)|sender(Type|Id|Name|DisplayName)' \
    "daemon e2e browser/API response assertions must not prove legacy sender wire"
}

check_disposal_inventory_quality() {
  require_contains \
    guardrails/message/author-source-substrate-inventory.md \
    'senderName' \
    "author/source disposal inventory must track legacy senderName"

  require_contains \
    guardrails/message/author-source-substrate-inventory.md \
    'metadata\.trigger' \
    "author/source disposal inventory must track trigger metadata source surfaces"

  require_contains \
    guardrails/message/author-source-substrate-inventory.md \
    'provider_watch' \
    "author/source disposal inventory must track provider watch source surfaces"

  require_contains \
    guardrails/message/author-source-substrate-inventory.md \
    'internal/store/message/contribution_source_schedule_replay\.go' \
    "author/source disposal inventory must name trigger/schedule run replay persistence surfaces"

  require_contains \
    guardrails/message/author-source-substrate-inventory.md \
    'web/src/types\.ts' \
    "author/source disposal inventory must name message sender wire fields"

  require_contains \
    guardrails/message/author-source-substrate-inventory.md \
    'ownerPrivateDelivery\.ts' \
    "author/source disposal inventory must name owner-private metadata source fallback"

  require_contains \
    guardrails/message/author-source-substrate-inventory.md \
    'compat-only.*named consumer' \
    "compat-only rows must require named consumers"

  require_contains \
    guardrails/message/author-source-substrate-inventory.md \
    'removal condition' \
    "compat-only rows must require removal conditions"
}

require_contains \
  specs/collaboration/source-projection-delivery.md \
  'Actor, author, principal, source, and recipient' \
  "collaboration source/projection spec must define the author/source/principal/recipient split"

require_contains \
  specs/collaboration/source-projection-delivery.md \
  'human \| agent \| service' \
  "collaboration source/projection spec must keep the core author family small"

require_contains \
  specs/collaboration/source-projection-delivery.md \
  'empty enum slots must not be pre-registered' \
  "collaboration source/projection spec must forbid future service-kind reservations"

require_contains \
  specs/collaboration/source-projection-delivery.md \
  'A facade-only change fails this contract' \
  "collaboration source/projection spec must reject facade-only migrations"

require_contains \
  specs/collaboration/source-projection-delivery.md \
  'Legacy trigger and schedule records are run sources; they do not' \
  "collaboration source/projection spec must classify trigger/schedule as source/run, not author"

require_contains \
  guardrails/message/README.md \
  'author-source-substrate\.sh' \
  "message guardrail README must list the author/source substrate guardrail"

check_disposal_inventory_quality
check_no_product_core_sender_types
check_no_future_service_kind_reservations
check_automation_service_author_contract
check_no_web_message_sender_truth_paths
check_no_web_message_sender_display_truth_paths
check_no_web_sender_legacy_naming
check_private_agent_contribution_capability_naming
check_desktop_author_identity_paths
check_reaction_receive_message_author_profile
check_reaction_reactor_author_profile
check_trigger_metadata_deleted
check_provider_watch_message_metadata_deleted
check_receive_message_source_propagation
check_store_runtime_sender_cutover
check_browser_legacy_sender_wire_deleted

if (( failures > 0 )); then
  exit 1
fi

printf 'message author/source substrate guardrail OK\n'
