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

ROOT="${AGENTTEAM_MESSAGE_REWIND_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_not_contains() {
  local file="$1"
  local needle="$2"
  local label="${3:-$file must not contain $needle}"
  if [[ ! -f "$file" ]]; then
    fail "missing $file"
    return
  fi
  if grep -Fq -- "$needle" "$file"; then
    fail "$label"
  fi
}

print_inventory() {
  if [[ -f guardrails/message/rewind-debt-inventory.md ]]; then
    cat guardrails/message/rewind-debt-inventory.md
  else
    printf 'No rewind debt inventory found.\n'
  fi
}

run_checks() {
  require_file specs/message/rewind.md
  require_file guardrails/message/rewind-debt-inventory.md
  require_file internal/core/rewindplan/types.go
  require_file internal/core/rewindplan/boundary.go
  require_file internal/store/rewind/store.go
  require_file internal/store/rewind/operation.go
  require_file internal/store/message/metadata.go
  require_file internal/store/message/message_semantics.go
  require_file internal/store/message/message_types.go
  require_file internal/store/message/scope_private_authority_sql.go
  require_file internal/store/message/scope_specialties.go
  require_file internal/store/message/shared_query_builders.go
  require_file internal/store/message/sidecar_authority.go
  require_file internal/store/message/scope_specialties_test.go
  require_file internal/store/message/scope_rewind_read_projection_test.go
  require_file internal/store/message/delivery_diagnostics_store_test.go
  require_file internal/api/dto/app_messages.go
  require_file internal/api/im/rewind_handler.go

  require_contains specs/message/README.md './rewind.md' 'message README must link the rewind spec'
  require_contains specs/message/rewind.md 'timeline rewound tombstone' 'rewind spec must define timeline rewound tombstone semantics'
  require_contains specs/message/rewind.md '不物理删除 primary suffix `scope_messages` 行' 'rewind spec must forbid primary suffix transcript removal'
  require_contains specs/message/rewind.md 'messageRecall.status = "rewound"' 'rewind spec must use the shared message lifecycle metadata key'
  require_contains specs/message/rewind.md 'recalled` 仍然是单条撤回占位' 'rewind spec must distinguish recalled placeholder from rewound invisibility'
  require_contains specs/message/rewind.md 'runtime 真实恢复' 'rewind spec must keep native runtime restore semantics'
  require_contains specs/message/rewind.md 'runtime 常量和 strategy 常量分别建模' 'rewind spec must require separate runtime and strategy constants'
  require_contains specs/message/rewind.md '策略名不是 runtime ID' 'rewind spec must separate runtime IDs from recovery strategy names'
  require_contains specs/message/rewind.md 'codex_app_server_turns' 'rewind spec must document codex native strategy'
  require_contains specs/message/rewind.md 'claude_session_resume_at' 'rewind spec must document claude native strategy'
  require_contains specs/message/rewind.md 'opencode_session_revert' 'rewind spec must document opencode native strategy'
  require_contains specs/message/rewind.md 'scope_activity_events' 'rewind spec must name activity suppression'
  require_contains specs/message/rewind.md 'scope_fanout_outbox' 'rewind spec must name outbox suppression'
  require_contains specs/message/rewind.md 'Scope suffix boundary contract' 'rewind spec must keep suffix boundary contract explicit'
  require_contains specs/message/rewind.md '`OperationScopeID`' 'rewind spec must name the durable operation serialization scope role'
  require_contains specs/message/rewind.md '`BroadcastScopeID`' 'rewind spec must name the realtime invalidation scope role'
  require_contains specs/message/rewind.md '`ReplyThreadScopeID`' 'rewind spec must name the reply-thread state recompute scope role'
  require_contains specs/message/rewind.md 'typed scope roles / `scope_id` boundary' 'rewind spec must require compatibility names to normalize to typed scope roles'
  require_contains specs/message/rewind.md 'Operation lifecycle' 'rewind spec must keep operation lifecycle explicit'
  require_contains specs/message/rewind.md 'completed' 'rewind spec must document completed operation status'
  require_contains specs/message/rewind.md 'conversation:rewound' 'rewind spec must document direct realtime invalidation'
  require_contains specs/message/rewind.md '不是 message append、reaction activity、runtime event 或 outbox delivery' 'rewind spec must keep conversation:rewound as direct no-outbox invalidation'
  require_contains specs/message/rewind.md '`conversation:rewound` payload 只提供 invalidation authority' 'rewind spec must keep conversation:rewound payload as invalidation-only authority'
  require_contains specs/message/rewind.md '不能携带 rewound suffix payload、frontend tombstone patch 或 per-surface replacement facts' 'rewind spec must forbid frontend tombstone/replacement payloads'
  require_contains specs/message/rewind.md 'message sync 不返回 tombstone page' 'rewind spec must keep message sync non-tombstone contract'
  require_contains specs/message/rewind.md 'Browser 客户端 recovery 顺序必须显式' 'rewind spec must keep browser recovery sequence explicit'
  require_contains specs/message/rewind.md '先推进本地 message-sync cursor generation' 'rewind spec must require advancing browser cursor generation before recovery'
  require_contains specs/message/rewind.md '再清空内存和持久化 opaque message-sync cursor' 'rewind spec must require clearing browser cursor state after generation advance'
  require_contains specs/message/rewind.md 'Read/search/source/share-card action state 必须先于 transcript `scope_messages` tombstone' 'rewind spec must keep projection-before-transcript-tombstone contract'
  require_contains specs/message/rewind.md 'Projection impact / read-surface boundary' 'rewind spec must name the projection impact/read-surface boundary'
  require_contains specs/message/rewind.md 'projection-backed surfaces' 'rewind spec must distinguish projection-backed surfaces from transcript authority'
  require_contains specs/message/rewind.md 'transcript-authority surfaces' 'rewind spec must keep transcript-authority read surfaces explicit'
  require_contains specs/message/rewind.md '它不是 message-sync tombstone payload' 'rewind projection/read-surface contract must not become a message-sync tombstone payload'
  require_contains specs/message/rewind.md 'Delivery diagnostics / diagnostics preview 的 live message authority 必须来自 readable `scope_messages`' 'rewind spec must keep diagnostics no-resurrection authority on readable scope_messages'
  require_contains specs/message/rewind.md '诊断查询必须先命中 readable transcript row' 'rewind spec must keep diagnostics query rooted in readable transcript rows'
  require_contains specs/message/rewind.md 'rewound transcript row 必须返回 not found / invisible' 'rewind spec must require rewound rows to hide diagnostics'
  require_contains specs/message/rewind.md '`claude_session_resume_at` 和 `opencode_session_revert` 是正式 runtime recovery strategies' 'rewind spec must describe current runtime recovery strategies'
  removed_opencode_strategy="opencode_session_""fork"
  removed_claude_strategy="claude_session_""snapshot"
  removed_legacy_apply_phrase="suffix ""delete ""操作"
  removed_physical_delete_phrase="target message 及其后的 suffix 会被服务端""物理删除"
  require_not_contains specs/message/rewind.md "$removed_legacy_apply_phrase" 'rewind spec must not describe message-side apply with the removed legacy wording'
  require_not_contains specs/message/rewind.md "$removed_physical_delete_phrase" 'rewind spec must not require physical deletion of the primary suffix transcript'
  require_not_contains specs/message/rewind.md "$removed_opencode_strategy" 'rewind spec must not mention removed OpenCode fork strategy'
  require_not_contains specs/message/rewind.md "$removed_claude_strategy" 'rewind spec must not mention removed Claude snapshot strategy'

  require_contains guardrails/message/rewind-debt-inventory.md 'rewound lifecycle' 'rewind inventory must record lifecycle tombstone semantics'
  require_contains guardrails/message/rewind-debt-inventory.md 'not a physical transcript' 'rewind inventory must forbid physical transcript delete semantics'
  require_contains guardrails/message/rewind-debt-inventory.md 'Message sync is not a tombstone page' 'rewind inventory must keep message sync non-tombstone contract'
  require_contains guardrails/message/rewind-debt-inventory.md 'Supported runtime implementations are currently `codex`, `claude`, and' 'rewind inventory must name supported runtime capabilities'
  require_contains guardrails/message/rewind-debt-inventory.md '`opencode`; `builtin` follows OpenCode capability' 'rewind inventory must name builtin as OpenCode capability alias'
  require_contains guardrails/message/rewind-debt-inventory.md 'recovery strategy names such as `claude_session_resume_at` and' 'rewind inventory must keep strategy names out of runtime negotiation'
  require_contains guardrails/message/rewind-debt-inventory.md '`opencode_session_revert` are not runtime IDs' 'rewind inventory must keep opencode strategy names out of runtime negotiation'
  require_contains guardrails/message/rewind-debt-inventory.md 'LAN E2E verifies the behavior' 'rewind inventory must keep LAN E2E gate for new runtimes'
  require_contains guardrails/message/rewind-debt-inventory.md 'before release checks exist' 'rewind inventory must keep LAN E2E gate for runtime release'
  require_contains guardrails/message/rewind-debt-inventory.md 'Rewind plan scope roles are explicit in code' 'rewind inventory must record explicit rewind plan scope roles'
  require_contains guardrails/message/rewind-debt-inventory.md 'Plan structs no longer carry alias fields' 'rewind inventory must record that plan aliases were removed'
  require_contains guardrails/message/rewind-debt-inventory.md 'The guardrail rejects reintroducing those aliases' 'rewind inventory must require typed rewind scope accessors'
  require_contains guardrails/message/rewind-debt-inventory.md 'generation-bound invalidation' 'rewind inventory must characterize browser recovery as generation-bound invalidation'
  require_contains guardrails/message/rewind-debt-inventory.md 'not a frontend suffix patch' 'rewind inventory must forbid frontend suffix patch semantics for conversation:rewound'
  require_not_contains guardrails/message/rewind-debt-inventory.md "$removed_opencode_strategy" 'rewind inventory must not mention removed OpenCode fork strategy'
  require_not_contains guardrails/message/rewind-debt-inventory.md "$removed_claude_strategy" 'rewind inventory must not mention removed Claude snapshot strategy'

  require_contains internal/core/rewindcaps/rewindcaps.go 'RuntimeClaude' 'rewind capability map must keep claude runtime constant explicit'
  require_contains internal/core/rewindcaps/rewindcaps.go 'RuntimeCodex' 'rewind capability map must keep codex runtime constant explicit'
  require_contains internal/core/rewindcaps/rewindcaps.go 'RuntimeOpenCode' 'rewind capability map must keep opencode runtime constant explicit'
  require_contains internal/core/rewindcaps/rewindcaps.go 'RuntimeBuiltin' 'rewind capability map must keep builtin runtime constant explicit'
  require_contains internal/core/rewindcaps/rewindcaps.go 'trimmed == RuntimeBuiltin' 'rewind capability map must route builtin through opencode'
  require_contains internal/core/rewindcaps/rewindcaps.go 'StrategyClaudeSessionResumeAt' 'rewind capability map must keep claude native strategy explicit'
  require_contains internal/core/rewindcaps/rewindcaps.go 'StrategyOpenCodeSessionRevert' 'rewind capability map must keep OpenCode native strategy explicit'
  require_contains internal/core/rewindcaps/rewindcaps.go 'UnsupportedReasonUnknownRuntime' 'rewind capability map must keep unknown runtime reason explicit'
  require_contains internal/core/rewindcaps/rewindcaps_test.go 'TestSupportedRuntimes' 'rewind runtime capability supported-set test must remain'
  require_contains internal/core/rewindcaps/rewindcaps_test.go 'TestRuntimeCapabilityDoesNotTreatReservedStrategiesAsRuntimes' 'rewind runtime capability must test reserved strategies are not runtimes'
  require_contains internal/core/rewindcaps/rewindcaps_test.go 'StrategyClaudeSessionResumeAt, StrategyOpenCodeSessionRevert' 'rewind runtime capability test must keep strategy names out of runtime IDs'
  require_contains internal/core/rewindcaps/rewindcaps_test.go '{runtime: RuntimeClaude, supported: true, strategy: StrategyClaudeSessionResumeAt}' 'rewind runtime capability test must keep claude supported native strategy'
  require_contains internal/core/rewindcaps/rewindcaps_test.go '{runtime: RuntimeOpenCode, supported: true, strategy: StrategyOpenCodeSessionRevert}' 'rewind runtime capability test must keep OpenCode supported native strategy'

  require_contains internal/core/rewindplan/types.go 'ScopeID is the canonical storage/tombstone scope' 'rewind plan type must name the primary storage/tombstone scope explicitly'
  require_contains internal/core/rewindplan/types.go 'BroadcastScopeID is the scope that receives coarse post-commit realtime' 'rewind plan type must name the realtime broadcast scope explicitly'
  require_contains internal/core/rewindplan/types.go 'ReplyThreadScopeID is populated only when the primary rewind target is a' 'rewind plan type must name reply-thread state scope explicitly'
  require_contains internal/core/rewindplan/types.go 'CascadeReplyThreads holds reply threads' 'rewind plan type must keep cascade reply-thread facts explicit'
  require_contains internal/core/rewindplan/types.go 'cascade-rewound' 'rewind cascade plan type must name child tombstone semantics explicitly'
  require_contains internal/core/rewindplan/types.go 'ScopeID is the canonical child reply-thread scope' 'rewind cascade plan type must name child scope explicitly'
  require_contains internal/store/rewind/store.go 'ValidateRewindPlan' 'rewind store must validate plans before apply'
  require_contains internal/store/rewind/store.go 'LoadCurrentSuffixMessageIDs' 'rewind store must re-load suffix message ids for drift validation'
  require_contains internal/store/rewind/store.go 'AgentHasCrossSurfaceActivityAfterExcluding' 'rewind store must guard agent cross-surface activity'
  require_contains internal/store/rewind/store.go 'RewindCascadeReplyThread' 'rewind store must cascade via rewind tombstone helper'
  require_contains internal/store/rewind/store.go 'rewindplan.OperationStatusCompleted' 'rewind operation completion must stay explicit and typed'
  require_contains internal/store/rewind/store.go 'plan.OperationScopeID()' 'rewind store must serialize operations through the typed operation scope accessor'
  require_contains internal/store/rewind/store.go 'plan.ReplyThreadStateScopeID()' 'rewind store must recompute reply-thread state through the typed scope accessor'
  require_contains internal/store/rewind/store.go 'crt.CascadeScopeID()' 'rewind store must use the typed cascade scope accessor'
  require_contains internal/store/rewind/operation.go 'SetOperationStatus' 'rewind operation status updates must use the typed helper boundary'
  require_contains internal/store/rewind/operation.go 'RewindCascadeReplyThread' 'rewind cascade operation must keep tombstone helper explicit'
  require_contains internal/store/rewind/operation.go 'DeleteCascadeReplyThread' 'channel deletion may keep a separate physical delete path'
  require_contains internal/store/rewind/operation.go 'crt.CascadeScopeID()' 'rewind cascade operation must use the typed cascade scope accessor'
  require_contains internal/core/rewindplan/boundary.go 'type OperationStatus string' 'rewind operation statuses must stay typed'
  require_contains internal/core/rewindplan/boundary.go 'OperationStatusCompleted' 'rewind completed status must stay in the typed lifecycle set'
  require_contains internal/core/rewindplan/boundary.go 'type SuffixBoundary struct' 'rewind suffix boundary must stay typed'
  require_contains internal/core/rewindplan/boundary.go 'SuffixBoundaryPrimary' 'rewind primary suffix boundary must stay explicit'
  require_contains internal/core/rewindplan/boundary.go 'SuffixBoundaryCascadeReplyThread' 'rewind cascade suffix boundary must stay explicit'
  require_contains internal/store/rewind/store.go 'ValidateRewindPlan' 'rewind store must validate plans before apply'
  require_contains internal/store/rewind/store.go 'LoadCurrentSuffixMessageIDs' 'rewind store must re-load suffix message ids for drift validation'
  require_contains internal/store/rewind/store.go 'AgentHasCrossSurfaceActivityAfterExcluding' 'rewind store must guard agent cross-surface activity'
  require_contains internal/store/rewind/store.go 'RewindCascadeReplyThread' 'rewind store must cascade via rewind tombstone helper'
  require_contains internal/store/rewind/store.go 'rewindplan.OperationStatusCompleted' 'rewind operation completion must stay explicit and typed'
  require_contains internal/store/rewind/operation.go 'RewindCascadeReplyThread' 'rewind cascade operation must stay separate from channel deletion'
  require_contains internal/store/rewind/operation.go 'DeleteCascadeReplyThread' 'channel deletion may keep a separate physical delete path'

  local rewind_plan_alias_matches
  rewind_plan_alias_matches="$(rg -n -P -- '\b(ChannelID|BroadcastChannelID|ReplyThreadID|ReplyParentID|ThreadChannelID|ThreadID)\b' internal/core/rewindplan/types.go internal/core/rewindplan/boundary.go 2>/dev/null || true)"
  if [[ -n "$rewind_plan_alias_matches" ]]; then
    fail 'rewind plan contracts must not reintroduce channel/thread alias fields'
    printf '%s\n' "$rewind_plan_alias_matches" >&2
  fi

  require_contains internal/store/message/metadata.go 'MessageLifecycleStatusRecalled' 'message lifecycle must name recalled status centrally'
  require_contains internal/store/message/metadata.go 'MessageLifecycleStatusRewound' 'message lifecycle must name rewound status centrally'
  require_contains internal/store/message/metadata.go 'BuildMessageRewoundMetadata' 'rewind tombstone metadata builder must stay centralized'
  require_contains internal/store/message/metadata.go 'MessageMetadataIsRewound' 'rewind lifecycle detector must stay centralized'
  require_contains internal/store/message/message_semantics.go 'applyLifecycleSemantics' 'rewind lifecycle must affect message semantics centrally'
  require_contains internal/store/message/message_types.go 'MessageNotRewoundSQL' 'rewind visible-row SQL predicate must stay centralized'
  require_contains internal/store/message/message_types.go 'MessageNotLifecycleTombstoneSQL' 'sidecar batch reads must filter lifecycle tombstones centrally'
  require_contains internal/store/message/scope_private_authority_sql.go 'MessageNotRewoundSQL("m")' 'read authority predicate must filter rewound messages'
  require_contains internal/store/message/scope_specialties.go 'scopeMessageTombstoneProjectionSteps' 'recall/rewind projection scrub contract must stay centralized'
  require_contains internal/store/message/scope_specialties.go 'scopeMessageTombstoneTranscriptStep' 'rewind transcript tombstone step must stay explicit'
  require_contains internal/store/message/scope_specialties.go 'BuildMessageRewoundMetadata' 'rewind suffix must write lifecycle metadata'
  require_contains internal/store/message/scope_specialties.go 'tombstoneScopeRewindSuffixMessages' 'rewind suffix must tombstone transcript rows through a named helper'
  require_contains internal/store/message/scope_specialties.go 'scrubScopeRewindDurableIMProjections' 'rewind must scrub durable IM stream/conversation projections'
  require_contains internal/store/message/scope_specialties.go 'DELETE FROM scope_share_cards' 'rewind must scrub share-card action state'
  require_contains internal/store/message/scope_specialties.go 'DELETE FROM scope_message_reactions' 'rewind must scrub reaction state for suffix messages'
  require_contains internal/store/message/scope_specialties.go 'DELETE FROM scope_message_pins' 'rewind must scrub pin state for suffix messages'
  require_contains internal/store/message/scope_specialties.go 'DELETE FROM scope_message_search_docs' 'rewind must scrub search docs for suffix messages'
  require_contains internal/store/message/scope_specialties.go 'DELETE FROM scope_message_sources' 'rewind must scrub source refs targeting or sourced from suffix messages'
  require_contains internal/store/message/scope_specialties.go 'UPDATE scope_messages' 'rewind must tombstone suffix message rows'
  require_not_contains internal/store/message/scope_specialties.go 'DELETE FROM scope_messages' 'rewind message store must not physically delete primary suffix transcript rows'
  require_contains internal/store/message/scope_specialties.go "status IN ('pending', 'leased')" 'rewind outbox suppression must stay limited to unfinished outbox rows'
  require_contains internal/store/message/scope_specialties.go 'clampScopeMembershipReadState' 'rewind must clamp scope read state after tombstone'
  require_contains internal/store/message/scope_specialties.go 'recomputeScopeStats' 'rewind must recompute scope stats after tombstone'
  require_contains internal/store/message/scope_specialties.go 'clampScopeMessageSyncDirtyState' 'rewind must clamp message sync dirty state after tombstone'
  require_contains internal/store/message/shared_query_builders.go 'BuildMessageDiagnosticsScopeQuery starts from currently readable' 'rewind diagnostics query builder must document readable transcript authority'
  require_contains internal/store/message/shared_query_builders.go 'FROM scope_messages m' 'rewind diagnostics must query scope_messages as source of truth'
  require_contains internal/store/message/sidecar_authority.go 'MessageMetadataIsTombstone' 'sidecar reads must reject lifecycle tombstones'
  require_contains internal/store/message/owner_private_store.go 'MessageNotRewoundSQL' 'owner quote source loads must filter rewound messages'
  require_contains internal/store/app/app_messages.go 'MessageNotRewoundSQL' 'participation stats must filter rewound messages'

  require_contains internal/api/dto/app_messages.go 'rewoundMessageIds' 'rewind API response must use rewound naming'
  require_not_contains internal/api/dto/app_messages.go 'deletedMessageIds' 'rewind API response must not use deleted suffix naming'
  require_contains internal/api/im/rewind_handler.go 'RewoundMessageIDs' 'IM rewind handler must expose rewound message ids, not deleted ids'
  require_not_contains internal/api/im/rewind_handler.go 'rewind delete failed' 'IM rewind handler must not use delete failure wording'

  require_contains internal/store/message/scope_specialties_test.go 'TestScopeMessageTombstoneProjectionLifecycleIsExplicit' 'rewind tombstone lifecycle test must remain'
  require_contains internal/store/message/scope_specialties_test.go 'transcript rows are tombstoned after read/search/source projections' 'rewind lifecycle test must keep transcript-last tombstone assertion'
  require_contains internal/store/message/scope_specialties_test.go 'TestScopeRewindSuffixTombstonesDerivedFactsAndRecomputesState' 'rewind derived-facts characterization test must remain'
  require_contains internal/store/message/scope_rewind_read_projection_test.go 'TestScopeRewindSuffixRemovesReadProjectionAndSearchVisibility' 'rewind read/projection visibility test must remain'
  require_contains internal/store/message/scope_rewind_read_projection_test.go 'afterReadCheck.Allowed' 'rewind read-check must reject rewound suffix messages'
  require_contains internal/store/message/scope_rewind_read_projection_test.go 'rewindSearchCandidatesFromProjectedDocsForTest' 'rewind search visibility must follow projected docs and readable transcript'
  require_contains internal/store/message/scope_rewind_read_projection_test.go 'GetMessageDeliveryDiagnosticsForUser' 'rewind regression must cover diagnostics preview disappearance'
  require_contains internal/store/message/scope_rewind_read_projection_test.go 'syncMessageIDsForRewindTest' 'rewind regression must cover message sync payload disappearance'
  require_contains internal/store/message/scope_rewind_read_projection_test.go 'im_stream_events' 'rewind regression must cover durable stream replay scrub'
  require_contains internal/store/message/scope_rewind_read_projection_test.go 'im_conversation_index' 'rewind regression must cover durable conversation projection scrub'
  require_contains internal/store/message/delivery_diagnostics_store_test.go 'TestGetMessageDeliveryDiagnosticsForUserDoesNotResurrectRewoundSuffix' 'rewind delivery diagnostics no-resurrection test must remain'
  require_contains internal/store/message/delivery_diagnostics_store_test.go 'require.ErrorIs(t, err, ErrNotFound)' 'rewind diagnostics test must assert rewound suffix diagnostics are not found'
}

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 '\nrewind contract guardrail failed with %d issue(s).\n' "$failures" >&2
  exit 1
fi

printf 'rewind contract guardrail passed.\n'
