#!/usr/bin/env bash
# Upload project, governance, release, and requirement evidence for one
# Stage 3 workflow invocation. Kept outside the workflow so GitHub Actions'
# 21 KB expression-template limit cannot make the workflow undispatchable.

set -uo pipefail

: "${DEVAUDIT_PROJECT_SLUG:?DEVAUDIT_PROJECT_SLUG is required}"
: "${DEVAUDIT_GIT_SHA:?DEVAUDIT_GIT_SHA is required}"
: "${DEVAUDIT_CI_RUN_ID:?DEVAUDIT_CI_RUN_ID is required}"
: "${DEVAUDIT_BRANCH:?DEVAUDIT_BRANCH is required}"
: "${DEVAUDIT_RELEASE_VERSION:?DEVAUDIT_RELEASE_VERSION is required}"
: "${DEVAUDIT_BASE_URL:?DEVAUDIT_BASE_URL is required}"
: "${DEVAUDIT_API_KEY:?DEVAUDIT_API_KEY is required}"

chmod +x scripts/upload-evidence.sh 2>/dev/null || true
# Common flags WITHOUT --release. Each upload appends its OWN
# --release so per-requirement artifacts land in their own release
# record (version = REQ-ID) instead of all collapsing into whichever
# single version the triggering commit derived. DevAudit #135 follow-
# up: this fixes release *attribution* (not just duplication) — an
# untagged docs commit must not sweep every in-scope REQ into a date
# release. Note: upload-evidence.sh keeps the LAST --release seen.
FLAGS="--git-sha ${DEVAUDIT_GIT_SHA} --ci-run-id ${DEVAUDIT_CI_RUN_ID} --branch ${DEVAUDIT_BRANCH}"
FLAGS="${FLAGS} --create-release-if-missing --environment uat --sdlc-stage 3"
DERIVED_RELEASE="${DEVAUDIT_RELEASE_VERSION}"

# devaudit-installer#621 — flags for documents that are genuinely
# project-level and must NOT be attached to any one release (see
# upload_project_level_doc below). --environment/--sdlc-stage require
# --release server-side (evidence without a release is "orphaned" by
# design for exactly these documents), so all three release-scoping
# flags are simply omitted here rather than passed and then overridden.
PROJECT_LEVEL_FLAGS="--git-sha ${DEVAUDIT_GIT_SHA} --ci-run-id ${DEVAUDIT_CI_RUN_ID} --branch ${DEVAUDIT_BRANCH}"

# Derive change_type for the bare-date (housekeeping) DERIVED_RELEASE:
# the prefix of the most recent commit's subject. No-op for tracked
# releases — they get per-REQ derivation in the loop below.
DERIVED_CT=$(git log -1 --pretty=%s 2>/dev/null \
  | grep -oE '^(feat|fix|refactor|perf|chore|docs|ci|build|test|compliance|revert)' \
  | head -1 || true)
DERIVED_META=()
[ -n "$DERIVED_CT" ] && DERIVED_META+=(--change-type "$DERIVED_CT")

# Upload project-level planning docs with specific evidence_type
# so the portal's framework-coverage predicates attribute them
# correctly (DevAudit-Installer#205). Previously all three uploaded
# as compliance_document — the catch-all that satisfies no specific
# clause predicate.
#
#   - RTM.md        → rtm         (ISO 29119 §3.3 traceability)
#   - test-plan.md  → test_plan   (ISO 29119 §3.4 test plan)
#   - test-cases.md → test_cases  (ISO 29119 §3.4 test cases)
#
# RTM.md is genuinely per-release evidence — it's kept current per REQ
# by sdlc-implementer and the portal's release-completeness check reads
# it per release — so it stays release-scoped via upload_project_doc.
upload_project_doc() {
  local DOC="$1" EVTYPE="$2"
  if [ ! -f "$DOC" ]; then return 0; fi
  echo "Uploading: $(basename "$DOC") (${EVTYPE})"
  bash scripts/upload-evidence.sh \
    ${DEVAUDIT_PROJECT_SLUG} _compliance-docs "$EVTYPE" "$DOC" \
    --category planning ${FLAGS} --release "${DERIVED_RELEASE}" \
    "${DERIVED_META[@]}" \
    || echo "Warning: Failed to upload $(basename "$DOC")"
}
upload_project_doc compliance/RTM.md rtm

