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

# Invariant: round-10 error-contract integration must keep all three dependency
# behaviors and their focused tests present and wired, so none can silently
# disappear. Machine-checkable (see #t2687 integrating #t2684/#t2685/#t2686).
#
# The three integrated contracts:
#   A. Restored observation (#t2684/#t2665): storeerr.InternalCause is consumed
#      only at the structured observation boundary; never in public
#      errors.Is/As, HTTP/Exec JSON, prompts, or protected-selector responses.
#   B. Public failure receipts (#t2685): shared public producer
#      validators (code/stage/path/WorkItem key/dependencyPath fail closed, never
#      truncated into a different identity) and closed truncation receipts
#      (true-union dedup+sort, prior+fresh merge, fresh >32KiB minimal fallback,
#      DroppedCount/MinimalFallback in clone/equality/digest/replay).
#   C. Closed Run event/EffectResult schemas (#t2686): every event kind and flat
#      EffectResult is a closed typed public schema with typed ID prefixes,
#      closed enums, RFC3339 retryAt, validated list members/WorkItem fields.
#
# Cross-layer propagation matrix (store -> API -> runtime -> agent -> desktop):
#   see specs/workflow/error-contract-propagation.md — this guardrail proves the
#   code and tests named there still exist and are wired.

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$repo_root"

if ! command -v rg >/dev/null 2>&1; then
  echo "FAIL: ripgrep (rg) is required" >&2
  exit 2
fi

status=0
fail() { echo "FAIL: $1" >&2; status=1; }
ok()   { echo "OK: $1"; }

# ---------------------------------------------------------------------------
# A. Restored observation (#t2684)
# ---------------------------------------------------------------------------
if rg -q 'storeerr\.InternalCause' internal/api/error_observation.go \
  && rg -q 'func ObservedErrorWithCause' internal/api/error_observation.go; then
  ok "A1 error_observation.go consumes storeerr.InternalCause at the structured boundary and declares ObservedErrorWithCause"
else
  fail "A1 error_observation.go must consume storeerr.InternalCause and declare ObservedErrorWithCause"
fi

if rg -q 'ObservedErrorWithCause' internal/api/agent/api_workflow_steps.go \
  && rg -q '"internal_cause"' internal/api/agent/api_workflow_steps.go; then
  ok "A2 Workflow Step handler logs internal_cause as a separate slog attribute"
else
  fail "A2 api_workflow_steps.go must log internal_cause separately via ObservedErrorWithCause"
fi

if [ -f guardrails/api/internal-cause-observation.sh ] \
  && rg -q 'guardrails/api/internal-cause-observation.sh' scripts/harness-fast.sh; then
  ok "A3 internal-cause observation guardrail exists and is wired into harness-fast.sh"
else
  fail "A3 guardrails/api/internal-cause-observation.sh must exist and be wired into scripts/harness-fast.sh"
fi

if [ -f internal/api/error_observation_test.go ] \
  && [ -f internal/api/agent/workflow_observation_test.go ]; then
  ok "A4 both observation regression suites exist"
else
  fail "A4 observation regression suites (internal/api/error_observation_test.go, internal/api/agent/workflow_observation_test.go) must exist"
fi

# ---------------------------------------------------------------------------
# B. Public failure receipts (#t2685)
# ---------------------------------------------------------------------------
if [ -f internal/workflow/run/failure_identity.go ] \
  && rg -q 'func PublicFailureCode' internal/workflow/run/failure_identity.go \
  && rg -q 'func PublicFailurePath' internal/workflow/run/failure_identity.go; then
  ok "B1 shared public producer validators exist (PublicFailureCode/Path)"
else
  fail "B1 internal/workflow/run/failure_identity.go must declare public producer validators"
fi

if [ -f internal/workflow/run/failure_identity_test.go ] \
  && [ -f internal/api/workflow/run/failure_receipt_roundtrip_test.go ] \
  && [ -f internal/workflow/orchestrator/persisted_truncation_equality_test.go ]; then
  ok "B2 public failure receipt focused tests exist (identity, round-trip, persisted truncation equality)"
else
  fail "B2 public failure receipt focused tests must exist"
fi

if rg -q 'DroppedCount' internal/api/workflow/run/failure_projection.go \
  && rg -q 'MinimalFallback' internal/api/workflow/run/failure_projection.go \
  && rg -q 'func mergeFailureTruncation' internal/workflow/run/failure_diagnostic_budget.go; then
  ok "B3 failure projection closes truncation receipts (DroppedCount/MinimalFallback/merge)"
else
  fail "B3 failure_projection.go must merge truncation receipts with DroppedCount/MinimalFallback via failure_diagnostic_budget.go"
fi

