#!/usr/bin/env bash
# Structural repository guard for the compiler-owned prompt boundary.
set -euo pipefail

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

fail=0
report() {
  echo "prompt-policy: $*" >&2
  fail=1
}

# Every repository rule below is a named detector. Negative fixtures invoke
# these same functions (with the same rg configuration) so a weakened rule
# fails both repository scanning and its regression fixture.
detector_legacy_launch_prompt_field() {
  rg -n --glob '*.go' --glob '!**/*_test.go' \
    '(^|[[:space:]])(Prompt|FreshPrompt|ResumePrompt|BaseInstructions|DeveloperInstructions|ContractCarrierText)[[:space:]]+string' "$@"
}

detector_runtime_input_text_field() {
  rg -n --glob '*.go' --glob '!**/*_test.go' \
    'type RuntimeInput[[:space:]]+struct[[:space:]]*\{[^}]*\bText[[:space:]]+string' "$@"
}

detector_raw_live_delivery_entrypoint() {
  rg -n --glob '*.go' --glob '!**/*_test.go' \
    'CompiledSlotText|DeliverBusyStdinMessage|RuntimeInput\{[^\n]*Text[[:space:]]*:' "$@"
}

detector_carrier_deserialization() {
  rg -n --glob '*.go' --glob '!**/*_test.go' \
    --glob '!desktop/internal/promptcompiler/prompt_block.go' \
    'func \(.*CompiledPrompt\) UnmarshalJSON|func \(.*CompiledPrompt\) UnmarshalText|json\.Unmarshal' "$@"
}

detector_prompt_block_outside_compiler() {
  rg -n -U --glob '*.go' --glob '!**/*_test.go' --glob '!desktop/internal/promptcompiler/**' \
    'promptcompiler\.PromptBlock\s*\{\s*(ID|Title|text|Text)\s*:' "$@"
}

detector_generated_prompt_block_escape() {
  rg -n -U --glob '*.go' --glob '!**/*_test.go' \
    --glob '!desktop/internal/promptcompiler/prompt_block.go' \
    'CompileGenerated(Turn|Prompt)\s*\([^)]*PromptBlock|NewRegisteredGeneratedBlock|PromptBlock\{[^}]*Text' "$@"
}

detector_generic_template_escape() {
  rg -n --glob '*.go' --glob '!**/*_test.go' \
    'promptTemplateInput|typedPromptInput|promptInputText|validatePromptTemplateData|external_guidance|RuntimeGuidanceTemplateData|newRuntimeGuidancePromptInput' "$@"
}

detector_skill_semantic_passthrough() {
  rg -n --glob '*.go' --glob '!**/*_test.go' \
    'Supplemental[[:space:]]+string|\.Supplemental|SkillDoc(Core|Reference)|memoryskill\.SearchGuidance' "$@" || \
    rg -n '\.Supplemental|Supplemental[[:space:]]+string' "$@"
}

detector_thread_guidance_prose() {
  rg -n --glob '*.go' --glob '!**/*_test.go' \
    'fmt\.Sprintf|strings\.Join|\[\]string\s*\{' "$@"
}

detector_prompt_block_inside_compiler() {
  rg -n -U --glob '*.go' --glob '!**/*_test.go' \
    --glob '!desktop/internal/promptcompiler/registry.go' \
    --glob '!desktop/internal/promptcompiler/prompt_block.go' \
    'PromptBlock\s*\{\s*(ID|Title|Type|Layer|Slot|Durability|Priority|Source|text|TokenEstimate)' "$@"
}

detector_registry_bypass() {
  rg -n --glob '*.go' --glob '!**/*_test.go' \
    'mustBlock\s*\(|renderPromptTemplate\s*\(|prompts\.MustRender' "$@"
}

detector_missing_expected_type() {
  rg -n --glob '*.go' --glob '!**/*_test.go' \
    'PromptBlockSpec\{[^}]*ExpectedType:[[:space:]]*""|ExpectedType:[[:space:]]*""' "$@"
}