# devaudit-installer#621 — test-plan.md and test-cases.md, unlike
# RTM.md above, are project-level test-strategy documents, not
# per-release evidence: they were previously attached to
# "${DERIVED_RELEASE}" via upload_project_doc just like RTM.md, so the
# portal displayed whatever these files happened to say (often stale by
# months) as if it were current evidence for every release — most
# visibly on bare-date housekeeping releases, which have no REQ-scoped
# test-scope.md/test-plan.md to show instead. upload_project_level_doc
# omits --release (and the flags that require it) so these upload as
# genuine Documents-tab artefacts, decoupled from any one release.
upload_project_level_doc() {
  local DOC="$1" EVTYPE="$2"
  if [ ! -f "$DOC" ]; then return 0; fi
  echo "Uploading: $(basename "$DOC") (${EVTYPE} — project-level, no release attachment)"
  bash scripts/upload-evidence.sh \
    ${DEVAUDIT_PROJECT_SLUG} _compliance-docs "$EVTYPE" "$DOC" \
    --category planning ${PROJECT_LEVEL_FLAGS} \
    || echo "Warning: Failed to upload $(basename "$DOC")"
}
upload_project_level_doc compliance/test-plan.md   test_plan
upload_project_level_doc compliance/test-cases.md  test_cases

# Project-level Test Summary Report — a hand-authored baseline
# describing the project's testing posture. As of v0.1.32 this is
# uploaded as `compliance_document` (NOT `test_report`) — the
# per-release Test Reports gate is now satisfied by the per-REQ
# `test-execution-summary.md` uploaded in the in-scope-requirements
# loop below, which carries fresh release-specific data. The
# project-level TSR continues to ship as a Documents-tab baseline
# but no longer poses as per-release test evidence.
# See DevAudit-Installer#101.
#
# devaudit-installer#621 — the comment above was aspirational, not
# actual: this call still attached --release "${DERIVED_RELEASE}", so
# every release displayed this static file as if it were current
# per-release test evidence. Uses upload_project_level_doc now, same
# fix as test-plan.md/test-cases.md above.
upload_project_level_doc compliance/test-summary-report.md compliance_document

# NOTE: test-summary-report.md stays as compliance_document because
# it is a project-level evergreen baseline, NOT per-release test
# evidence. It does not satisfy any specific clause predicate —
# the per-REQ test-execution-summary.md (test_report) does that.
# This is the one intentional compliance_document use for project-
# level docs (DevAudit-Installer#205).

# Tier 3 per-event governance docs (devaudit#370 Phase 3a, narrowed
# in v0.1.39). Only periodic-review and incident-report are CI-uploaded
# — both are auto-generated by other workflows (periodic-review by the
# quarterly cron, incident-report by the incident-export workflow).
#
# Tier 1/2 governance docs (Test_Policy, AGENT, INSTRUCTIONS,
# Test_Strategy, Test_Architecture, Periodic_Security_Review_Schedule,
# ROPA, DPIA, AI Disclosure) are operator-uploaded via the portal
# Upload Evidence form. CI auto-upload was removed because placeholder
# starter content was landing as canonical evidence; the portal form
# ensures the operator reviews each refresh before it counts.
upload_governance() {
  local FILE="$1" TYPE="$2"
  if [ ! -f "$FILE" ]; then return 0; fi
  echo "Uploading governance: $(basename "$FILE") (type=${TYPE})"
  bash scripts/upload-evidence.sh \
    ${DEVAUDIT_PROJECT_SLUG} _compliance-docs "$TYPE" "$FILE" \
    --category planning ${FLAGS} --release "${DERIVED_RELEASE}" \
    "${DERIVED_META[@]}" \
    || echo "Warning: Failed to upload $(basename "$FILE")"
}
# Recognise governance docs at top-level OR under compliance/governance/
# (operator's choice — both layouts are common).
upload_governance compliance/periodic-review.md             periodic_review
upload_governance compliance/governance/periodic-review.md  periodic_review
frontmatter_value() {
  local FILE="$1" KEY="$2"
  awk -v key="$KEY" '
    NR == 1 && $0 == "---" { in_fm=1; next }
    in_fm && $0 == "---" { exit }
    in_fm && $0 ~ "^[[:space:]]*" key ":" {
      sub("^[[:space:]]*" key ":[[:space:]]*", "")
      gsub(/^["'\'' ]+|["'\'' ]+$/, "")
      print
      exit
    }
  ' "$FILE" 2>/dev/null || true
}

