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

ROOT="${AGENTTEAM_MESSAGE_TARGET_ACCESS_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_file() {
  local file="$1"
  [[ -f "$file" ]] || fail "missing $file"
}

require_contains() {
  local file="$1"
  local needle="$2"
  local label="${3:-$file must contain $needle}"
  if [[ ! -f "$file" ]]; then
    fail "missing $file"
    return
  fi
  if ! grep -Fq -- "$needle" "$file"; then
    fail "$label"
  fi
}

require_no_matches() {
  local label="$1"
  local pattern="$2"
  shift 2
  local matches
  matches="$(rg -n -P -- "$pattern" "$@" 2>/dev/null || true)"
  if [[ -n "$matches" ]]; then
    fail "$label"
    printf '%s\n' "$matches" >&2
  fi
}

legacy_scope_identifier_pattern='(^|[^A-Za-z0-9_])(spaceID|spaceId|SpaceID|DeliverySpaceID|PermissionCheckSpaceID|PermissionSpaceID)([^A-Za-z0-9_]|$)'
private_agent_scope_projection_legacy_pattern='(^|[^A-Za-z0-9_])(spaceID|spaceId|SpaceID|spaceKind|SpaceKind|SourceSpaceID|SourceSpaceKind|sourceSpaceKind|SpaceProjection|SpaceParticipant|HumanSpaceSubscription|NormalizeHumanSpaceSubscription)([^A-Za-z0-9_]|$)|(^|[^A-Za-z0-9_])(space projection|space participant|source_space|parent_space)([^A-Za-z0-9_]|$)'
semantic_scope_legacy_pattern='(?<!Work)Space(Ref|ID|Kind|Projection|Participant|Subscription|Target|Source|Context|Message|Record)|(?<![Ww]ork)space(Id|ID|Kind|Ref)|space_(write|projection|participant|target|source|context|message)|space[ -](target|projection|participant|fallback|compatibility)|writeMessageToSpace|writeSpaceMessagesBatch|Resolve[A-Za-z0-9_]*ForSpace|targetChannelId|TargetChannelID|parent_space_id|(?<!work)space_id'

print_live_legacy_scope_identifier_inventory() {
  printf '# Live legacy scope identifier matches\n'
  printf 'This live inventory lists target/access-adjacent code that still uses generic space naming for scope semantics. New shared contracts should use explicit scope roles instead.\n\n'
  if ! rg -n -P -- "$legacy_scope_identifier_pattern" internal/store/access internal/store/app internal/store/attachment internal/store/message internal/store/privateagent internal/store/reaction internal/store/taskboard 2>/dev/null; then
    printf '(none)\n'
  fi
}

print_live_semantic_scope_legacy_inventory() {
  printf '\n# Live semantic space→scope legacy matches\n'
  printf 'This live inventory is broader than *Id aliases: it also lists current message-domain type/helper/file/wire names and SQL aliases that still use space terminology for scope semantics. Workspace and true channel route names are intentionally outside this scan.\n\n'
  if ! rg -n -P -- "$semantic_scope_legacy_pattern" \
    internal/api/agent \
    internal/api/dto \
    internal/api/im \
    internal/api/task \
    internal/api/testkit/store_message.go \
    internal/store/app \
    internal/store/message \
    internal/store/model \
    internal/store/privateagent \
    internal/store/taskboard \
    internal/trigger \
    internal/service/message \
    desktop/internal/wire \
    web/src/agentteam \
    web/src/types.ts 2>/dev/null \
    | grep -v -E 'Workspace|workspace|targetChannelId|TargetChannelID'; then
    printf '(none)\n'
  fi
}

print_inventory() {
  if [[ -f guardrails/message/scope-target-access-debt-inventory.md ]]; then
    cat guardrails/message/scope-target-access-debt-inventory.md
    printf '\n'
  else
    printf '# Scope target/access legacy debt inventory\n\n(missing guardrails/message/scope-target-access-debt-inventory.md)\n\n'
  fi
  print_live_legacy_scope_identifier_inventory
  print_live_semantic_scope_legacy_inventory
}