detector_attachment_prompt_mutation() {
  rg -n --glob '*.go' --glob '!**/*_test.go' --glob '!desktop/internal/runtimeattachments/**' --glob '!desktop/internal/promptcompiler/**' \
    'runtimeattachments\.(RenderManifest|RenderPreparationFailure|AppendManifest|AppendPreparationFailure)|appendPromptBlock|AppendAttachmentResolution' "$@"
}

detector_adapter_join() {
  local found=1 target
  for target in "$@"; do
    if [[ -f "$target" ]]; then
      [[ "$(basename "$target")" == driver_*.go ]] || continue
      if rg -n --glob '*.go' --glob '!**/*_test.go' \
        'joinClaudePromptSections|strings\.Join\([^\n]*(Prompt|Instructions|Carrier)|fmt\.Sprintf\([^\n]*(Prompt|Instructions|Carrier)' "$target"; then
        found=0
      fi
    elif rg -n --glob 'driver_*.go' --glob '!**/*_test.go' \
      'joinClaudePromptSections|strings\.Join\([^\n]*(Prompt|Instructions|Carrier)|fmt\.Sprintf\([^\n]*(Prompt|Instructions|Carrier)' "$target"; then
      found=0
    fi
  done
  return "$found"
}

detector_adapter_prose() {
  local found=1 target
  for target in "$@"; do
    if [[ -f "$target" ]]; then
      [[ "$(basename "$target")" == driver_*.go ]] || continue
      if rg -n --glob '*.go' --glob '!**/*_test.go' \
        '"(You are |Do not |Before |After |Continue |Reply |Respond |Handle |Summarize the task)' "$target"; then
        found=0
      fi
    elif rg -n --glob 'driver_*.go' --glob '!**/*_test.go' \
      '"(You are |Do not |Before |After |Continue |Reply |Respond |Handle |Summarize the task)' "$target"; then
      found=0
    fi
  done
  return "$found"
}

detector_raw_metadata_interpretation() {
  rg -n --glob '*.go' '(?i)metadata\[|promptStringMapValue\(' "$@"
}

detector_dispatcher_prompt_reassembly() {
  rg -n --glob '*.go' --glob '!**/*_test.go' \
    'joinPromptSections\(|Rendered\.SlotText\(|rendered\.SlotText\(|strings\.Join\([^\n]*(Prompt|Instructions|Carrier)' "$@"
}

detector_guidance_string_channel() {
  rg -n --glob '*.go' --glob '!**/*_test.go' \
    'type (Exec|Extension)CompileInput[[:space:]]+struct[[:space:]]*\{[^}]*Prompt[[:space:]]+string|RuntimeGuidanceTemplateData|newRuntimeGuidancePromptInput' "$@"
}

detector_unread_go_authored_syntax() {
  rg -n --glob 'text_event.go' \
    'UnreadLines|fmt\.Sprintf\([^\n]*unread|strings\.Join\([^\n]*lines' "$@"
}

run_detector() {
  local detector="$1"
  local message="$2"
  shift 2
  if "$detector" "$@"; then
    report "$message"
  fi
}

[[ -f desktop/prompts/INVENTORY.md ]] || report "missing desktop/prompts/INVENTORY.md"

run_detector detector_legacy_launch_prompt_field \
  "drivercontract exposes a legacy mutable launch prompt field" \
  desktop/internal/agent/drivercontract
run_detector detector_runtime_input_text_field \
  "RuntimeInput exposes a mutable semantic Text field" \
  desktop/internal/agent/drivercontract
run_detector detector_raw_live_delivery_entrypoint \
  "production code retains a prompt fallback or raw live-delivery entry point" \
  desktop/internal/agent desktop/internal/dispatcher

if detector_carrier_deserialization desktop/internal/promptcompiler >/dev/null 2>&1; then
  if ! rg -q 'compiled prompt is sealed and cannot be unmarshaled' desktop/internal/promptcompiler/compiled_prompt.go; then
    report "production exposes semantic CompiledPrompt deserialization"
  fi