bundle_manifest_allows_source_release() {
  local SOURCE_RELEASE="$1" MANIFEST=""
  [ -n "$SOURCE_RELEASE" ] || return 1
  for MANIFEST in \
    "compliance/pending-releases/BUNDLED-CHANGES-${DERIVED_RELEASE}.json" \
    "compliance/approved-releases/BUNDLED-CHANGES-${DERIVED_RELEASE}.json" \
    "compliance/superseded-releases/BUNDLED-CHANGES-${DERIVED_RELEASE}.json"; do
    [ -f "$MANIFEST" ] || continue
    if jq -e --arg source "$SOURCE_RELEASE" '
      any(.members[]?;
        .version == $source
        and (
          .evidenceInheritancePolicy.mode == "all_eligible"
          or any(.evidenceInheritancePolicy.scopes[]?; . == "release" or . == "stage" or . == "approval")
        )
      )
    ' "$MANIFEST" >/dev/null 2>&1; then
      return 0
    fi
  done
  return 1
}

# Incident reports are selected by frontmatter ownership, not by
# repository-wide glob attribution. Historical files may stay on disk,
# but only the incident/no-incident artefact whose source release is
# the current derived release is uploaded to that release. If the
# current release has an explicit bundle manifest that grants
# inheritance for a predecessor, upload the file to the SOURCE release
# so the portal can inherit by lineage without relabelling ownership.
upload_incident_report() {
  local FILE="$1"
  if [ ! -f "$FILE" ]; then return 0; fi
  local KIND SOURCE_RELEASE RELEASE_SCOPE SOURCE_ISSUE SEMANTIC_ID CONTENT_HASH TARGET_RELEASE
  KIND="$(frontmatter_value "$FILE" incident_kind)"
  SOURCE_RELEASE="$(frontmatter_value "$FILE" source_release)"
  RELEASE_SCOPE="$(frontmatter_value "$FILE" release_scope)"
  SOURCE_ISSUE="$(frontmatter_value "$FILE" source_issue)"
  SEMANTIC_ID="$(frontmatter_value "$FILE" semantic_id)"
  [ -n "$SEMANTIC_ID" ] || SEMANTIC_ID="$(frontmatter_value "$FILE" incident_id)"

  case "$KIND" in
    incident|no_incidents_attestation) ;;
    "")
      echo "::error::$(basename "$FILE"): missing required frontmatter incident_kind. Expected incident or no_incidents_attestation."
      return 1 ;;
    *)
      echo "::error::$(basename "$FILE"): invalid incident_kind '${KIND}'. Expected incident or no_incidents_attestation."
      return 1 ;;
  esac
  if [ "$KIND" = "no_incidents_attestation" ] && [ -z "$SOURCE_RELEASE" ]; then
    echo "::error::$(basename "$FILE"): nil incident reports require source_release frontmatter."
    return 1
  fi
  if [ "$KIND" = "incident" ]; then
    if [ -z "$SOURCE_RELEASE" ] && [ -z "$RELEASE_SCOPE" ]; then
      echo "::error::$(basename "$FILE"): incident reports require source_release or release_scope frontmatter."
      return 1
    fi
    if [ -z "$SOURCE_ISSUE" ]; then
      echo "::error::$(basename "$FILE"): incident reports require source_issue frontmatter."
      return 1
    fi
  fi
  if [ -z "$SEMANTIC_ID" ]; then
    echo "::error::$(basename "$FILE"): incident reports require semantic_id or incident_id frontmatter."
    return 1
  fi

  TARGET_RELEASE="${SOURCE_RELEASE:-$RELEASE_SCOPE}"
  if [ "$TARGET_RELEASE" != "$DERIVED_RELEASE" ]; then
    if bundle_manifest_allows_source_release "$TARGET_RELEASE"; then
      echo "Incident $(basename "$FILE") is inherited by ${DERIVED_RELEASE}; preserving source release ${TARGET_RELEASE}."
    else
      echo "::notice::Skipping $(basename "$FILE"): source release ${TARGET_RELEASE} is not ${DERIVED_RELEASE} and no explicit bundle manifest grants inheritance."
      return 0
    fi
  fi

  # Completeness gate — check for REPLACE markers in structured sections.
  # §4 Root cause, §5 Impact, §9 Sign-off (populated reports).
  # For nil reports, only check the Sign-off section.
  if grep -q '## 4\. Root cause' "$FILE" 2>/dev/null; then
    SECTION=$(sed -n '/^## 4\. Root cause/,/^## [0-9]/p' "$FILE")
    if echo "$SECTION" | grep -q 'REPLACE'; then
      echo "::warning::$(basename "$FILE"): §4 (Root cause) contains REPLACE markers — skipping upload. Complete the section and push to develop."
      return 0
    fi
  fi
  if grep -q '## 5\. Impact' "$FILE" 2>/dev/null; then
    SECTION=$(sed -n '/^## 5\. Impact/,/^## [0-9]/p' "$FILE")
    if echo "$SECTION" | grep -q 'REPLACE'; then
      echo "::warning::$(basename "$FILE"): §5 (Impact) contains REPLACE markers — skipping upload. Complete the section and push to develop."
      return 0
    fi
  fi
  if grep -q '## 9\. Sign-off' "$FILE" 2>/dev/null; then
    SECTION=$(sed -n '/^## 9\. Sign-off/,/^## /p' "$FILE")
    if echo "$SECTION" | grep -q 'REPLACE'; then
      echo "::warning::$(basename "$FILE"): §9 (Sign-off) contains REPLACE markers — skipping upload. Complete the sign-off and push to develop."
      return 0
    fi
  fi
  # Nil reports have a simpler sign-off section — check it too.
  if grep -q '## Sign-off' "$FILE" 2>/dev/null && ! grep -q '## 9\. Sign-off' "$FILE" 2>/dev/null; then
    SECTION=$(sed -n '/^## Sign-off/,/^## /p' "$FILE")
    if echo "$SECTION" | grep -q 'REPLACE'; then
      echo "::warning::$(basename "$FILE"): Sign-off section contains REPLACE markers — skipping upload. Complete the sign-off and push to develop."
      return 0
    fi
  fi
  CONTENT_HASH="sha256:$(sha256sum "$FILE" | awk '{print $1}')"
  echo "Uploading governance: $(basename "$FILE") (type=incident_report, source_release=${TARGET_RELEASE})"
  bash scripts/upload-evidence.sh \
    ${DEVAUDIT_PROJECT_SLUG} _compliance-docs incident_report "$FILE" \
    --category planning ${FLAGS} --release "${TARGET_RELEASE}" \
    --evidence-scope release \
    --meta-key "incident_kind=${KIND}" \
    --meta-key "source_release=${TARGET_RELEASE}" \
    --meta-key "source_issue=${SOURCE_ISSUE:-none}" \
    --meta-key "semantic_id=${SEMANTIC_ID}" \
    --meta-key "content_hash=${CONTENT_HASH}" \
    "${DERIVED_META[@]}" \
    || echo "Warning: Failed to upload $(basename "$FILE")"
}
shopt -s nullglob
for f in compliance/incident-report*.md compliance/governance/incident-report*.md; do
  upload_incident_report "$f"