if rg -q 'DroppedCount' internal/workflow/run/reducer.go \
  && rg -q 'MinimalFallback' internal/workflow/run/reducer.go \
  && rg -q 'TruncatedFields' internal/workflow/run/reducer.go; then
  ok "B4 completion failure digest includes the truncation receipt fields"
else
  fail "B4 internal/workflow/run/reducer.go completionFailureDigest must include DroppedCount/MinimalFallback/TruncatedFields"
fi

# ---------------------------------------------------------------------------
# C. Closed Run event/EffectResult schemas (#t2686)
# ---------------------------------------------------------------------------
if [ -f internal/api/workflow/run/event_effect_schema.go ] \
  && rg -q 'projectOperationalEventEnvelope' internal/api/workflow/run/failure_projection.go; then
  ok "C1 per-kind closed event schema exists and failure projection routes through it"
else
  fail "C1 event_effect_schema.go must exist and failure_projection.go must route envelopes through per-kind schema"
fi

if [ -f internal/api/workflow/run/event_effect_schema_test.go ] \
  && [ -f internal/api/workflow/run/event_effect_schema_storage_test.go ]; then
  ok "C2 event/effect schema focused tests exist (unit + storage/E2E)"
else
  fail "C2 event/effect schema tests must exist"
fi

# Producer-derived ID grammars, tied mechanically to each generator family:
# kernel logical activation (a+digits), 32-hex messageId + msg_ legacy,
# StableChildID wfx_/wra_ (32 hex), deterministic btn_ notification (32 hex),
# NewEntityID usr_/agt_ (14 base36), and RAW NewMemberID (12 base36, NO mem_).
if rg -q 'envelopeFieldKernelActivationID' internal/api/workflow/run/event_effect_schema.go \
  && rg -q 'envelopeFieldMessageID' internal/api/workflow/run/event_effect_schema.go \
  && rg -q 'envelopeFieldStableChildID' internal/api/workflow/run/event_effect_schema.go \
  && rg -q 'envelopeFieldEntityID' internal/api/workflow/run/event_effect_schema.go \
  && rg -q 'envelopeFieldRawMemberID' internal/api/workflow/run/event_effect_schema.go \
  && rg -q 'envelopeFieldActor' internal/api/workflow/run/event_effect_schema.go \
  && rg -q 'envelopeFieldCancelReason' internal/api/workflow/run/event_effect_schema.go; then
  ok "C3 generator-family validators exist (activation/messageId/StableChildID/EntityID/raw-member/actor/cancelReason)"
else
  fail "C3 event_effect_schema.go must declare per-generator-family validators (kernel activation, 32-hex messageId, StableChildID wfx/btn hex, NewEntityID base36, raw NewMemberID, actor, cancelReason)"
fi

# The factually-false universal ValidNewEntityIDForm/knownEntityIDPrefixes
# source of truth must be gone; the identifiers package exposes per-family
# validators tied to the producers.
if rg -q 'func ValidNewEntityIDForm' internal/core/identifiers/ids.go \
  && rg -q 'func ValidStableChildID' internal/core/identifiers/ids.go \
  && rg -q 'func ValidMemberIDRaw' internal/core/identifiers/ids.go \
  && ! rg -q 'knownEntityIDPrefixes' internal/core/identifiers/ids.go; then
  ok "C3b producer-family validators exported in identifiers (EntityIDPrefix/StableChildID/MemberIDRaw); no universal prefix list"
else
  fail "C3b identifiers must export per-family validators (ValidNewEntityIDForm/ValidStableChildID/ValidMemberIDRaw) and remove knownEntityIDPrefixes"
fi

# Exact failure-namespace allowlist: a flat failure event never inherits the
# EffectResult open output exemption and never retains unknown failure-prefixed
# secrets. The allowlist lives in the effect_result_projection.go split.
if rg -q 'exactEffectResultFailureNamespace' internal/api/workflow/run/effect_result_projection.go \
  && rg -q 'projectEffectResultFailureFields' internal/api/workflow/run/effect_result_projection.go \
  && rg -q 'isKnownEffectResultFailureField' internal/api/workflow/run/effect_result_projection.go; then
  ok "C4 exact failure-namespace allowlist exists (no output exemption, no unknown failure-prefixed secrets)"
else
  fail "C4 effect_result_projection.go must close the flat EffectResult failure namespace with an exact allowlist"
fi

# Exhaustive event-kind registry + machine-checkable producer<->schema invariant.
# The producer side is derived mechanically from source via an AST bijection
# (NOT a self-mirroring literal): it detects new kinds, changed Event vs
# FailureEvent classification, stale/duplicate schema entries, and the reviewed
# concrete field corrections independently.
if [ -f internal/api/workflow/run/event_kind_invariant_test.go ] \
  && rg -q 'TestProducerKindSchemaBijection' internal/api/workflow/run/event_kind_invariant_test.go \
  && rg -q 'TestConcreteReviewedEventFieldCorrections' internal/api/workflow/run/event_kind_invariant_test.go \
  && rg -q 'discoverProducerEventKinds' internal/api/workflow/run/event_kind_invariant_test.go \
  && rg -q 'go/ast' internal/api/workflow/run/event_kind_invariant_test.go; then
  ok "C5 AST producer<->schema bijection invariant exists (mechanical kind/classification/stale detection + concrete field corrections)"