fi

run_detector detector_prompt_block_outside_compiler \
  "production code constructs PromptBlock outside promptcompiler" desktop
run_detector detector_generated_prompt_block_escape \
  "generated prompt compilation accepts arbitrary PromptBlock prose" desktop
run_detector detector_generic_template_escape \
  "registry retains a generic/pre-rendered or external-guidance escape hatch" \
  desktop/internal/promptcompiler desktop/prompts/INVENTORY.md
run_detector detector_skill_semantic_passthrough \
  "skill guidance retains a Supplemental or pre-rendered semantic channel" \
  desktop/internal/promptcompiler desktop/internal/dispatcher desktop/prompts/wire desktop/prompts/INVENTORY.md
run_detector detector_thread_guidance_prose \
  "threadcontext authors prompt prose outside registered templates" desktop/internal/threadcontext
run_detector detector_prompt_block_inside_compiler \
  "production promptcompiler constructs semantic PromptBlock outside the registry materializer" \
  desktop/internal/promptcompiler
run_detector detector_registry_bypass \
  "production promptcompiler bypasses the executable registry" desktop/internal/promptcompiler
run_detector detector_missing_expected_type \
  "registry entry omits its concrete expected input type" desktop/internal/promptcompiler/registry.go
run_detector detector_attachment_prompt_mutation \
  "production code mutates prompt text outside promptcompiler" desktop
run_detector detector_adapter_join \
  "runtime adapter joins or formats semantic prompt text" desktop/internal/agent
run_detector detector_adapter_prose \
  "runtime adapter owns semantic prompt prose" desktop/internal/agent
run_detector detector_raw_metadata_interpretation \
  "prompt render path interprets raw metadata maps" desktop/internal/promptcompiler/render.go
run_detector detector_dispatcher_prompt_reassembly \
  "dispatcher reassembles or extracts semantic prompt text" desktop/internal/dispatcher
run_detector detector_guidance_string_channel \
  "runtime guidance retains a generic pre-rendered string channel" \
  desktop/internal/promptcompiler desktop/internal/dispatcher
run_detector detector_unread_go_authored_syntax \
  "unread summary syntax is authored in Go instead of its template" \
  desktop/internal/promptcompiler

# The executable registry/template validation is the completeness check for
# duplicate IDs and missing typed constructor/template/slot/priority/durability/budget metadata.
if ! (cd desktop && go test ./internal/promptcompiler -run 'TestPromptBlockRegistryIsComplete|TestPromptMatrixMatchesCheckedInGoldenArtifact' -count=1); then
  report "prompt registry/template/golden validation failed"
fi
if [[ ! -f desktop/internal/promptcompiler/testdata/prompt_matrix.json ]] || ! rg -q 'go:embed testdata/prompt_matrix\.json' desktop/internal/promptcompiler/launch_matrix_golden_test.go; then
  report "prompt golden matrix must use a checked-in artifact"