done
# DevAudit-Installer#210 §8a: nil incident reports (per-release
# "no incidents" attestations). Glob alongside populated reports
# and upload as incident_report evidence.
for f in compliance/nil-incident-report*.md compliance/governance/nil-incident-report*.md; do
  upload_incident_report "$f"
done
shopt -u nullglob

# ── Audit-log export (DevAudit-Installer#98 WS2) ──────────────
# Snapshot the portal's audit log for the rolling 90-day window
# and upload as `evidence_type=audit_log`. Closes three
# framework-coverage clauses on every release:
#   - ISO27001.A.8.16 — Monitoring activities
#   - EUAIA.Art-12   — Record-keeping (automatic logging)
#   - GDPR.Art-32    — Security of processing (audit-log half)
#
# The portal endpoint defaults to the last 90 days when no
# `since`/`until` query params are passed; omit them so the
# consumer side stays zero-config. Endpoint shipped in
# META-COMPLY PR #413; project-scoped API key (uploader role)
# already has read access via `resolveCiUploadAuth`.
AUDIT_LOG_FILE="$(mktemp -t audit-log-XXXXXX.json)"
if curl -sSf -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
  "${DEVAUDIT_BASE_URL%/}/api/ci/projects/${DEVAUDIT_PROJECT_SLUG}/audit-log/export" \
  -o "$AUDIT_LOG_FILE"; then
  echo "Uploading: audit-log.json (audit_log — 90-day window)"
  bash scripts/upload-evidence.sh \
    ${DEVAUDIT_PROJECT_SLUG} _compliance-docs audit_log "$AUDIT_LOG_FILE" \
    --category audit_log ${FLAGS} --release "${DERIVED_RELEASE}" \
    "${DERIVED_META[@]}" \
    || echo "Warning: Failed to upload audit-log.json"