else
  fail "C5 event_kind_invariant_test.go must derive producer kinds mechanically from AST (not a literal list) and pin the concrete reviewed field corrections"
fi

# Producer-driven regressions exist: tests that call the REAL generators
# (StableChildID, NewEntityID, NewMemberID, NewMessageID) and project them
# through the public surfaces, plus malformed actor redaction.
if [ -f internal/api/workflow/run/producer_generator_roundtrip_test.go ] \
  && rg -q 'orchestrator.StableChildID' internal/api/workflow/run/producer_generator_roundtrip_test.go \
  && rg -q 'identifiers.NewEntityID' internal/api/workflow/run/producer_generator_roundtrip_test.go \
  && rg -q 'identifiers.NewMemberID' internal/api/workflow/run/producer_generator_roundtrip_test.go \
  && rg -q 'TestRealActorContextSurvivesAndMalformedActorDrops' internal/api/workflow/run/producer_generator_roundtrip_test.go; then
  ok "C5d producer-driven regressions exist (real StableChildID/NewEntityID/NewMemberID/NewMessageID through public projections + actor redaction)"
else
  fail "C5d producer_generator_roundtrip_test.go must drive real generators through the public projections and cover actor redaction"
fi

# Producer-owned contract architecture: the exact event field sets are declared
# at the producing boundary (orchestrator) and mechanically consumed/checked; the
# genuine contract path drives a real dispatcher with real producer ids through
# the workflow store and the public DTOs. The cross-layer genuine tests live in
# the API package (not the store package) so store never imports API.
if [ -f internal/workflow/orchestrator/producer_event_fields.go ] \
  && rg -q 'ProducerEventFieldSets' internal/workflow/orchestrator/producer_event_fields.go \
  && [ -f internal/workflow/orchestrator/producer_event_fields_test.go ] \
  && rg -q 'TestProducerOwnedFieldSetsCoversEveryDispatchEvent' internal/workflow/orchestrator/producer_event_fields_test.go \
  && [ -f internal/api/workflow/run/genuine_fullchain_e2e_test.go ] \
  && rg -q 'TestGenuineProducerContractPathThroughPublicDTOs' internal/api/workflow/run/genuine_fullchain_e2e_test.go \
  && rg -q 'orchestrator.StableChildID' internal/api/workflow/run/genuine_fullchain_e2e_test.go \
  && rg -q 'TestGenuineDispatcherFailureThroughRegisteredAuthenticatedHTTPRoute' internal/api/workflow/run/genuine_fullchain_e2e_test.go \
  && ! rg -q 'github\.com/tidbcloud/AgentTeam/internal/api' internal/store --glob '*.go'; then
  ok "C5e producer-owned event field registry + genuine contract path exist (real dispatcher + StableChildID through public DTOs; genuine tests live in API package, store never imports API)"
else
  fail "C5e producer-owned event field registry (producer_event_fields.go) and the genuine API-package full-chain path (genuine_fullchain_e2e_test.go) must exist; store must never import internal/api"
fi

# Cross-package producer->public schema bijection (#t2587): the complete
# producer event declarations live in the neutral shared contract package
# (internal/workflow/eventcontract) imported by BOTH the producers and the closed
# public projector; the API projector enforces the bijection and the bijection
# tests cover missing/extra kinds, field drift, and failure-capability mismatch.
if [ -f internal/workflow/eventcontract/event_fields.go ] \
  && rg -q 'ProducerEventFieldSets' internal/workflow/eventcontract/event_fields.go \
  && rg -q 'eventcontract.Lookup' internal/api/workflow/run/event_effect_schema.go \
  && [ -f internal/api/workflow/run/event_contract_bijection_test.go ] \
  && rg -q 'TestCrossPackageBijectionEveryProducerKindHasPublicSchema' internal/api/workflow/run/event_contract_bijection_test.go \
  && rg -q 'TestCrossPackageBijectionCompleteKindCoverage' internal/api/workflow/run/event_contract_bijection_test.go; then
  ok "C5f cross-package producer->public schema bijection exists (shared eventcontract + API enforcement + mutation/drift tests)"
else
  fail "C5f the neutral eventcontract package and the cross-package bijection tests must exist and be wired into eventKindSchema"
fi

