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

MODE="${1:-}"
ROOT="${2:-}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FIXTURE_DIR="$SCRIPT_DIR/fixtures/lot3"
MARKER='.soria-ir-triage-investigation-evidence'
DECISION='INVESTIGATION_READY_WITH_ACTIONS'

fail(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; }
need(){ command -v "$1" >/dev/null 2>&1 || fail "missing command: $1"; }

safe_root(){
  [[ -n "$ROOT" ]] || return 1
  case "$ROOT" in
    /tmp/soria-ir-*|"$HOME"/soria-ir-*) return 0 ;;
    *) return 1 ;;
  esac
}
require_safe_root(){ safe_root || fail "unsafe lab root: ${ROOT:-<empty>}"; }
require_marker(){ [[ -f "$ROOT/$MARKER" ]] || fail "missing marker: $ROOT/$MARKER"; }
fixture_count(){ find "$1" -type f | wc -l | tr -d ' '; }
valid_timestamp(){ [[ "$1" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; }

build_manifest(){
  local tree="$1" out="$2"
  ( cd "$tree"; find . -type f -print | LC_ALL=C sort | while IFS= read -r file; do sha256sum "$file"; done ) > "$out"
}
verify_source_integrity(){
  [[ -f "$ROOT/.source-manifest.sha256" ]] || return 1
  ( cd "$ROOT/source"; sha256sum -c "$ROOT/.source-manifest.sha256" >/dev/null 2>&1 )
}
known_asset(){ grep -q "^${1}," "$ROOT/source/context/assets.csv"; }
known_identity(){ [[ "$1" == "-" ]] || grep -q "^${1}," "$ROOT/source/context/identities.csv"; }
known_alert(){ grep -q "^${1}," "$ROOT/source/alerts/alerts.csv"; }
asset_criticality(){ awk -F, -v id="$1" 'NR>1 && $1==id {print $3; found=1; exit} END{if(!found) exit 1}' "$ROOT/source/context/assets.csv"; }
all_event_files(){ printf '%s\n' "$ROOT/source/events/host-events.csv" "$ROOT/source/events/network-events.csv" "$ROOT/source/events/application-events.csv"; }
source_row_exists(){
  local ref="$1" rel id
  rel="${ref%%#*}"; id="${ref#*#}"
  [[ "$rel" != "$ref" && -f "$ROOT/source/$rel" ]] || return 1
  awk -F, -v id="$id" 'NR>1 && $1==id {found=1} END{exit(found?0:1)}' "$ROOT/source/$rel"
}
known_evidence(){ grep -q "^${1}," "$ROOT/source/evidence/collection-register.csv"; }

validate_alert_line(){
  local line="$1" alert_id timestamp rule_id asset_id identity_id severity confidence business_impact summary synthetic
  IFS=, read -r alert_id timestamp rule_id asset_id identity_id severity confidence business_impact summary synthetic <<< "$line"
  [[ -n "$alert_id" && -n "$rule_id" && -n "$summary" ]] || return 1
  valid_timestamp "$timestamp" || return 1
  known_asset "$asset_id" || return 1
  known_identity "$identity_id" || return 1
  [[ "$severity" =~ ^(low|medium|high|critical)$ ]] || return 1
  [[ "$confidence" =~ ^[0-9]+$ ]] || return 1
  (( confidence >= 0 && confidence <= 100 )) || return 1
  [[ "$business_impact" =~ ^(low|medium|high)$ ]] || return 1
  [[ "$synthetic" == SYNTHETIC ]] || return 1
}
validate_event_line(){
  local line="$1" event_id timestamp alert_id source_type asset_id identity_id event_kind outcome detail synthetic
  IFS=, read -r event_id timestamp alert_id source_type asset_id identity_id event_kind outcome detail synthetic <<< "$line"
  [[ -n "$event_id" && -n "$source_type" && -n "$event_kind" && -n "$outcome" && -n "$detail" ]] || return 1
  valid_timestamp "$timestamp" || return 1
  known_alert "$alert_id" || return 1
  known_asset "$asset_id" || return 1
  known_identity "$identity_id" || return 1
  [[ "$synthetic" == SYNTHETIC ]] || return 1
}
validate_priority(){ [[ "$1" =~ ^P[1-4]$ ]]; }

setup_lab(){
  require_safe_root
  [[ ! -e "$ROOT" ]] || fail "root already exists: $ROOT"
  [[ -d "$FIXTURE_DIR" ]] || fail "missing fixture directory: $FIXTURE_DIR"
  [[ "$(fixture_count "$FIXTURE_DIR")" -eq 16 ]] || fail 'reference fixture bundle must contain exactly 16 files'
  mkdir -p "$ROOT/source" "$ROOT/output" "$ROOT/work"
  printf 'SORIA_IR_TRIAGE_INVESTIGATION_EVIDENCE\n' > "$ROOT/$MARKER"
  cp -R "$FIXTURE_DIR"/. "$ROOT/source/"
  [[ "$(fixture_count "$ROOT/source")" -eq 16 ]] || fail 'copied fixture count mismatch'
  build_manifest "$ROOT/source" "$ROOT/.source-manifest.sha256"
  verify_source_integrity || fail 'source integrity failed immediately after setup'
  printf 'Setup OK: 16 synthetic investigation fixtures copied and integrity manifest created.\n'
}
status_lab(){
  require_safe_root; require_marker
  local count integrity outputs decision
  count="$(fixture_count "$ROOT/source")"
  if verify_source_integrity; then integrity=OK; else integrity=FAILED; fi
  if [[ -s "$ROOT/output/case-summary.md" ]]; then outputs=PRESENT; else outputs=ABSENT; fi
  if [[ -s "$ROOT/output/decision.txt" ]]; then decision="$(cat "$ROOT/output/decision.txt")"; else decision=NOT_RUN; fi
  printf 'marker=OK\nfixtures=%s\nintegrity=%s\noutputs=%s\ndecision=%s\n' "$count" "$integrity" "$outputs" "$decision"
}

write_triage(){
  local out="$ROOT/output/alert-triage.csv"
  printf 'alert_id,asset_criticality,confidence,business_impact,priority,triage_decision,reason,synthetic\n' > "$out"
  tail -n +2 "$ROOT/source/alerts/alerts.csv" | while IFS= read -r line; do
    validate_alert_line "$line" || fail "invalid positive alert line: $line"
    local alert_id timestamp rule_id asset_id identity_id severity confidence business_impact summary synthetic criticality priority decision reason
    IFS=, read -r alert_id timestamp rule_id asset_id identity_id severity confidence business_impact summary synthetic <<< "$line"
    criticality="$(asset_criticality "$asset_id")"
    if [[ "$criticality" == high && "$severity" == high ]] && (( confidence >= 80 )); then priority=P1; decision=INVESTIGATE_IMMEDIATE; reason=high-criticality-high-confidence
    elif [[ "$criticality" == high ]] && (( confidence >= 60 )); then priority=P2; decision=INVESTIGATE_PROMPT; reason=high-criticality-context-required
    elif (( confidence >= 40 )); then priority=P3; decision=CONTEXT_REVIEW; reason=moderate-confidence-context
    else priority=P4; decision=MONITOR; reason=low-confidence-monitor; fi
    printf '%s,%s,%s,%s,%s,%s,%s,%s\n' "$alert_id" "$criticality" "$confidence" "$business_impact" "$priority" "$decision" "$reason" "$synthetic" >> "$out"
  done
}
write_correlation_and_timeline(){
  local corr="$ROOT/output/correlation.csv" combined="$ROOT/work/all-events.csv" timeline="$ROOT/output/timeline.csv"
  printf 'alert_id,event_id,timestamp,asset_id,identity_id,source_type,source_ref,relation,synthetic\n' > "$corr"; : > "$combined"
  all_event_files | while IFS= read -r file; do tail -n +2 "$file"; done > "$combined"
  local duplicates; duplicates="$(awk -F, '{c[$1]++} END{for(id in c) if(c[id]>1) print id}' "$combined")"
  [[ -z "$duplicates" ]] || fail "duplicate positive event IDs: $duplicates"
  while IFS= read -r line; do
    [[ -n "$line" ]] || continue; validate_event_line "$line" || fail "invalid positive event line: $line"
    local event_id timestamp alert_id source_type asset_id identity_id event_kind outcome detail synthetic rel
    IFS=, read -r event_id timestamp alert_id source_type asset_id identity_id event_kind outcome detail synthetic <<< "$line"
    case "$source_type" in windows-security|linux-auth) rel="events/host-events.csv#$event_id" ;; suricata-eve) rel="events/network-events.csv#$event_id" ;; application) rel="events/application-events.csv#$event_id" ;; *) fail "unknown source type: $source_type" ;; esac
    source_row_exists "$rel" || fail "source row missing for $event_id"
    printf '%s,%s,%s,%s,%s,%s,%s,%s,%s\n' "$alert_id" "$event_id" "$timestamp" "$asset_id" "$identity_id" "$source_type" "$rel" RELATED "$synthetic" >> "$corr"
  done < "$combined"
  printf 'event_id,timestamp,alert_id,source_type,asset_id,identity_id,event_kind,outcome,detail,synthetic\n' > "$timeline"
  LC_ALL=C sort -t, -k2,2 -k1,1 "$combined" >> "$timeline"
}
evidence_links_for(){ local hyp="$1" relation="$2"; awk -F, -v h="$hyp" -v r="$relation" 'NR>1 && $1==h && $3==r {if(out!="") out=out"|"; out=out $2} END{print out}' "$ROOT/source/hypotheses/evidence-links.csv"; }
evidence_count_for(){ local hyp="$1" relation="$2"; awk -F, -v h="$hyp" -v r="$relation" 'NR>1 && $1==h && $3==r {c++} END{print c+0}' "$ROOT/source/hypotheses/evidence-links.csv"; }
write_hypotheses(){
  local out="$ROOT/output/hypothesis-status.csv"; printf 'hypothesis_id,claim,result,supporting_evidence,contradicting_evidence,missing_evidence,reason,synthetic\n' > "$out"
  tail -n +2 "$ROOT/source/hypotheses/hypotheses.csv" | while IFS=, read -r id claim min_support requires_refute missing expected synthetic; do
    local support contradict support_ids contradict_ids result reason
    support="$(evidence_count_for "$id" SUPPORT)"; contradict="$(evidence_count_for "$id" CONTRADICT)"; support_ids="$(evidence_links_for "$id" SUPPORT)"; contradict_ids="$(evidence_links_for "$id" CONTRADICT)"
    while IFS=, read -r lh evid relation required syn; do [[ "$lh" == "$id" ]] || continue; known_evidence "$evid" || fail "hypothesis $id references unknown evidence $evid"; done < "$ROOT/source/hypotheses/evidence-links.csv"
    if [[ "$requires_refute" == yes && "$contradict" -gt 0 ]]; then result=REFUTED; reason=explicit-contradicting-evidence
    elif [[ "$missing" != "-" ]]; then result=INCONCLUSIVE; reason="missing-$missing"
    elif (( support >= min_support )); then result=SUPPORTED; reason=minimum-supporting-evidence-met
    else result=INCONCLUSIVE; reason=insufficient-support; fi
    [[ "$result" == "$expected" ]] || fail "hypothesis result drift for $id"
    printf '%s,%s,%s,%s,%s,%s,%s,%s\n' "$id" "$claim" "$result" "${support_ids:--}" "${contradict_ids:--}" "$missing" "$reason" "$synthetic" >> "$out"
  done
}
write_evidence_register(){
  local out="$ROOT/output/evidence-register.csv"; printf 'evidence_id,source_ref,collected_at,collector,purpose,sha256,status,synthetic\n' > "$out"
  tail -n +2 "$ROOT/source/evidence/collection-register.csv" | while IFS=, read -r eid ref collected collector purpose synthetic; do
    source_row_exists "$ref" || fail "evidence source row missing: $ref"; local rel hash; rel="${ref%%#*}"; hash="$(sha256sum "$ROOT/source/$rel" | awk '{print $1}')"; printf '%s,%s,%s,%s,%s,%s,PRESERVED,%s\n' "$eid" "$ref" "$collected" "$collector" "$purpose" "$hash" "$synthetic" >> "$out"
  done
  local item eid hash
  for item in correlation.csv timeline.csv hypothesis-status.csv; do case "$item" in correlation.csv) eid=DERIVED-CORRELATION ;; timeline.csv) eid=DERIVED-TIMELINE ;; hypothesis-status.csv) eid=DERIVED-HYPOTHESES ;; esac; hash="$(sha256sum "$ROOT/output/$item" | awk '{print $1}')"; printf '%s,output/%s,2026-08-08T08:30:00Z,lab-engine,derived-investigation-artifact,%s,DERIVED,SYNTHETIC\n' "$eid" "$item" "$hash" >> "$out"; done
}
write_case_artifacts(){
  cp "$ROOT/source/case/tasks.csv" "$ROOT/output/task-report.csv"; cp "$ROOT/source/case/observables.csv" "$ROOT/output/observable-report.csv"
  cat > "$ROOT/output/decision-log.csv" <<'EOF_DEC'
decision_id,timestamp,owner,decision,basis,status,synthetic
DEC-001,2026-08-08T08:31:00Z,secops,escalate-investigation,AL-001-P1,DECIDED,SYNTHETIC
DEC-002,2026-08-08T08:32:00Z,secops,containment-decision,requires-Lot4-authorisation,DEFERRED,SYNTHETIC
EOF_DEC
  cat > "$ROOT/output/case-summary.md" <<'EOF_SUM'
# CASE-IR-003 — SYNTHETIC investigation summary

## Scope
SORIA Distribution represented identity, host, network and application evidence.

## Triage
AL-001 is P1, AL-002 is P2 and AL-003 remains contextual P3.

## Confirmed facts
The frozen source corpus contains represented authentication failures and success on AST-DC-01, represented Suricata network activity on AST-WEB-01, and represented application requests.

## Hypotheses
HYP-001 and HYP-004 are SUPPORTED by the frozen evidence links.
HYP-002 is REFUTED by explicit contradicting service-context evidence.
HYP-003 is INCONCLUSIVE because endpoint-edr evidence is missing.

## Timeline
The derived timeline is sorted by represented UTC timestamps. The application source is marked UNTRUSTED with represented 120-second uncertainty, so strict cross-source ordering involving application events is not independently established.

## Evidence and provenance
Source fixtures remain preserved under the SHA-256 source manifest. Correlation, timeline and hypothesis outputs are DERIVED artifacts and do not replace originals.

## Open questions
Endpoint EDR evidence is absent. Real log completeness, identity authenticity and endpoint state are not established.

## Decisions
The case is escalated for investigation. Containment is explicitly DEFERRED to the next Lot and is not executed here.

## Proof boundaries
This synthetic lab does not establish a real incident, legal chain of custody, forensic acquisition, admissibility, malware execution, containment, eradication or recovery.
EOF_SUM
  cat > "$ROOT/output/proof-boundaries.md" <<'EOF_BOUND'
# SYNTHETIC Lot 3 proof boundaries

This lab proves deterministic triage, correlation, hypothesis handling, SHA-256 integrity checks and case documentation over a synthetic local corpus only.

It does not prove a real incident, real asset or identity state, log completeness, forensic acquisition soundness, legal chain of custody, admissibility, TheHive deployment, malware analysis, memory or disk forensics, containment, eradication or recovery.

Missing telemetry is preserved as uncertainty and is never treated as proof of absence.
EOF_BOUND
}
record_negative(){ printf '%s,%s,SYNTHETIC\n' "$1" "$2" >> "$ROOT/output/negative-results.csv"; }
run_negative_tests(){
  local out="$ROOT/output/negative-results.csv" neg="$ROOT/work/negative"; rm -rf "$neg"; mkdir -p "$neg"; printf 'family,status,synthetic\n' > "$out"; local line
  line='NEG-A1,2026-08-08T09:00:00Z,DET-X,AST-NOPE,ID-ADMIN,high,90,high,unknown-asset,SYNTHETIC'; if validate_alert_line "$line"; then fail 'negative unknown asset passed'; else record_negative alert-unknown-asset BLOCKED; fi
  line='NEG-A2,2026-08-08T09:00:00Z,DET-X,AST-DC-01,ID-ADMIN,high,101,high,bad-confidence,SYNTHETIC'; if validate_alert_line "$line"; then fail 'negative confidence passed'; else record_negative triage-confidence-out-of-range BLOCKED; fi
  if validate_priority P9; then fail 'negative priority passed'; else record_negative invalid-triage-priority BLOCKED; fi
  line='NEG-E1,2026-08-08T09:00:00Z,AL-NOPE,windows-security,AST-DC-01,ID-ADMIN,auth_failure,FAILURE,unknown-alert,SYNTHETIC'; if validate_event_line "$line"; then fail 'negative unknown alert event passed'; else record_negative correlation-unknown-source-row BLOCKED; fi
  printf '%s\n%s\n' 'NEG-DUP,2026-08-08T09:00:00Z,AL-001,windows-security,AST-DC-01,ID-ADMIN,auth_failure,FAILURE,dup,SYNTHETIC' 'NEG-DUP,2026-08-08T09:00:01Z,AL-001,windows-security,AST-DC-01,ID-ADMIN,auth_failure,FAILURE,dup,SYNTHETIC' > "$neg/duplicate-events.csv"
  if awk -F, '{c[$1]++} END{bad=0; for(id in c) if(c[id]>1) bad=1; exit(bad?0:1)}' "$neg/duplicate-events.csv"; then record_negative duplicate-event-id BLOCKED; else fail 'duplicate event not detected'; fi
  line='NEG-E2,2026/08/08-09:00:00,AL-001,windows-security,AST-DC-01,ID-ADMIN,auth_failure,FAILURE,bad-ts,SYNTHETIC'; if validate_event_line "$line"; then fail 'negative malformed timestamp passed'; else record_negative malformed-event-timestamp BLOCKED; fi
  cp -R "$ROOT/source" "$neg/tampered-source"; printf '\nSYNTHETIC-TAMPER\n' >> "$neg/tampered-source/events/application-events.csv"; if (cd "$neg/tampered-source" && sha256sum -c "$ROOT/.source-manifest.sha256" >/dev/null 2>&1); then fail 'negative SHA tamper passed'; else record_negative source-sha256-mismatch BLOCKED; fi
  if source_row_exists 'events/no-such.csv#EV-X'; then fail 'negative unknown evidence source passed'; else record_negative evidence-unknown-source-artifact BLOCKED; fi
  if known_evidence EVID-NOPE; then fail 'negative unknown evidence id passed'; else record_negative hypothesis-unknown-evidence-id BLOCKED; fi
  if [[ -n "$(evidence_links_for HYP-001 CONTRADICT)" ]]; then fail 'unexpected contradiction in HYP-001'; else record_negative refuted-without-contradiction BLOCKED; fi
  if [[ "$(awk -F, '$1=="HYP-003"{print $5}' "$ROOT/source/hypotheses/hypotheses.csv")" != "-" ]]; then record_negative supported-with-mandatory-evidence-missing BLOCKED; else fail 'reference missing evidence unexpectedly absent'; fi
  local bad_decision='DEC-X,2026-08-08T09:00:00Z,,containment-executed,none,EXECUTED,SYNTHETIC' did dts downer ddecision dbasis dstatus dsyn; IFS=, read -r did dts downer ddecision dbasis dstatus dsyn <<< "$bad_decision"; if [[ -z "$downer" || ( "$ddecision" == *containment* && "$dstatus" == EXECUTED ) ]]; then record_negative decision-missing-owner-or-containment-executed BLOCKED; else fail 'negative decision unexpectedly accepted'; fi
}
run_lab(){
  require_safe_root; require_marker; need awk; need sort; need sha256sum; need grep; need find; need cut; need cmp
  [[ "$(fixture_count "$ROOT/source")" -eq 16 ]] || fail 'source fixture count must remain 16'; verify_source_integrity || fail 'source integrity mismatch before run'
  rm -rf "$ROOT/output" "$ROOT/work"; mkdir -p "$ROOT/output" "$ROOT/work"; cp "$ROOT/.source-manifest.sha256" "$ROOT/output/source-integrity.sha256"
  write_triage; write_correlation_and_timeline; write_hypotheses; write_evidence_register; write_case_artifacts; run_negative_tests; printf '%s\n' "$DECISION" > "$ROOT/output/decision.txt"
  printf 'Run OK: triage, correlation, timeline, hypotheses and evidence dossier generated.\n'
}
validate_lab(){
  require_safe_root; require_marker; [[ "$(fixture_count "$ROOT/source")" -eq 16 ]] || fail 'expected exactly 16 source fixtures'; verify_source_integrity || fail 'source integrity mismatch'
  local required=(alert-triage.csv correlation.csv timeline.csv hypothesis-status.csv evidence-register.csv decision-log.csv task-report.csv observable-report.csv source-integrity.sha256 case-summary.md negative-results.csv proof-boundaries.md decision.txt) f
  for f in "${required[@]}"; do [[ -s "$ROOT/output/$f" ]] || fail "missing output: $f"; done
  [[ $(( $(wc -l < "$ROOT/output/alert-triage.csv") - 1 )) -eq 3 ]] || fail 'expected exactly three triaged alerts'; grep -Fq 'AL-001,high,92,high,P1,INVESTIGATE_IMMEDIATE' "$ROOT/output/alert-triage.csv" || fail 'missing P1 triage'; grep -Fq 'AL-002,high,74,medium,P2,INVESTIGATE_PROMPT' "$ROOT/output/alert-triage.csv" || fail 'missing P2 triage'
  local event_count corr_count; event_count="$(all_event_files | while IFS= read -r ef; do tail -n +2 "$ef"; done | wc -l | tr -d ' ')"; corr_count=$(( $(wc -l < "$ROOT/output/correlation.csv") - 1 )); [[ "$event_count" -eq "$corr_count" ]] || fail 'correlation row count mismatch'
  tail -n +2 "$ROOT/output/correlation.csv" | while IFS=, read -r aid eid ts asset ident stype sref relation syn; do known_alert "$aid" || exit 1; known_asset "$asset" || exit 1; known_identity "$ident" || exit 1; source_row_exists "$sref" || exit 1; [[ "$relation" == RELATED && "$syn" == SYNTHETIC ]] || exit 1; done || fail 'correlation traceability failure'
  { printf 'event_id,timestamp,alert_id,source_type,asset_id,identity_id,event_kind,outcome,detail,synthetic\n'; all_event_files | while IFS= read -r ef; do tail -n +2 "$ef"; done | LC_ALL=C sort -t, -k2,2 -k1,1; } > "$ROOT/work/expected-timeline.csv"; cmp -s "$ROOT/work/expected-timeline.csv" "$ROOT/output/timeline.csv" || fail 'timeline is not deterministic'
  grep -Fq 'application,UNTRUSTED,120' "$ROOT/source/timeline/clock-notes.csv" || fail 'untrusted clock fixture missing'; grep -Fq 'application source is marked UNTRUSTED' "$ROOT/output/case-summary.md" || fail 'clock limitation not surfaced'
  [[ $(( $(wc -l < "$ROOT/output/hypothesis-status.csv") - 1 )) -eq 4 ]] || fail 'expected four hypothesis results'; grep -q ',SUPPORTED,' "$ROOT/output/hypothesis-status.csv" || fail 'SUPPORTED missing'; grep -q ',REFUTED,' "$ROOT/output/hypothesis-status.csv" || fail 'REFUTED missing'; grep -q ',INCONCLUSIVE,' "$ROOT/output/hypothesis-status.csv" || fail 'INCONCLUSIVE missing'; grep -Fq 'HYP-003,web-data-exfiltration,INCONCLUSIVE' "$ROOT/output/hypothesis-status.csv" || fail 'endpoint evidence hypothesis must remain inconclusive'; grep -Fq 'endpoint-edr' "$ROOT/output/hypothesis-status.csv" || fail 'endpoint-edr gap missing'
  awk -F, 'NR>1 && $7=="PRESERVED" {if(length($6)!=64) bad=1; c++} END{exit(c>0 && !bad ? 0 : 1)}' "$ROOT/output/evidence-register.csv" || fail 'preserved evidence hashes invalid'; grep -q ',DERIVED,SYNTHETIC$' "$ROOT/output/evidence-register.csv" || fail 'derived evidence missing'
  grep -Fq '## Confirmed facts' "$ROOT/output/case-summary.md" || fail 'facts section missing'; grep -Fq '## Hypotheses' "$ROOT/output/case-summary.md" || fail 'hypothesis section missing'; grep -Fq '## Open questions' "$ROOT/output/case-summary.md" || fail 'open questions missing'; grep -Fq 'Containment is explicitly DEFERRED' "$ROOT/output/case-summary.md" || fail 'deferred containment missing'; ! grep -Eq ',containment[^,]*,[^,]*,EXECUTED,' "$ROOT/output/decision-log.csv" || fail 'containment cannot be executed in Lot 3'
  [[ $(( $(wc -l < "$ROOT/output/negative-results.csv") - 1 )) -eq 12 ]] || fail 'expected exactly twelve negative families'; [[ "$(grep -c ',BLOCKED,SYNTHETIC$' "$ROOT/output/negative-results.csv")" -eq 12 ]] || fail 'all negative families must be BLOCKED'; [[ "$(cat "$ROOT/output/decision.txt")" == "$DECISION" ]] || fail 'unexpected reference decision'; grep -Fq 'does not prove a real incident' "$ROOT/output/proof-boundaries.md" || fail 'real-incident boundary missing'; grep -Fq 'legal chain of custody' "$ROOT/output/proof-boundaries.md" || fail 'legal boundary missing'
  printf 'Validate OK: 16 fixtures, 3 alerts, 4 hypotheses and 12 blocked negatives. Decision=%s\n' "$DECISION"
}
reset_lab(){ require_safe_root; require_marker; rm -rf -- "$ROOT"; [[ ! -e "$ROOT" ]] || fail 'reset failed'; printf 'Reset OK: lab root removed.\n'; }
case "$MODE" in setup) setup_lab ;; status) status_lab ;; run) run_lab ;; validate) validate_lab ;; reset) reset_lab ;; *) fail 'usage: module3-triage-investigation-evidence.sh {setup|status|run|validate|reset} ROOT' ;; esac