else
  # Soft-fail: an export hiccup shouldn't break the rest of the
  # evidence pipeline. Surfaces as a warning in the workflow log;
  # the framework-coverage panel will show MISSING for the three
  # clauses above until the next successful upload.
  echo "::warning::Audit-log export failed — endpoint unreachable or 4xx/5xx. Three framework-coverage clauses (ISO27001.A.8.16, EUAIA.Art-12, GDPR.Art-32 audit-log half) will stay MISSING until the next run."
fi
rm -f "$AUDIT_LOG_FILE"

# Helper: emit `--release-title …` `--release-summary …` `--change-type …`
# args for a given REQ, derived from its release-ticket metadata and
# the most recent commit attributed to that REQ. Empty pair when
# neither is available. Uses the shared extraction helper so both
# workflow paths produce the same title/summary for the same REQ.
# DevAudit-Installer#285.
req_meta_args() {
  local REQ="$1"; local CT=""
  # Source the shared helper and extract canonical title + summary
  source scripts/extract-release-metadata.sh 2>/dev/null || true
  extract_release_metadata "$REQ" 2>/dev/null || true
  CT=$(git log --grep "\[${REQ}\]\|Ref: ${REQ}" --pretty=%s -1 2>/dev/null \
    | grep -oE '^(feat|fix|refactor|perf|chore|docs|ci|build|test|compliance|revert)' \
    | head -1 || true)
  # Use if/then/fi (not `[ … ] && cmd`) — under bash -eo pipefail a
  # trailing `[ … ] && cmd` whose test fails returns 1, and the
  # function inherits that exit code; calling via $(req_meta_args …)
  # then aborts the step silently (DevAudit-Installer#77).
  if [ -n "$RELEASE_TITLE" ]; then printf -- '--release-title %q ' "$RELEASE_TITLE"; fi
  if [ -n "$RELEASE_SUMMARY" ]; then printf -- '--release-summary %q ' "$RELEASE_SUMMARY"; fi
  if [ -n "$CT" ]; then printf -- '--change-type %q ' "$CT"; fi
}

# Upload root-level housekeeping security summaries:
#   compliance/security-summary-vYYYY.MM.DD.md
#   compliance/security-summary-vYYYY.MM.DD.N.md
#
# These are generated by the housekeeping stub path and belong to
# the bare-date housekeeping release encoded in the filename, not
# to whichever release the current commit happened to derive.
shopt -s nullglob
for SECSUM in compliance/security-summary-*.md; do
  [ -f "$SECSUM" ] || continue
  SECSUM_VER=$(basename "$SECSUM" .md | sed 's/^security-summary-//')
  if ! echo "$SECSUM_VER" | grep -qE '^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?$'; then
    echo "::warning::Skipping $(basename "$SECSUM"): filename does not encode a housekeeping release version."
    continue
  fi
  echo "Uploading housekeeping security summary: $(basename "$SECSUM") -> release ${SECSUM_VER}"
  bash scripts/upload-evidence.sh \
    ${DEVAUDIT_PROJECT_SLUG} _compliance-docs security_summary "$SECSUM" \
    --category security ${FLAGS} --release "${SECSUM_VER}" \
    "${DERIVED_META[@]}" \
    || echo "Warning: Failed to upload $(basename "$SECSUM")"