fi
if rg -n 'expectedGeneratedMatrixMarker|require\.(NotEmpty|Contains)\([^\n]*expected' desktop/internal/promptcompiler/*golden*_test.go; then
  report "golden tests may not be marker-only assertions"
fi

# Durable detector self-test. Each fixture is written as a .go source (or a
# driver_*.go source where repository scoping requires that filename) and is
# checked by the exact detector used above. The final allowed fixture proves
# the shared detectors do not reject the approved typed registry shape.
fixture_dir="$(mktemp -d)"
trap 'rm -rf "$fixture_dir"' EXIT
expect_rejected() {
  local detector="$1"
  local name="$2"
  local filename="$3"
  local body="$4"
  local path="$fixture_dir/$filename"
  printf '%b\n' "$body" >"$path"
  if ! "$detector" "$path" >/dev/null; then
    report "negative fixture failed to detect $name"
  fi
}
expect_allowed() {
  local filename="$1"
  local body="$2"
  local path="$fixture_dir/$filename"
  printf '%b\n' "$body" >"$path"
  local detector
  for detector in \
    detector_legacy_launch_prompt_field detector_runtime_input_text_field \
    detector_raw_live_delivery_entrypoint detector_carrier_deserialization \
    detector_prompt_block_outside_compiler detector_generated_prompt_block_escape \
    detector_generic_template_escape detector_skill_semantic_passthrough \
    detector_thread_guidance_prose detector_prompt_block_inside_compiler \
    detector_registry_bypass detector_attachment_prompt_mutation detector_adapter_join \
    detector_adapter_prose detector_raw_metadata_interpretation detector_dispatcher_prompt_reassembly \
    detector_guidance_string_channel detector_unread_go_authored_syntax; do
    if "$detector" "$path" >/dev/null; then
      report "allowed typed registry fixture was rejected by $detector"
    fi
  done
}

expect_rejected detector_legacy_launch_prompt_field "legacy LaunchConfig field" fixture.go \
'type LaunchConfig struct { Prompt string }'
expect_rejected detector_runtime_input_text_field "RuntimeInput.Text" fixture.go \
'type RuntimeInput struct { Text string }'
expect_rejected detector_raw_live_delivery_entrypoint "raw live-delivery entrypoint" fixture.go 'var _ = CompiledSlotText'
expect_rejected detector_carrier_deserialization "carrier deserialization" fixture.go 'func forged() { var p CompiledPrompt; _ = json.Unmarshal(data, &p) }'
expect_rejected detector_raw_metadata_interpretation "prompt metadata map parsing" fixture.go 'var metadata map[string]any; _ = metadata["task"]'
expect_rejected detector_prompt_block_outside_compiler "direct PromptBlock construction" fixture.go 'var forged = promptcompiler.PromptBlock{ID: "event.current_message", text: "FORGED REGISTERED PROMPT"}'
expect_rejected detector_generated_prompt_block_escape "generated PromptBlock prose" fixture.go 'func bypass() { CompileGeneratedTurn(PromptBlock{Text: "prose"}) }'
expect_rejected detector_generic_template_escape "generic template input" fixture.go 'type promptTemplateInput struct { data any }'
expect_rejected detector_prompt_block_inside_compiler "semantic PromptBlock construction" fixture.go 'var forged = PromptBlock{ID: "event.current_message", text: "FORGED REGISTERED PROMPT"}'
expect_rejected detector_registry_bypass "caller-owned block admission" fixture.go 'func bypass() { mustBlock(desc, block) }'
expect_rejected detector_registry_bypass "template registry bypass" fixture.go 'func bypass() { return renderPromptTemplate("events/current_message.md", data) }'
expect_rejected detector_adapter_join "adapter join" driver_fixture.go 'func bypass() { return strings.Join(PromptParts, "\\n") }'
expect_rejected detector_adapter_prose "adapter prose" driver_fixture.go 'var _ = "You are an agent"'
expect_rejected detector_guidance_string_channel "runtime guidance string channel" fixture.go 'type RuntimeGuidanceTemplateData struct { Content string }'
expect_rejected detector_skill_semantic_passthrough "Supplemental skill channel" fixture.go 'type SkillPlan struct { Supplemental string }'
expect_rejected detector_thread_guidance_prose "Go-authored thread guidance" threadcontext_fixture.go 'func GuidanceText() string { return fmt.Sprintf("Thread context: %s", target) }'
expect_rejected detector_unread_go_authored_syntax "Go-authored unread syntax" text_event.go 'var _ = fmt.Sprintf("- %s: %d unread", ch, count)'
expect_allowed allowed.go 'func allowed(input promptcompiler.ExecGuidanceTemplateData) { _ = input; _ = registry.Construct("platform.agentteam.exec", input) }'

if [[ "$fail" -ne 0 ]]; then
  exit 1
fi
echo "prompt-policy: structural checks passed"