# The concrete reviewed event/EffectResult field corrections are present:
# parallel_opened.activationId, fan_out_opened.nodeId, fan_out_resolved.port,
# approval_decided without notificationRevision/completedAt, loop_broken NOT
# failure-capable, and no invented EffectResult kind/status/retryAt/
# receiptRecovery projection.
if rg -Uq '"parallel_opened": \{context: contextFields\(\n\s*"kind", "activationId", "scopeId"' internal/api/workflow/run/event_effect_schema.go \
  && rg -Uq '"fan_out_opened": \{context: contextFields\(\n\s*"kind", "activationId", "nodeId", "scopeId"' internal/api/workflow/run/event_effect_schema.go \
  && rg -Uq '"fan_out_resolved": \{context: contextFields\(\n\s*"kind", "scopeId", "port"' internal/api/workflow/run/event_effect_schema.go \
  && rg -Uq '"loop_broken": \{context: contextFields\(\n\s*"kind", "activationId", "scopeId"\),\n\s*\}' internal/api/workflow/run/event_effect_schema.go \
  && rg -Uq '"approval_decided": \{context: contextFields\(\n\s*"effectId", "activationId", "nodeId", "notificationId",\n\s*"decision", "decidedByMemberId"' internal/api/workflow/run/event_effect_schema.go; then
  ok "C5b reviewed event field corrections present (parallel_opened.activationId, fan_out_opened.nodeId, fan_out_resolved.port, approval_decided without notificationRevision/completedAt, loop_broken not failure-capable)"
else
  fail "C5b event_effect_schema.go must carry the reviewed field corrections (parallel_opened.activationId, fan_out_opened.nodeId, fan_out_resolved.port, approval_decided without notificationRevision/completedAt, loop_broken not failure-capable)"
fi

# EffectResult non-failure projection matches orchestrator.EffectResult exactly:
# no invented kind/status/retryAt/receiptRecovery, and the authoritative Plan
# port grammar is used for every port projection.
if rg -q 'effectResultNonFailureContext' internal/api/workflow/run/effect_result_projection.go \
  && rg -q 'validPlanPort' internal/api/workflow/run/event_effect_schema.go \
  && ! rg -q '"kind".*eventEnvelopeKindValues.*effectResultNonFailureContext|effectResultNonFailureContext.*"kind"' internal/api/workflow/run/effect_result_projection.go; then
  ok "C5c EffectResult non-failure projection matches orchestrator.EffectResult (no invented kind/status/retryAt/receiptRecovery; Plan port grammar)"
else
  fail "C5c effect_result_projection.go must remove invented kind/status/retryAt/receiptRecovery and use the authoritative Plan port grammar"
fi

# run_canceled typed {reason, actor} with authoritative 2048-byte prose limit and
# human/agent actor + authoritative IDs.
if rg -q 'maxCancelReasonReadBytes' internal/api/workflow/run/event_effect_schema.go \
  && rg -q '"run_canceled"' internal/api/workflow/run/event_effect_schema.go \
  && rg -q 'envelopeFieldActor' internal/api/workflow/run/event_effect_schema.go; then
  ok "C6 run_canceled typed contract exists (2048-byte reason prose + human/agent actor + authoritative IDs)"
else
  fail "C6 run_canceled must be typed {reason, actor} with a 2048-byte prose limit and human/agent actor grammar"
fi

# WorkItem keys use the shared authoritative 256-byte grammar.
if rg -q 'func PublicWorkItemKey' internal/workflow/run/failure_identity.go \
  && rg -q 'TestPublicWorkItemKeyAuthoritative256ByteGrammar' internal/workflow/run/failure_identity_test.go; then
  ok "C7 WorkItem keys share the authoritative 256-byte grammar with max-valid/leading/dot tests"
else
  fail "C7 PublicWorkItemKey must enforce the 256-byte WorkItem grammar and test max-valid + leading/dot invalid cases"
fi

# ---------------------------------------------------------------------------
# Propagation matrix documentation
# ---------------------------------------------------------------------------
if [ -f specs/workflow/error-contract-propagation.md ]; then
  ok "D1 cross-layer propagation matrix documented in specs/workflow/error-contract-propagation.md"
else
  fail "D1 specs/workflow/error-contract-propagation.md must document the cross-layer propagation matrix"
fi

# ---------------------------------------------------------------------------
# Diff quality: no ratchet ceilings raised by this integration.
# ---------------------------------------------------------------------------
if rg -q 'internal-cause-observation.sh' scripts/harness-fast.sh; then
  ok "D2 internal-cause observation wired into fast harness (single added check)"
else
  fail "D2 harness-fast.sh must wire internal-cause-observation.sh"
fi

if (( status > 0 )); then
  echo "round-10 error-contract integration invariant FAILED ($status issue(s))" >&2
  exit 1
fi

echo "round-10 error-contract integration invariant OK"