done
shopt -u nullglob

# Upload release tickets (pending only)
if [ -d "compliance/pending-releases" ]; then
  for TICKET in compliance/pending-releases/*.md; do
    [ -f "$TICKET" ] || continue
    # A RELEASE-TICKET-REQ-XXX.md belongs to that requirement's
    # release record; any other ticket rides the derived release.
    TICKET_BASE=$(basename "$TICKET" .md)
    case "$TICKET_BASE" in
      RELEASE-TICKET-REQ-*)
        TICKET_REQ="${TICKET_BASE#RELEASE-TICKET-}"
        TICKET_OWNER="$TICKET_REQ"; TICKET_RELEASE="$TICKET_REQ"
        TICKET_META_ARGS=$(req_meta_args "$TICKET_REQ") ;;
      *)
        TICKET_OWNER="_compliance-docs"; TICKET_RELEASE="$DERIVED_RELEASE"
        TICKET_META_ARGS="" ;;
    esac
    echo "Uploading: $(basename "$TICKET") -> release ${TICKET_RELEASE}"
    eval "bash scripts/upload-evidence.sh \
      ${DEVAUDIT_PROJECT_SLUG} \"${TICKET_OWNER}\" release_ticket \"$TICKET\" \
      --category release_artifact ${FLAGS} --release \"${TICKET_RELEASE}\" \
      ${TICKET_META_ARGS}" \
      || echo "Warning: Failed to upload $(basename "$TICKET")"
  done
fi

# Upload per-requirement evidence — scoped to requirements with a
# pending release ticket. Without this scoping every historical
# compliance/evidence/REQ-*/ folder would be re-uploaded on every
# run, re-populating the release-requirement matrix with the full
# project catalogue (DevAudit #135, sibling of #133).
IN_SCOPE_REQS=()
if [ -d compliance/pending-releases ]; then
  for TICKET in compliance/pending-releases/RELEASE-TICKET-REQ-*.md; do
    [ -f "$TICKET" ] || continue
    REQ_ID=$(basename "$TICKET" .md | sed 's/^RELEASE-TICKET-//')
    # devaudit-installer#192 — skip REQs already in a terminal
    # release directory. See ci.yml.template for full rationale.
    if compgen -G "compliance/approved-releases/RELEASE-TICKET-${REQ_ID}.md" > /dev/null 2>&1 \
       || compgen -G "compliance/superseded-releases/RELEASE-TICKET-${REQ_ID}.md" > /dev/null 2>&1; then
      echo "Skipping ${REQ_ID}: already in a terminal release directory (stale pending ticket)"
      continue
    fi
    IN_SCOPE_REQS+=("$REQ_ID")
  done
fi