run_checks() {
  require_file specs/message/scope-target-access-contract.md
  require_file guardrails/message/scope-target-access-debt-inventory.md
  require_file internal/store/access/scope_target_contract.go
  require_file internal/store/app/agent_target_resolution.go
  require_file internal/store/app/agent_target_basis.go
  require_file internal/store/app/messaging_adapter.go
  require_file internal/api/dto/agent_messages.go
  require_file internal/store/privateagent/scope_projection.go
  require_file internal/store/privateagent/conversation_write.go
  require_file internal/store/message/sidecar_authority.go
  require_file internal/store/app/scope_projection.go
  require_file internal/store/app/provider_watch_authorization.go
  require_file internal/store/app/provider_watch_target_binding.go
  require_file internal/store/app/provider_watch_target_contract.go

  require_contains specs/message/README.md './scope-target-access-contract.md' 'message spec README must link the scope target/access contract spec'
  require_contains guardrails/message/README.md './scope-target-access-debt-inventory.md' 'message guardrail README must link the target/access debt inventory'
  require_contains specs/message/scope-target-access-contract.md 'target/access debt inventory' 'scope target/access spec must name the debt inventory ratchet'
  require_contains specs/message/scope-target-access-contract.md 'Compatibility recovery paths must be named, bounded by the requested target kind' 'scope target/access spec must constrain resolver recovery paths'
  require_contains specs/message/scope-target-access-contract.md 'delete-first rule' 'scope target/access spec must require the delete-first rule for legacy names'
  require_contains specs/message/scope-target-access-contract.md 'Space→scope is a semantic legacy boundary, not just an id-field rename' 'scope target/access spec must classify non-ID space legacy'
  require_contains specs/message/scope-target-access-contract.md 'Allowed exceptions must list a reason, boundary, and exit condition' 'scope target/access spec must require exception evidence'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Space→scope semantic legacy boundaries' 'target/access debt inventory must classify semantic legacy naming boundaries'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'not just `*Id`' 'target/access debt inventory must state semantic legacy is broader than ID aliases'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Allowed semantic legacy exceptions and follow-ups' 'target/access debt inventory must classify semantic exception boundaries'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'reason, boundary, and exit condition' 'target/access debt inventory must require exception evidence'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Resolver compatibility recovery inventory' 'target/access debt inventory must classify resolver recovery paths'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Agent target basis resolver convergence' 'target/access debt inventory must record owner-private target recovery removal'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'No resolve-channel daemon alias' 'target/access debt inventory must record removal of resolve-channel spaceId alias'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'resolve-channel scope-only' 'target/access debt inventory must record resolve-channel scope-only status'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Agent target resolver scope-named wrappers' 'target/access debt inventory must record agent resolver wrapper scope naming'
  require_contains specs/message/scope-target-access-contract.md 'Agent daemon target resolver wrappers are scope-named' 'scope target/access spec must forbid legacy agent resolver wrapper names'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'No daemon receive/history/search `space_id` query alias' 'target/access debt inventory must record removal of daemon receive/history/search space_id query alias'
  require_contains specs/message/scope-target-access-contract.md 'Daemon receive/history/search query parameters are current scope surfaces' 'scope target/access spec must forbid daemon receive/history/search space_id query aliases'
  require_no_matches 'message visibility must not retain receive request-kind mode; root previews use ordinary read-check/readable predicate' 'ScopeVisibilityRequestReceive|RequestKindReceive|RequestKind[A-Za-z0-9_]*Receive' internal/store/message internal/api/im
  require_contains specs/message/scope-target-access-contract.md 'Focused negative tests may construct retired request keys dynamically' 'scope target/access spec must bound retired request-key negative tests'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'delete-first' 'target/access debt inventory must make legacy aliases delete-first debt'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Clients and schema have moved to scope terminology' 'target/access debt inventory must state the current no-legacy default'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'delivery_scope_id` and `permission_check_scope_id' 'target/access debt inventory must classify provider-watch scope column convergence'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Private-agent/app projection and owner quote burn-down' 'target/access debt inventory must record the private-agent/app projection and task-context burn-down'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'conversation_write.go' 'target/access debt inventory must record private-agent conversation write naming and boundary'
  require_contains specs/message/scope-target-access-contract.md 'conversation_write.go' 'scope target/access spec must record private-agent conversation writer naming boundary'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Agent send-param target reference boundary' 'target/access debt inventory must record the agent send-param TargetRef boundary'
  require_contains specs/message/scope-target-access-contract.md '`message.SendMessageParams` uses `TargetRef`' 'scope target/access spec must require SendMessageParams TargetRef naming'

  if [[ -e internal/store/privateagent/space_projection.go || -e internal/store/app/space_projection.go ]]; then
    fail 'private-agent/app projection helpers must be scope_projection.go, not space_projection.go'
  fi

  if [[ -e internal/store/privateagent/space_write.go ]]; then
    fail 'private-agent ordinary conversation writer must be conversation_write.go, not space_write.go'
  fi

  require_no_matches \
    'private-agent conversation writer must not reintroduce legacy space write helper names' \
    '(^|[^A-Za-z0-9_])(writeMessageToSpace|space write)([^A-Za-z0-9_]|$)' \
    internal/store/privateagent

  require_no_matches \
    'private-agent/app projection and owner quote paths must not reintroduce generic space naming' \
    "$private_agent_scope_projection_legacy_pattern" \
    internal/store/privateagent \
    internal/store/app/scope_projection.go \
    internal/store/app/provider_watch_service.go \
    internal/store/app/provider_watch_service_integration_test.go \
    internal/store/app/domain_adapters.go \
    internal/store/app/app_channels.go \
    internal/store/app/app_runtime.go \
    internal/store/app/app_workspaces.go \
    internal/store/app/owner_private_delivery.go \
    internal/api/dto/web_agents.go \
    internal/api/webagent/api_context.go \
    internal/api/webagent/context_params.go \
    internal/store/taskboard/private_agent_context_state.go \
    internal/store/taskboard/types.go \
    internal/store/message/owner_quote.go \
    internal/store/message/owner_private_store.go \
    internal/store/message/thread_subscriptions.go \
    internal/store/message/thread_subscriptions_store.go \
    internal/service/organization/core_bridge.go \
    internal/api/dto/task_parity.go \
    internal/api/dto/converter/task.go \
    desktop/internal/wire/mcp_tools_tasks.go \
    web/src/types.ts \
    web/src/agentteam/workspace/ContextShareFlowDialog.tsx \
    web/src/agentteam/workspace/conversation/components/ThreadMessages.tsx \
    web/src/agentteam/workspace/ownerPrivateDelivery.ts \
    web/src/agentteam/workspace/task-detail/TaskExecutionSurface.tsx
  require_no_matches \
    'desktop task detail fixture must use sourceScopeKind, not sourceSpaceKind' \
    'sourceSpaceKind|SourceSpaceKind' \
    desktop/internal/wire/agentteam_test.go

  require_contains specs/message/scope-target-access-contract.md 'Private-agent target/send integration tests use scope naming' 'scope target/access spec must classify private-agent target/send test helper scope naming'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Private-agent target/send integration test helper scope naming' 'target/access debt inventory must classify private-agent target/send test helper scope naming'
  require_no_matches \
    'private-agent target/send integration test helpers must use scope naming, not spaceID/wantSpaceID/parentSpaceID' \
    'wantSpaceID|parentSpaceID|(?<!work)spaceID|(?<!Work)SpaceID|spaceId' \
    internal/api/agent/private_agent_target_send_integration_test.go

  require_no_matches \
    'retired owner-private context delivery API/carrier must not be reintroduced' \
    'WebAgentDeliverContextRequest|DeliverContextParams|DeliverContextToAgentForUser|LoadDeliverContextSourceMessages|BuildOwnerQuoteDeliveryContent|ownerQuoteKindDeliveryCarrier|ownerDeliveredContextPrefix|Owner-delivered context|/deliver-context|delivery_carrier|isOwnerDeliveredCarrierOwnerQuoteContent' \
    internal \
    desktop/internal/wire \
    web/src \
    docs \
    specs
  require_no_matches \
    'owner_quote current writers/UI/specs must not reintroduce shared-context naming or legacy shared-context headers' \
    'shared_context|Shared context|Owner-shared context|Quoted shared context|Quoted context:|SharedContext|sharedContext|shared-context|BuildSharedContext|LoadSharedContext|ValidateSharedContext|ownerQuoteKindSharedContext|legacySharedContextHeader|sharedContextHeader' \
    internal/core/routing/owner_quote.go \
    internal/core/routing/owner_quote_test.go \
    internal/store/app/app_messages.go \
    internal/store/app/app_messages_integration_test.go \
    internal/store/app/mention_routing.go \
    internal/store/app/owner_private_delivery.go \
    internal/store/message/message_types.go \
    internal/store/message/message_types_test.go \
    internal/store/message/owner_private_store.go \
    internal/store/message/owner_quote.go \
    internal/store/message/owner_quote_test.go \
    internal/store/message/scope_rewind_read_projection_test.go \
    internal/api/im \
    web/src/agentteam/workspace/conversation \
    web/src/agentteam/pages/workspace-shell/useWorkspaceSocket.ts \
    web/src/agentteam/i18n/locales/en/workspace.json \
    web/src/agentteam/i18n/locales/zh-CN/workspace.json \
    web/e2e/replyThread.spec.ts \
    web/e2e/channel.spec.ts \
    docs/tech_design.md \
    specs/collaboration/collaboration.md \
    specs/task \
    guardrails/message/scope-target-access-debt-inventory.md

  local send_message_params_body
  send_message_params_body="$(awk '/type SendMessageParams struct/,/^}/' internal/store/message/api_types.go)"
  if ! grep -Eq 'TargetRef[[:space:]]+string' <<<"$send_message_params_body"; then
    fail 'SendMessageParams must expose TargetRef for channel/reply-thread target references'
  fi
  if grep -Eq 'SpaceRef|Channel[[:space:]]+string' <<<"$send_message_params_body"; then
    fail 'SendMessageParams must not retain SpaceRef or a duplicate Channel fallback'
    grep -En 'SpaceRef|Channel[[:space:]]+string' <<<"$send_message_params_body" >&2 || true
  fi

  local agent_send_param_legacy_matches
  agent_send_param_legacy_matches="$(
    rg -n -P -- 'SpaceRef|params\\.Channel|Channel:\\s*reference\\.String\\(\\)|Channel:\\s*\"#\"' \
      internal/store/message/api_types.go \
      internal/store/app/agent_messages.go \
      internal/api/agent/message_flow.go \
      internal/api/im/fanout/schedule_effect_writer.go \
      internal/api/im/fanout/schedule_effect_writer_test.go \
      specs/message/server-side-freshness-gate-design.md \
      2>/dev/null || true
  )"
  if [[ -n "$agent_send_param_legacy_matches" ]]; then
    fail 'agent send-param target reference paths must use TargetRef/DMTo/TaskScopeID, not SpaceRef or Channel fallback'
    printf '%s\n' "$agent_send_param_legacy_matches" >&2
  fi
  if ! grep -Fq 'SendMessageParams{TargetRef: reference.String(), Content:' internal/api/agent/message_flow.go && ! grep -Fq 'baseParams.TargetRef = reference.String()' internal/api/agent/message_flow.go; then
    fail 'agent message flow must pass TargetRef without duplicate Channel fallback'
  fi
  require_contains internal/api/im/fanout/schedule_effect_writer.go 'request.TargetRef' 'schedule effect writer must pass TargetRef'

  local legacy_contract_matches
  legacy_contract_matches="$(rg -n -P -- "$legacy_scope_identifier_pattern" internal/store/access/scope_target_contract.go internal/store/app/agent_target_resolution.go internal/api/agent/message_ports.go 2>/dev/null || true)"
  if [[ -n "$legacy_contract_matches" ]]; then
    fail 'typed target/access contracts and resolver ports must use explicit scope roles, not generic space identifiers'
    printf '%s\n' "$legacy_contract_matches" >&2
  fi

  require_contains internal/store/app/agent_target_resolution.go 'ResolveWritableAgentMessageTarget' 'agent target write resolver must stay on the typed target/access contract'
  require_contains internal/store/app/agent_target_resolution.go 'ResolveReadableAgentMessageTarget' 'agent target read resolver must stay on the typed target/access contract'
  require_contains internal/store/app/agent_target_resolution.go 'validateAgentScopeTarget' 'agent target resolvers must validate the typed target/access contract'
  require_contains internal/store/app/agent_target_resolution.go 'resolveAgentThreadTarget(ctx, tx, authenticatedAgent, request' 'agent target write resolver must use the unified target-basis resolver'
  require_contains internal/store/app/agent_target_resolution.go 'resolveAgentThreadTarget(ctx, store.db, authenticatedAgent, request' 'agent target read/review resolvers must use the unified target-basis resolver'

  local legacy_message_target_parser_matches
  legacy_message_target_parser_matches="$(
    rg -n -P \
      --glob '*.go' \
      --glob '!**/*_test.go' \
      --glob '!internal/core/domain/channel.go' \
      -- '\b(ParseThreadReference|ParseRawScopeTarget|ParseTeamTargetReference)\b' \
      . 2>/dev/null || true
  )"
  if [[ -n "$legacy_message_target_parser_matches" ]]; then
    fail 'production message target entrypoints must use ParseMessageTargetReference instead of legacy split parsers'
    printf '%s\n' "$legacy_message_target_parser_matches" >&2
  fi
  require_contains desktop/internal/api/mcp/message_history.go 'domain.ParseMessageTargetReference' 'desktop MCP target normalization must use the shared message target parser'
  require_contains desktop/internal/dispatcher/dispatcher.go 'domain.ParseMessageTargetReference' 'desktop delivery target projection must use the shared message target parser'
  require_contains desktop/internal/dispatcher/memory_target.go 'domain.ParseMessageTargetReference' 'desktop memory target projection must use the shared message target parser'
  require_contains desktop/internal/promptcompiler/compile.go 'domain.ParseMessageTargetReference' 'desktop prompt binding must use the shared message target parser'
  require_contains desktop/internal/threadcontext/guard.go 'domain.ParseMessageTargetReference' 'desktop reply-thread guidance must use the shared message target parser'
  require_contains desktop/internal/types/session.go 'domain.ParseMessageTargetReference' 'desktop Task target projection must use the shared message target parser'
  require_contains desktop/internal/wire/util.go 'domain.ParseMessageTargetReference' 'desktop wire target projection must use the shared message target parser'

  require_contains internal/store/app/agent_target_basis.go 'func (store *Store) resolveAgentThreadTarget' 'unified agent target-basis resolver must live in agent_target_basis.go'
  require_contains internal/store/app/agent_target_basis.go 'loadAgentTargetChannelRosterBasis' 'unified agent target-basis resolver must use channel roster state'
  require_contains internal/store/app/agent_target_basis.go "sm.visibility IN ('public', 'private')" 'agent target basis must accept public and private channel roster rows'
  require_contains internal/store/app/messaging_adapter.go 'resolveAgentThreadTarget(ctx, tx, authorAgent, request' 'legacy send adapter must delegate channel/thread sends to the unified target-basis resolver'
  require_contains internal/store/app/agent_target_resolution_test.go 'TestAgentTargetResolutionDoesNotUseLegacyOwnerPrivateTargetShape' 'agent target resolver must have focused coverage preventing old owner-private target recovery'
  require_no_matches \
    'agent target resolver must not reintroduce owner-private recovery helpers or root-visibility target gates' \
    'ownerPrivateAgentTargetRecoveryAllowed|resolveOwnerPrivateAgent(SendTarget|HistoryThread)|RequireOwnerPrivate(ContextRecord|ParentRoot)Visible' \
    internal/store/app/agent_target_resolution.go \
    internal/store/app/agent_target_basis.go \
    internal/store/app/messaging_adapter.go \
    internal/store/review/review_context.go
  require_contains internal/api/agent/api_messages.go 'response is scope-only' 'resolve-channel handler must document scope-only daemon response'
  require_contains internal/api/agent/api_messages_test.go '"scopeId":"scp_general","targetKey":"scp_general"' 'resolve-channel test must assert the scope-only response contract'

  local agent_scope_resolver_wrapper_matches
  agent_scope_resolver_wrapper_matches="$(rg -n -P -- 'Resolve(Readable)?ChannelIDForAgentTarget|ResolveReviewSubjectIDForAgentTarget|legacy daemon protocol fields|pre-scope naming|exposed as spaceId' internal/store/app/agent_messages.go internal/api/agent/trigger_owner.go internal/api/agent/api_triggers_test.go internal/api/testkit/store_message.go internal/store/testkit/store.go internal/store/app/app_messages_integration_test.go 2>/dev/null || true)"
  if [[ -n "$agent_scope_resolver_wrapper_matches" ]]; then
    fail 'agent scope resolver boundary must use scope-named wrappers, not legacy channel/space compatibility names'
    printf '%s\n' "$agent_scope_resolver_wrapper_matches" >&2
  fi

  local resolve_space_alias_matches
  resolve_space_alias_matches="$(rg -n -P -- '(^|[^A-Za-z0-9_])(spaceId|SpaceID)([^A-Za-z0-9_]|$)' internal/api/dto/agent_messages.go internal/api/agent/api_messages.go desktop/internal/wire/mcp_tools_chat.go desktop/internal/wire/mcp_tools_review.go desktop/internal/wire/outbound.go 2>/dev/null || true)"
  if [[ -n "$resolve_space_alias_matches" ]]; then
    fail 'agent resolve-channel and desktop target/file/review clients must not carry legacy spaceId aliases or fallbacks'
    printf '%s\n' "$resolve_space_alias_matches" >&2
  fi

  local daemon_query_space_alias_matches
  daemon_query_space_alias_matches="$(rg -n -- 'Query\("space_id"\)|space_id.*legacy daemon alias|LegacySpaceIDAlias|AcceptsLegacySpaceIDAlias|PreferScopeIDOverLegacySpaceIDAlias' internal/api/agent/api_messages.go internal/api/agent/api_messages_test.go desktop/internal/wire/mcp_tools_chat.go 2>/dev/null || true)"
  if [[ -n "$daemon_query_space_alias_matches" ]]; then
    fail 'daemon receive/history/search must not accept or test legacy space_id query aliases; use scope_id only'
    printf '%s\n' "$daemon_query_space_alias_matches" >&2
  fi
  require_contains internal/api/agent/api_messages_test.go 'TestMessageHandlerHistorySearchIgnoreDeprecatedScopedCursorAlias' 'daemon history/search must have coverage ignoring the retired scoped cursor alias when scope_id is present'
  require_contains internal/api/agent/api_messages_test.go 'deprecatedScopedCursorQueryAliasForNegativeTest' 'daemon retired scoped cursor alias coverage must construct the key behind an explicit negative-test helper'

  require_contains specs/message/scope-target-access-contract.md 'Attachment upload is not a legacy compatibility boundary' 'scope target/access spec must forbid attachment upload spaceId compatibility'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Explicit invalid legacy request-key tests' 'target/access debt inventory must classify explicit negative-test legacy key boundaries'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Attachment upload scopeId-only boundary' 'target/access debt inventory must classify attachment upload as scopeId-only'
  require_contains internal/api/media/api_attachments.go 'ctx.PostForm("scopeId")' 'browser attachment upload must read scopeId'
  require_contains internal/api/media/api_attachments_test.go 'TestAttachmentHandlerUploadRejectsLegacyTargetField' 'browser attachment upload must reject legacy target field coverage'
  require_contains internal/api/agent/api_files.go 'c.PostForm("scopeId")' 'agent attachment upload must read scopeId'
  require_contains internal/api/agent/api_files_test.go 'TestFileHandlerUploadRejectsLegacyTargetField' 'agent attachment upload must reject legacy target field coverage'
  require_contains web/src/agentteam/api/attachments.ts 'formData.append("scopeId", normalizedScopeId)' 'browser attachment upload client must send scopeId'
  require_contains desktop/internal/wire/agentteam_test.go 'deprecatedUploadScopeAliasFormKeyForNegativeTest' 'desktop upload tests must keep retired upload alias coverage behind an explicit negative-test helper'

  local retired_negative_test_literal_matches
  retired_negative_test_literal_matches="$(
    {
      rg -n -P -- '(^|[^A-Za-z0-9_])space_id([^A-Za-z0-9_]|$)|LegacySpaceIDAlias' internal/api/agent/api_messages_test.go 2>/dev/null || true
      rg -n -P -- '(^|[^A-Za-z0-9_])spaceId([^A-Za-z0-9_]|$)' desktop/internal/wire/agentteam_test.go 2>/dev/null || true
    } | sort -u
  )"
  if [[ -n "$retired_negative_test_literal_matches" ]]; then
    fail 'explicit negative tests must construct retired request keys behind helper functions instead of carrying live legacy literals or names'
    printf '%s\n' "$retired_negative_test_literal_matches" >&2
  fi

  local legacy_attachment_upload_matches
  legacy_attachment_upload_matches="$(
    {
      rg -n -P -- '(^|[^A-Za-z0-9_])(spaceID|spaceId|SpaceID)([^A-Za-z0-9_]|$)' internal/store/attachment internal/api/media internal/api/agent/api_files.go internal/api/agent/file_ports.go 2>/dev/null || true
      rg -n -- 'PostForm\("spaceId"\)|formData\.append\("spaceId"|ResolvePostTargetForUser|ResolveUploadSpaceFor|uploadAttachmentsToSpace|AttachmentSpace|attachment space|upload space|(^|[^A-Za-z0-9_])spaceId is required' internal/store/attachment internal/store/app/domain_adapters.go internal/api/media internal/api/agent/api_files.go internal/api/agent/file_ports.go web/src/agentteam/api.ts web/src/agentteam/api/attachments.ts 2>/dev/null || true
    } | sort -u
  )"
  if [[ -n "$legacy_attachment_upload_matches" ]]; then
    fail 'attachment upload/access code must use scopeId/scopeID only; legacy spaceId upload fallback is not allowed'
    printf '%s\n' "$legacy_attachment_upload_matches" >&2
  fi

  require_contains specs/message/scope-target-access-contract.md 'Browser/public message creation is not a legacy compatibility boundary' 'scope target/access spec must forbid message create spaceId/channelId request compatibility'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Browser/public message create scopeId-only boundary' 'target/access debt inventory must classify browser/public message create as scopeId-only'
  require_contains internal/api/im/routes.go 'workspaceScoped.POST("/scopes/:scopeId/messages"' 'workspace IM message create route must carry scopeId in the route'
  require_contains internal/api/im/routes_test.go 'POST /api/v1/workspaces/:workspaceId/scopes/:scopeId/messages' 'workspace IM route test must assert the scopeId message create route'
  require_contains internal/api/im/messages_handler.go 'routeScopeID(r.WorkspaceScopedRequest)' 'message create handler must read the route scopeId'
  require_contains internal/api/im/messages_handler.go '"scopeId is required"' 'message create handler must name scopeId in target validation errors'
  require_contains web/src/agentteam/api/messages.ts 'export function writeMessageToScope' 'browser message create client must use scope naming'
  require_contains web/src/agentteam/api/messages.ts 'export function writeScopeMessagesBatch' 'browser batch message create client must use scope naming'

  local legacy_message_create_matches
  legacy_message_create_matches="$(
    {
      rg -n -P -- 'request\.(SpaceID|ChannelID)|json:"spaceId"|json:"channelId,omitempty"|(^|[^A-Za-z0-9_])spaceId is required|writeMessageToSpace|writeSpaceMessagesBatch' internal/api/dto/app_messages.go internal/api/dto/apps.go internal/api/im/messages_handler.go web/src/agentteam/api.ts web/src/agentteam/api/messages.ts web/src/agentteam/workspace/conversation/components/ThreadMessages.tsx web/src/agentteam/workspace/tasks/TaskDetailRoute.tsx web/src/agentteam/workspace/conversation/state/shareFlowExecution.ts 2>/dev/null || true
      rg -n -P -- '(^|[^A-Za-z0-9_])spaceId([^A-Za-z0-9_]|$)' web/e2e/channel.spec.ts web/e2e/mobileComposerFocus.spec.ts web/e2e/replyThread.spec.ts e2e/daemon_e2e_test.go 2>/dev/null || true
    } | sort -u
  )"
  if [[ -n "$legacy_message_create_matches" ]]; then
    fail 'browser/public message create paths must use scopeId only; legacy spaceId/channelId request aliases are not allowed'
    printf '%s\n' "$legacy_message_create_matches" >&2
  fi

  require_contains specs/message/scope-target-access-contract.md 'Task parity message-send ports use scope/target naming' 'scope target/access spec must classify task parity scope naming'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Task parity and metadata-send scope naming' 'target/access debt inventory must classify task parity scope naming'
  require_contains internal/api/dto/task_parity.go 'TargetScopeID string' 'task parity request DTO must expose targetScopeId'
  require_contains internal/api/dto/task_parity.go 'json:"targetScopeId' 'task parity request DTO must encode targetScopeId'
  require_contains web/src/types/tasks.ts 'targetScopeId' 'web task parity type must expose targetScopeId'
  require_contains internal/api/task/store_adapter.go 'scopeID string' 'task parity store adapter must name metadata-send target as scopeID'
  require_contains internal/schedule/manage/service.go 'targetRef string' 'schedule message writer port must name generic message target as targetRef'

  local legacy_task_announcement_matches
  legacy_task_announcement_matches="$(rg -n -P -- 'targetChannelId|TargetChannelID' internal/api/dto/task_parity.go internal/api/task/api_parity.go internal/api/task/api_parity_test.go web/src/types.ts 2>/dev/null || true)"
  local legacy_metadata_send_matches
  legacy_metadata_send_matches="$(rg -n -P -- 'SendMessageForUserWithMetadata\([^[:cntrl:]]*(spaceID|channelID)' internal/api/task/ports.go internal/api/task/store_adapter.go internal/api/testkit/store_message.go internal/store/app/app_messages.go internal/service/message/service.go internal/schedule/manage/service.go 2>/dev/null | grep -v 'scopeID' | grep -v 'targetRef' | grep -v 'TargetRef' || true)"
  local legacy_task_parity_scope_matches
  legacy_task_parity_scope_matches="$({ printf '%s\n' "$legacy_task_announcement_matches"; printf '%s\n' "$legacy_metadata_send_matches"; } | sed '/^$/d' | sort -u)"
  if [[ -n "$legacy_task_parity_scope_matches" ]]; then
    fail 'task parity and metadata-send paths must use targetScopeId/scopeID/targetRef, not targetChannelId/spaceID/generic channelID'
    printf '%s\n' "$legacy_task_parity_scope_matches" >&2
  fi

  require_contains specs/message/scope-target-access-contract.md 'Task collaborator access helpers use scope naming' 'scope target/access spec must classify task collaborator helper scope naming'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Task collaborator access helper scope naming' 'target/access debt inventory must classify task collaborator helper scope naming'
  require_contains internal/store/taskboard/collaborator_access.go 'func ensureMemberCanReadScope' 'task collaborator membership helper must use scope naming'

  local legacy_task_collaborator_access_matches
  legacy_task_collaborator_access_matches="$(rg -n -P -- 'ensureMemberInSpace|(?<![Ww]ork)spaceID|(?<![Ww]ork)SpaceID|(?<![Ww]ork)spaceId' internal/store/taskboard/collaborator_access.go 2>/dev/null || true)"
  if [[ -n "$legacy_task_collaborator_access_matches" ]]; then
    fail 'task collaborator access helper must use scope naming, not ensureMemberInSpace or generic spaceID aliases'
    printf '%s\n' "$legacy_task_collaborator_access_matches" >&2
  fi

  require_contains specs/message/scope-target-access-contract.md 'Message history/search/read models use scope naming' 'scope target/access spec must classify read/search/history scope naming'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Message history/search/read model scope naming' 'target/access debt inventory must classify read/search/history scope naming'
  require_contains specs/message/scope-target-access-contract.md 'shared row-level SQL readable predicate' 'scope target/access spec must make SQL readable predicate the read-row authority'
  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Message read-row authority convergence' 'target/access debt inventory must record SQL-only message read authority convergence'
  require_contains specs/message/scope-model.md 'Production read-row authority is SQL readable predicate' 'scope model must state SQL-only read-row authority'
  require_contains specs/message/search.md 'SQL readable predicate / visible-row mapper' 'message search spec must use SQL readable predicate plus mapper terminology'
  require_contains internal/store/message/scope_private_authority_sql.go 'func scopeReadableMembershipJoinSQL' 'message read authority must own canonical readable membership joins, including parent-channel thread readability'
  require_contains internal/store/message/scope_private_authority_sql.go 'const scopeReadableMessageReasonSQL' 'message read authority SQL must select canonical visibility reasons'
  require_contains internal/store/message/scope_private_authority_sql.go 'const scopePrivateViewerMemberSQL' 'private readable SQL must fail closed unless the private viewer member is an agent'
  require_contains internal/store/message/scope_store.go 'scopeReadableMessageReasonSQL' 'history visible-row SQL must select canonical visibility reasons'
  require_contains internal/store/message/scope_api.go 'scopeReadableMessageReasonSQL' 'search/read-check visible-row SQL must select canonical visibility reasons'
  require_contains internal/store/message/shared_query_builders.go 'scopeReadableMessagePredicateSQL' 'diagnostics/resource read surfaces must reuse the shared readable predicate'
  require_no_matches \
    'message read-row authority must not reintroduce Go-side public/private owner-grant authorization in VisibilityFilter' \
    'visibleByMembership|resolveScopeVisibilityViewerType|ResolveAgentScopeViewerType|privateReadAuthorityBackfillCandidate' \
    internal/store/message/scope_visibility.go
  require_contains internal/store/model/model.go 'ScopeID            string' 'ThreadMessageRecord must expose ScopeID'
  require_contains internal/store/model/model.go 'ScopeKind          string' 'ThreadMessageRecord must expose ScopeKind'
  require_contains internal/store/model/model.go 'ParentScopeID      string' 'ThreadMessageRecord must expose ParentScopeID'
  require_contains internal/store/message/search.go 'scopeID string' 'message search must name the requested read surface as scopeID'
  require_contains internal/store/message/shared_query_builders.go 'func BuildThreadTimelineReadQuery(scopeID string' 'thread timeline query builder must name read target as scopeID'
  require_contains internal/store/message/sidecar_authority.go 'func (service ScopeAPIService) AuthorizeSidecarRead' 'message sidecar read authority must be owned by the message domain'
  require_contains internal/store/message/sidecar_authority.go 'func (service ScopeAPIService) AuthorizeSidecarReadBatch' 'message sidecar read authority must expose a bounded batch API for sidecar callers'
  require_contains internal/store/message/sidecar_authority.go 'scopeReadableMessagePredicateSQL' 'message sidecar read authority must reuse the shared readable-row predicate'
  require_contains internal/store/message/sidecar_authority.go 'scopePrivateAuthorityGrantJoinsSQL' 'message sidecar read authority must not copy private authority grant SQL outside the message domain'
  require_contains internal/store/message/service.go 'func LoadReplyThreadShape' 'reply-thread topology must be loaded by a shape-only helper'
  require_contains internal/store/message/service.go 'func RequireMemberReplyThreadRead' 'reply-thread read authority must be separated from shape loading'
  local legacy_reply_thread_access_matches
  legacy_reply_thread_access_matches="$(rg -n -P --glob '!**/*_test.go' 'ResolveReplyThreadChannelAccess|resolveReplyThreadChannelAccess|loadAgentChannelReactionThreadID' internal/store internal/api 2>/dev/null || true)"
  if [[ -n "$legacy_reply_thread_access_matches" ]]; then
    fail 'reply-thread target/read code must not reintroduce the legacy structure+public-only access helper'
    printf '%s\n' "$legacy_reply_thread_access_matches" >&2
  fi
  require_no_matches \
    'message sidecar batch authority must not add sidecar-specific parent/scope fallback branches' \
    'sidecarKindAllowsParentScopeRead|parentBranch|parent_scope|message_scope\\.parent_scope_id' \
    internal/store/message/sidecar_authority.go
  require_contains internal/store/attachment/app_attachments.go 'authorizeAttachmentMessageReadForUser' 'attachment downloads must authorize by message_id read-check for human users'
  require_contains internal/store/attachment/app_attachments.go 'authorizeAttachmentMessageReadForMember' 'attachment downloads must authorize by message_id read-check for agent/member users'
  require_contains internal/store/app/domain_adapters.go 'SidecarKindAttachment' 'app attachment adapter must route attachment sidecar reads through message authority'
  require_contains internal/store/reaction/reactions.go 'SidecarKindReaction' 'reaction sidecar reads/writes must route through message authority'
  require_contains internal/store/app/channel_share_cards.go 'SidecarKindShareCard' 'share-card sidecar reads must route through message authority'
  require_contains internal/store/taskboard/private_agent_context_state.go 'AuthorizeSidecarReadBatch' 'source-ref sidecar reads must use message-owned batch authority'
  require_contains internal/store/privateagent/conversation_write.go 'ApplyMessagePlanTxWithSidecarWriter' 'ordinary message append must drive attachment sidecar claims through the message plan applier'
  require_contains internal/store/taskboard/task_scope_message.go 'ApplyMessagePlanTxWithSidecarWriter' 'task discussion append must drive attachment sidecar claims through the message plan applier'
  require_no_matches \
    'message sidecar read paths must not fall back to scope-level attachment/task access helpers or taskboard-local readable SQL' \
    'ensure(User|Member)CanAccess(Attachment|Task)Scope|AuthorizeAttachmentReadFor(User|Member)|AuthorizeReadForUser|buildSourceScopeReadabilityRequestsCTE|source_membership\\.visibility' \
    internal/store/attachment/app_attachments.go \
    internal/store/attachment/dependencies.go \
    internal/store/taskboard/private_agent_context_state.go
  require_no_matches \
    'message append paths must not claim attachment sidecars outside the message plan applier' \
    'claimConversationMessageAttachments|claimAttachmentsForTaskDiscussionMessage\\(ctx, tx' \
    internal/store/privateagent/conversation_write.go \
    internal/store/taskboard/discussion_entry.go

  local legacy_message_read_scope_matches
  legacy_message_read_scope_matches="$(rg -n -P -- '(^|[^A-Za-z0-9_])(SpaceID|SpaceKind|ParentSpaceID|spaceID|parent_space_id|scopeSpaceID|space_id)([^A-Za-z0-9_]|$)' internal/store/model/model.go internal/store/message/history.go internal/store/message/history_test.go internal/store/message/owner_private_visibility.go internal/store/message/search.go internal/store/message/shared_query_builders.go internal/store/message/shared_query_builders_test.go internal/store/message/store_wrappers.go internal/store/message/visible_thread_scope_test.go internal/store/message/visible_threads.go internal/store/app/owner_private_history.go internal/store/app/task_source_thread_context.go 2>/dev/null | grep -v 'WorkspaceID' | grep -v 'workspaceID' | grep -v 'workspace_id' | grep -v 'requestedWorkspaces' || true)"
  if [[ -n "$legacy_message_read_scope_matches" ]]; then
    fail 'message history/search/read model paths must use scope naming, not SpaceID/parent_space_id/space_id aliases'
    printf '%s\n' "$legacy_message_read_scope_matches" >&2
  fi

  local provider_watch_legacy_matches
  provider_watch_legacy_matches="$(rg -n -i -P -- 'DeliverySpaceID|PermissionCheckSpaceID|deliverySpaceId|permissionCheckSpaceId|deliverySpaceID|permissionSpaceID|permissionCheckSpaceID|delivery_space_id|permission_check_space_id|ResolveProviderWatchTargetForSpace|delivery[ -]space|permission[ -]check[ -]space' internal/store/app/provider_watch*.go internal/api/agent/api_provider_apps.go internal/api/dto/app_responses.go internal/api/dto/converter/app.go internal/api/provider/api_provider_watch_blocking_integration_test.go web/src/types.ts specs/app/provider-watch-agent-mcp.md internal/tidb/bootstrap.sql internal/tidb/admin.go 2>/dev/null || true)"
  if [[ -n "$provider_watch_legacy_matches" ]]; then
    fail 'provider-watch runtime, current schema, DTO, frontend type, and current spec surfaces must use scope names, not legacy space aliases'
    printf '%s\n' "$provider_watch_legacy_matches" >&2
  fi
  require_contains specs/app/provider-watch-agent-mcp.md 'Provider-watch target, binding, and authorization boundaries stay separate' 'provider-watch spec must classify target/binding/authorization boundaries'
  require_contains internal/store/app/provider_watch_target_contract.go 'func ResolveProviderWatchTargetForScope' 'provider-watch target resolver must live in the target contract boundary'
  require_contains internal/store/app/service_event_target_contract.go 'DeliveryScopeID' 'provider-watch service-event target must expose delivery scope role'
  require_contains internal/store/app/service_event_target_contract.go 'PermissionCheckScopeID' 'provider-watch service-event target must expose permission-check scope role'
  require_contains internal/store/app/provider_watch_target_binding.go 'func (store *Store) upsertTargetAppBinding' 'provider-watch target app binding persistence must live in the binding boundary'
  require_contains internal/store/app/provider_watch_target_binding.go 'func loadCurrentTargetAppBindingForUpdate' 'provider-watch target app binding loaders must live in the binding boundary'
  require_contains internal/store/app/provider_watch_authorization.go 'func (store *Store) requireProviderWatchActorAuthorization' 'provider-watch actor authorization must live in the authorization boundary'
  require_contains internal/store/app/provider_watch_authorization.go 'func requireProviderWatchActorOwnsWatch' 'provider-watch watch ownership authorization must live in the authorization boundary'
  local provider_watch_boundary_leaks
  provider_watch_boundary_leaks="$(rg -n -P -- 'func (\\(store \\*Store\\) )?(ResolveProviderWatchTargetForAgentTarget|ResolveProviderWatchTargetForScope|resolveProviderWatch(TaskTarget|ReplyThread|DM|AgentChannelTarget)|upsertTargetAppBinding|requireProviderWatchActorAuthorization|requireProviderWatchActorOwnsWatch)|func (loadCurrentTargetAppBindingForUpdate|loadTargetAppBindingByID|scanTargetAppBindingRecord)' internal/store/app/provider_watch_service.go 2>/dev/null || true)"
  if [[ -n "$provider_watch_boundary_leaks" ]]; then
    fail 'provider-watch lifecycle facade must not inline target resolution, target binding persistence, or actor authorization boundaries'
    printf '%s\n' "$provider_watch_boundary_leaks" >&2
  fi
  require_contains internal/tidb/bootstrap.sql '`delivery_scope_id` varchar(255) NOT NULL' 'current target_app_bindings schema must use delivery_scope_id'
  require_contains internal/tidb/bootstrap.sql '`permission_check_scope_id` varchar(255) NOT NULL' 'current target_app_bindings schema must use permission_check_scope_id'
  require_contains internal/tidb/migrations/00145_provider_watch_scope_column_names.sql 'CHANGE COLUMN delivery_space_id delivery_scope_id' 'provider-watch migration must rename legacy delivery column'
  require_contains internal/tidb/migrations/00145_provider_watch_scope_column_names.sql 'CHANGE COLUMN permission_check_space_id permission_check_scope_id' 'provider-watch migration must rename legacy permission column'

  require_contains guardrails/message/scope-target-access-debt-inventory.md 'Task collaborator scope-edge schema' 'target/access debt inventory must classify task collaborator scope-edge schema naming'
  require_contains specs/storage/storage.md 'no default execution row for owner-only scopes' 'storage spec must not preserve participant-space edge wording for task collaborator execution edges'
  require_contains internal/tidb/migrations/00146_task_collaborator_scope_column_cleanup.sql 'DROP COLUMN IF EXISTS `participant_space_id`' 'task collaborator migration must drop legacy participant_space_id'

  local task_collaborator_participant_space_matches
  task_collaborator_participant_space_matches="$(rg -n -P -- 'participant_space_id|ParticipantSpaceID|participant space|participant_space|collaborator_scope_id|CollaboratorScopeID|collaborator_scope|newTaskScopeParticipantSpaceID|task_scope_executions_participant_space|fk_task_scope_executions_participant_space' internal/store/taskboard internal/store/message internal/store/app internal/tidb/bootstrap.sql internal/tidb/admin.go specs/storage specs/task 2>/dev/null || true)"
  if [[ -n "$task_collaborator_participant_space_matches" ]]; then
    fail 'task collaborator runtime/current schema/specs must not preserve participant-space/collaborator-scope edge columns; legacy names are allowed only in historical migrations/testdata/rollback SQL'
    printf '%s\n' "$task_collaborator_participant_space_matches" >&2
  fi

  if [[ -d internal/store/reaction ]]; then
    local legacy_reaction_matches
    legacy_reaction_matches="$(rg -n -P -- "$legacy_scope_identifier_pattern" internal/store/reaction 2>/dev/null || true)"
    if [[ -n "$legacy_reaction_matches" ]]; then
      fail 'reaction target/access code must use explicit scope roles, not generic space identifiers'
      printf '%s\n' "$legacy_reaction_matches" >&2
    fi

    if ! grep -Fq 'ScopeTargetOperationReactionWrite' internal/store/reaction/reactions.go; then
      fail 'reaction writes must be represented in the shared scope target/access contract'
    fi
    if ! grep -Fq 'ResolvedScopeTarget' internal/store/reaction/reactions.go; then
      fail 'reaction writes must validate a ResolvedScopeTarget instead of a reaction-local target contract'
    fi
  fi
}

case "${1:-}" in
  --inventory)
    print_inventory
    exit 0
    ;;
  --check-only|"")
    run_checks
    ;;
  *)
    printf 'Usage: %s [--check-only|--inventory]\n' "$0" >&2
    exit 2
    ;;
esac

if (( failures > 0 )); then
  printf '\nscope target/access contract guardrail failed with %d issue(s).\n' "$failures" >&2
  exit 1
fi

printf 'scope target/access contract guardrail passed.\n'