if [ ${#IN_SCOPE_REQS[@]} -eq 0 ]; then
  echo "No pending release tickets found — skipping per-requirement evidence upload"
else
  echo "In-scope requirements for this release: ${IN_SCOPE_REQS[*]}"
  for REQ_ID in "${IN_SCOPE_REQS[@]}"; do
    REQ_DIR="compliance/evidence/${REQ_ID}/"
    if [ ! -d "$REQ_DIR" ]; then
      echo "Warning: pending ticket for ${REQ_ID} but no ${REQ_DIR} on disk"
      continue
    fi
    REQ_META_ARGS=$(req_meta_args "$REQ_ID")
    for ARTIFACT in "$REQ_DIR"*.md; do
      [ -f "$ARTIFACT" ] || continue
      # Per-REQ basename → (evidence_type, evidence_category) routing.
      # Every SDLC-expected artifact has an explicit case with a
      # dedicated evidence_type so the portal's framework-coverage
      # predicates attribute them to the correct clause. The `*)`
      # catch-all was removed (DevAudit-Installer#205) — unrecognized
      # filenames are now skipped with a warning instead of being
      # silently uploaded as compliance_document/planning, which
      # caused wrong artifacts to satisfy wrong framework clauses.
      #
      # Evidence type → framework clause mapping:
      #
      #   - test-execution-summary.md → test_report
      #       ISO 29119-3 §3.5.6 Test Completion Report.
      #       DevAudit-Installer#101.
      #
      #   - srs-alignment.md → srs_alignment
      #       Output of requirements-aligner skill at Stage 3.
      #       DevAudit-Installer#119.
      #
      #   - architecture-decision.md → architecture_decision
      #       ISO 27001 A.8.25 (Secure SDLC).
      #       DevAudit-Installer#120.
      #
      #   - risk-assessment.md → risk_assessment
      #       SOC 2 CC3.2 (Risk identification).
      #       DevAudit-Installer#121.
      #
      #   - e2e-scope-decision.md → e2e_scope_decision
      #       Records the e2e-test-engineer invoke/skip decision so the
      #       portal's per-REQ "No E2E screenshots" advisory can tell a
      #       deliberate no-e2e call apart from a genuine coverage gap.
      #       DevAudit-Installer#737.
      #
      #   - test-scope.md → test_scope
      #       ISO 29119 §3.3 (Test scope).
      #
      #   - test-plan.md → test_plan
      #       ISO 29119 §3.4 (Test plan, per-REQ).
      #
      #   - implementation-plan.md → implementation_plan
      #       ISO 27001 A.8.25 (Secure SDLC planning).
      #
      #   - security-summary.md → security_summary
      #       ISO 27001 A.8.28 / SOC2 CC6.1.
      #
      #   - ai-use-note.md → ai_use_note
      #       EUAIA Art. 13 (Transparency).
      #
      #   - ai-prompts.md → ai_prompt_log
      #       EUAIA Art. 12 (Record-keeping).
      #
      #   - ai-agent-handoff.md → ai_agent_handoff
      #       EUAIA Art. 14 (Human oversight).
      #
      # DevAudit-Installer#146, #205.
      BASENAME=$(basename "$ARTIFACT")
      case "$BASENAME" in
        test-execution-summary.md|test-summary-report.md)
          EVTYPE=test_report; EVCAT=test_report ;;
        srs-alignment.md)
          EVTYPE=srs_alignment; EVCAT=planning ;;
        architecture-decision.md)
          EVTYPE=architecture_decision; EVCAT=planning ;;
        risk-assessment.md)
          EVTYPE=risk_assessment; EVCAT=planning ;;
        e2e-scope-decision.md)
          EVTYPE=e2e_scope_decision; EVCAT=planning ;;
        test-scope.md)
          EVTYPE=test_scope; EVCAT=planning ;;
        test-plan.md)
          EVTYPE=test_plan; EVCAT=planning ;;
        implementation-plan.md)
          EVTYPE=implementation_plan; EVCAT=planning ;;
        security-summary.md)
          EVTYPE=security_summary; EVCAT=security ;;
        ai-use-note.md)
          EVTYPE=ai_use_note; EVCAT=ai_governance ;;
        ai-prompts.md)
          EVTYPE=ai_prompt_log; EVCAT=ai_governance ;;
        ai-agent-handoff.md)
          EVTYPE=ai_agent_handoff; EVCAT=ai_governance ;;
        *)
          echo "::warning::Unrecognized artifact ${REQ_ID}/${BASENAME} — skipping upload. Add explicit routing in compliance-evidence.yml if this is a new evidence type. (DevAudit-Installer#205)"
          continue ;;
      esac
      echo "Uploading: ${REQ_ID}/${BASENAME} (${EVTYPE})"
      eval "bash scripts/upload-evidence.sh \
        ${DEVAUDIT_PROJECT_SLUG} \"${REQ_ID}\" ${EVTYPE} \"$ARTIFACT\" \
        --category ${EVCAT} ${FLAGS} --release \"${REQ_ID}\" \
        ${REQ_META_ARGS}" \
        || echo "Warning: Failed to upload ${BASENAME}"
    done
  done
fi
