Files
ngit-grasp/docs/how-to/migration-scripts/40-classify-actions.sh
T
DanConwayDev ef50f4bd97 Fix parse failure output to show repo|npub instead of event_id|kind
Phase 4 (30-extract-parse-failures.sh) now enriches parse failures with
repo name and npub by looking up event_id in announcements.json. This is
critical because 'Invalid announcement' rejections only log event_id and
kind, not the repo name or npub.

Phase 5 (40-classify-actions.sh) was also fixed to extract columns 4 and 5
(repo|npub) instead of columns 1 and 2 (event_id|kind) from parse-failures.txt.

Without this fix, action-required.txt showed unusable output like:
  000014b2... | 30617 | parse failure logged | fix event format...

Now it correctly shows:
  scripts | npub1hs5244... | parse failure logged | fix event format...

The enrichment uses jq to build a lookup table from announcements.json and
optionally uses 'nak' to convert hex pubkeys to npub format.
2026-01-27 20:38:09 +00:00

773 lines
29 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# 40-classify-actions.sh - Final classification of repos for migration action
#
# PHASE 5 of the GRASP relay to ngit-grasp migration analysis pipeline.
# Combines all data sources from previous phases to produce actionable results.
#
# USAGE:
# ./40-classify-actions.sh <analysis-dir>
#
# EXAMPLES:
# ./40-classify-actions.sh work/migration-analysis-20260122-1430
#
# INPUT DIRECTORY STRUCTURE:
# <analysis-dir>/
# ├── prod/
# │ ├── raw/
# │ │ └── deletions.json # Phase 1: kind 5 deletion events
# │ ├── category1-complete-match.txt # Phase 3: complete git sync
# │ ├── category2-empty-blank.txt # Phase 3: no git data
# │ ├── category3-partial-match.txt # Phase 3: partial git sync
# │ └── category4-no-match.txt # Phase 3: git exists, refs don't match
# ├── archive/
# │ ├── raw/
# │ │ └── deletions.json
# │ ├── category1-complete-match.txt
# │ ├── category2-empty-blank.txt
# │ ├── category3-partial-match.txt
# │ └── category4-no-match.txt
# ├── comparison/
# │ ├── complete-in-both.txt # Phase 3: no action needed
# │ ├── complete-prod-missing-archive.txt # Phase 3: needs investigation
# │ ├── complete-prod-incomplete-archive.txt # Phase 3: sync in progress?
# │ ├── incomplete-in-both.txt # Phase 3: git incomplete
# │ └── in-archive-not-prod.txt # Phase 3: deleted or new
# └── logs/
# ├── parse-failures.txt # Phase 4: events that failed to parse
# └── purgatory-expired.txt # Phase 4: repos that expired from purgatory
#
# OUTPUT:
# <analysis-dir>/results/
# ├── no-action-required.txt # Repos that are fine as-is
# ├── action-required.txt # Repos needing intervention
# ├── manual-investigation.txt # Repos needing human review
# └── summary.txt # Human-readable summary
#
# OUTPUT FORMATS:
# no-action-required.txt:
# repo | npub | reason
#
# action-required.txt:
# repo | npub | reason | suggested_action
#
# manual-investigation.txt:
# repo | npub | reason | context
#
# CLASSIFICATION LOGIC:
#
# NO ACTION REQUIRED:
# - Complete in both prod and archive (successfully migrated)
# - Empty/blank in both (user never pushed any data)
# - Deleted by user (kind 5 deletion event exists)
# - In purgatory expiry logs (system already handled it)
#
# ACTION REQUIRED:
# - Complete in prod, missing from archive → Re-sync needed
# - Complete in prod, incomplete in archive → Wait for sync or re-trigger
# - Partial match in prod → Investigate why refs don't match
# - No match (category 4) → Investigate git data corruption
# - Parse failures → Fix event format or re-announce
#
# MANUAL INVESTIGATION:
# - Conflicting states (e.g., complete in prod but parse failure logged)
# - In archive but not prod (deleted? or new announcement?)
# - Multiple issues for same repo
# - Unexpected state combinations
#
# PREREQUISITES:
# - jq (for parsing JSON)
# - awk, sort, comm (standard Unix tools)
#
# RUNTIME: < 5 seconds (local processing only)
#
# SEE ALSO:
# docs/how-to/migrate-to-ngit-grasp.md - Full migration guide
# 01-fetch-events.sh - Phase 1 (fetch events)
# 10-check-git-sync.sh - Phase 2 (git sync check)
# 20-categorize.sh, 21-compare-relays.sh - Phase 3 (categorize and compare)
# 30-extract-parse-failures.sh, 31-extract-purgatory-expiry.sh - Phase 4 (logs)
#
set -euo pipefail
# Colors for output (disabled if not a terminal)
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'
else
RED=''
GREEN=''
YELLOW=''
BLUE=''
BOLD=''
NC=''
fi
log_info() {
echo -e "${BLUE}[INFO]${NC} $*" >&2
}
log_success() {
echo -e "${GREEN}[OK]${NC} $*" >&2
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $*" >&2
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*" >&2
}
usage() {
echo "Usage: $0 <analysis-dir>"
echo ""
echo "Arguments:"
echo " analysis-dir Directory containing Phase 1-4 output"
echo ""
echo "Examples:"
echo " $0 work/migration-analysis-20260122-1430"
echo ""
echo "Required input structure:"
echo " <analysis-dir>/prod/category*.txt"
echo " <analysis-dir>/archive/category*.txt"
echo " <analysis-dir>/comparison/*.txt"
echo " <analysis-dir>/logs/*.txt (optional)"
echo " <analysis-dir>/prod/raw/deletions.json"
echo ""
echo "Output:"
echo " <analysis-dir>/results/no-action-required.txt"
echo " <analysis-dir>/results/action-required.txt"
echo " <analysis-dir>/results/manual-investigation.txt"
echo " <analysis-dir>/results/summary.txt"
exit 1
}
# Extract repo|npub key from category line
# Input: "repo | npub | state_refs=N | ..."
# Output: "repo|npub"
extract_key() {
awk -F' \\| ' '{print $1 "|" $2}'
}
# Extract repo from category line
# Input: "repo | npub | ..."
# Output: "repo"
extract_repo() {
awk -F' \\| ' '{print $1}'
}
# Extract npub from category line
# Input: "repo | npub | ..."
# Output: "npub"
extract_npub() {
awk -F' \\| ' '{print $2}'
}
# Check if a file exists and has content (ignoring comment lines)
file_has_content() {
local file="$1"
if [[ ! -f "$file" ]]; then
return 1
fi
# Check for non-comment, non-empty lines
grep -v '^#' "$file" 2>/dev/null | grep -q '.' 2>/dev/null
}
# Count non-comment lines in a file
count_lines() {
local file="$1"
if [[ ! -f "$file" ]]; then
echo "0"
return
fi
local count
count=$(grep -v '^#' "$file" 2>/dev/null | grep -c '.' 2>/dev/null) || count=0
# Ensure we return a clean integer
echo "${count:-0}"
}
# Parse deletions.json to extract deleted repo identifiers
# Kind 5 events have "e" tags pointing to the deleted event
# We need to cross-reference with announcements to get repo/npub
# For now, we extract the pubkey and any "a" tags (addressable event references)
parse_deletions() {
local deletions_file="$1"
local output_file="$2"
if [[ ! -f "$deletions_file" ]]; then
touch "$output_file"
return
fi
# Extract deletion targets from kind 5 events
# Kind 5 events can reference:
# - "e" tag: specific event ID
# - "a" tag: addressable event (kind:pubkey:identifier)
# For 30617 announcements, "a" tag format is: 30617:<pubkey>:<repo-identifier>
jq -r '
select(.kind == 5) |
.pubkey as $pubkey |
.tags[] |
select(.[0] == "a") |
.[1] |
split(":") |
select(.[0] == "30617") |
"\(.[2])|\($pubkey)"
' "$deletions_file" 2>/dev/null | sort -u > "$output_file" || touch "$output_file"
}
# Build a lookup set from a file (repo|npub format)
# Returns keys one per line
build_key_set() {
local file="$1"
if [[ ! -f "$file" ]]; then
return 0
fi
# Use || true to prevent pipefail from exiting on empty grep
{ grep -v '^#' "$file" 2>/dev/null || true; } | extract_key | sort -u
}
# Main classification logic
main() {
if [[ $# -ne 1 ]]; then
usage
fi
local analysis_dir="$1"
# Validate input directory
if [[ ! -d "$analysis_dir" ]]; then
log_error "Analysis directory not found: $analysis_dir"
exit 1
fi
# Check for required subdirectories
local prod_dir="$analysis_dir/prod"
local archive_dir="$analysis_dir/archive"
local comparison_dir="$analysis_dir/comparison"
local logs_dir="$analysis_dir/logs"
local results_dir="$analysis_dir/results"
for dir in "$prod_dir" "$archive_dir" "$comparison_dir"; do
if [[ ! -d "$dir" ]]; then
log_error "Required directory not found: $dir"
log_error "Run Phases 1-3 first to generate input data."
exit 1
fi
done
# Check for required category files
if [[ ! -f "$prod_dir/category1-complete-match.txt" ]]; then
log_error "Missing category files in $prod_dir"
log_error "Run Phase 3 (20-categorize.sh) first."
exit 1
fi
log_info "Starting final classification"
log_info "Analysis directory: $analysis_dir"
# Create output directory
mkdir -p "$results_dir"
# Create temp directory for intermediate files
local tmp_dir
tmp_dir=$(mktemp -d)
# shellcheck disable=SC2064
trap "rm -rf '$tmp_dir'" EXIT
# Initialize output files
local no_action="$results_dir/no-action-required.txt"
local action_req="$results_dir/action-required.txt"
local manual_inv="$results_dir/manual-investigation.txt"
local summary="$results_dir/summary.txt"
# Write headers
{
echo "# No Action Required - Repos that are fine as-is"
echo "# Generated: $(date -Iseconds)"
echo "# Format: repo | npub | reason"
echo "#"
} > "$no_action"
{
echo "# Action Required - Repos needing intervention"
echo "# Generated: $(date -Iseconds)"
echo "# Format: repo | npub | reason | suggested_action"
echo "#"
} > "$action_req"
{
echo "# Manual Investigation Required - Repos needing human review"
echo "# Generated: $(date -Iseconds)"
echo "# Format: repo | npub | reason | context"
echo "#"
} > "$manual_inv"
# =========================================================================
# STEP 1: Parse deletion events
# =========================================================================
log_info "Parsing deletion events..."
parse_deletions "$prod_dir/raw/deletions.json" "$tmp_dir/prod_deletions.txt"
parse_deletions "$archive_dir/raw/deletions.json" "$tmp_dir/archive_deletions.txt"
# Combine deletions (union of both)
cat "$tmp_dir/prod_deletions.txt" "$tmp_dir/archive_deletions.txt" 2>/dev/null | sort -u > "$tmp_dir/all_deletions.txt"
local deletion_count
deletion_count=$(wc -l < "$tmp_dir/all_deletions.txt" | tr -d ' ')
log_info "Found $deletion_count deletion requests"
# =========================================================================
# STEP 2: Parse log-based categories (Phase 4)
# =========================================================================
log_info "Parsing log-based categories..."
# Parse failures: event_id<TAB>kind<TAB>reason<TAB>repo<TAB>npub
# Note: repo and npub are in columns 4 and 5 (enriched by Phase 4 from announcements.json)
# Some entries may have empty repo/npub if the event_id wasn't found in announcements
if [[ -f "$logs_dir/parse-failures.txt" ]] && file_has_content "$logs_dir/parse-failures.txt"; then
grep -v '^#' "$logs_dir/parse-failures.txt" | awk -F'\t' '{print $4 "|" $5}' | sort -u > "$tmp_dir/parse_failures.txt"
log_info "Found $(wc -l < "$tmp_dir/parse_failures.txt" | tr -d ' ') parse failure entries"
else
touch "$tmp_dir/parse_failures.txt"
log_info "No parse failures found (logs may be empty or not yet generated)"
fi
# Purgatory expired: repo<TAB>npub<TAB>timestamp<TAB>reason
if [[ -f "$logs_dir/purgatory-expired.txt" ]] && file_has_content "$logs_dir/purgatory-expired.txt"; then
grep -v '^#' "$logs_dir/purgatory-expired.txt" | awk -F'\t' '{print $1 "|" $2}' | sort -u > "$tmp_dir/purgatory_expired.txt"
log_info "Found $(wc -l < "$tmp_dir/purgatory_expired.txt" | tr -d ' ') purgatory expiry entries"
else
touch "$tmp_dir/purgatory_expired.txt"
log_info "No purgatory expiry entries found (logs may be empty or not yet generated)"
fi
# =========================================================================
# STEP 3: Build lookup tables from category files
# =========================================================================
log_info "Building lookup tables..."
# Build key sets for each category (prod)
build_key_set "$prod_dir/category1-complete-match.txt" > "$tmp_dir/prod_cat1.txt"
build_key_set "$prod_dir/category2-empty-blank.txt" > "$tmp_dir/prod_cat2.txt"
build_key_set "$prod_dir/category3-partial-match.txt" > "$tmp_dir/prod_cat3.txt"
build_key_set "$prod_dir/category4-no-match.txt" > "$tmp_dir/prod_cat4.txt"
# Build key sets for each category (archive)
build_key_set "$archive_dir/category1-complete-match.txt" > "$tmp_dir/archive_cat1.txt"
build_key_set "$archive_dir/category2-empty-blank.txt" > "$tmp_dir/archive_cat2.txt"
build_key_set "$archive_dir/category3-partial-match.txt" > "$tmp_dir/archive_cat3.txt"
build_key_set "$archive_dir/category4-no-match.txt" > "$tmp_dir/archive_cat4.txt"
# All repos in prod
cat "$tmp_dir"/prod_cat*.txt 2>/dev/null | sort -u > "$tmp_dir/all_prod.txt" || true
# All repos in archive
cat "$tmp_dir"/archive_cat*.txt 2>/dev/null | sort -u > "$tmp_dir/all_archive.txt" || true
# =========================================================================
# STEP 4: Process comparison files and apply classification
# =========================================================================
log_info "Applying classification logic..."
# Track processed repos to detect duplicates/conflicts
> "$tmp_dir/processed.txt"
# Counters
local count_no_action=0
local count_action=0
local count_manual=0
# --- NO ACTION: Complete in both ---
if [[ -f "$comparison_dir/complete-in-both.txt" ]]; then
while IFS= read -r line; do
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
repo=$(echo "$line" | extract_repo)
npub=$(echo "$line" | extract_npub)
key="${repo}|${npub}"
# Check if deleted (still no action, but different reason)
if grep -qF "$key" "$tmp_dir/all_deletions.txt" 2>/dev/null; then
echo "$repo | $npub | deleted by user (also complete in both)" >> "$no_action"
else
echo "$repo | $npub | complete in both prod and archive" >> "$no_action"
fi
echo "$key" >> "$tmp_dir/processed.txt"
((count_no_action++)) || true
done < "$comparison_dir/complete-in-both.txt"
fi
# --- NO ACTION: Deleted by user (not already processed) ---
while IFS='|' read -r repo npub; do
[[ -z "$repo" ]] && continue
key="${repo}|${npub}"
# Skip if already processed
if grep -qF "$key" "$tmp_dir/processed.txt" 2>/dev/null; then
continue
fi
# Convert pubkey to npub if needed (deletions use hex pubkey)
# For now, just use the pubkey as-is since we're matching by repo
echo "$repo | $npub | deleted by user" >> "$no_action"
echo "$key" >> "$tmp_dir/processed.txt"
((count_no_action++)) || true
done < "$tmp_dir/all_deletions.txt"
# --- NO ACTION: Empty/blank in both ---
# Find repos that are category 2 in both prod and archive
comm -12 "$tmp_dir/prod_cat2.txt" "$tmp_dir/archive_cat2.txt" 2>/dev/null | while IFS='|' read -r repo npub; do
[[ -z "$repo" ]] && continue
key="${repo}|${npub}"
if grep -qF "$key" "$tmp_dir/processed.txt" 2>/dev/null; then
continue
fi
echo "$repo | $npub | empty/blank in both (user never pushed)" >> "$no_action"
echo "$key" >> "$tmp_dir/processed.txt"
done
# --- NO ACTION: Purgatory expired (system handled it) ---
while IFS='|' read -r repo npub; do
[[ -z "$repo" ]] && continue
key="${repo}|${npub}"
if grep -qF "$key" "$tmp_dir/processed.txt" 2>/dev/null; then
continue
fi
echo "$repo | $npub | purgatory expired (system already handled)" >> "$no_action"
echo "$key" >> "$tmp_dir/processed.txt"
((count_no_action++)) || true
done < "$tmp_dir/purgatory_expired.txt"
# --- ACTION REQUIRED: Complete in prod, missing from archive ---
if [[ -f "$comparison_dir/complete-prod-missing-archive.txt" ]]; then
while IFS= read -r line; do
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
repo=$(echo "$line" | extract_repo)
npub=$(echo "$line" | extract_npub)
key="${repo}|${npub}"
if grep -qF "$key" "$tmp_dir/processed.txt" 2>/dev/null; then
continue
fi
# Check for parse failure
if grep -qF "$key" "$tmp_dir/parse_failures.txt" 2>/dev/null; then
echo "$repo | $npub | complete in prod, missing from archive, parse failure logged | investigate parse failure, may need re-announcement" >> "$manual_inv"
echo "$key" >> "$tmp_dir/processed.txt"
((count_manual++)) || true
else
echo "$repo | $npub | complete in prod, missing from archive | trigger re-sync or investigate why not archived" >> "$action_req"
echo "$key" >> "$tmp_dir/processed.txt"
((count_action++)) || true
fi
done < "$comparison_dir/complete-prod-missing-archive.txt"
fi
# --- ACTION REQUIRED: Complete in prod, incomplete in archive ---
if [[ -f "$comparison_dir/complete-prod-incomplete-archive.txt" ]]; then
while IFS= read -r line; do
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
repo=$(echo "$line" | extract_repo)
npub=$(echo "$line" | extract_npub)
key="${repo}|${npub}"
if grep -qF "$key" "$tmp_dir/processed.txt" 2>/dev/null; then
continue
fi
# Extract archive status from line
archive_status=$(echo "$line" | grep -oP 'archive=\K[^ ]+' || echo "unknown")
echo "$repo | $npub | complete in prod, $archive_status in archive | wait for sync to complete or trigger re-sync" >> "$action_req"
echo "$key" >> "$tmp_dir/processed.txt"
((count_action++)) || true
done < "$comparison_dir/complete-prod-incomplete-archive.txt"
fi
# --- ACTION REQUIRED: Incomplete in both ---
if [[ -f "$comparison_dir/incomplete-in-both.txt" ]]; then
while IFS= read -r line; do
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
repo=$(echo "$line" | extract_repo)
npub=$(echo "$line" | extract_npub)
key="${repo}|${npub}"
if grep -qF "$key" "$tmp_dir/processed.txt" 2>/dev/null; then
continue
fi
# Extract statuses
prod_status=$(echo "$line" | grep -oP 'prod=\K[^ ]+' | tr -d '|' || echo "unknown")
archive_status=$(echo "$line" | grep -oP 'archive=\K[^ ]+' || echo "unknown")
echo "$repo | $npub | incomplete in both (prod=$prod_status, archive=$archive_status) | investigate git data source, may need user to re-push" >> "$action_req"
echo "$key" >> "$tmp_dir/processed.txt"
((count_action++)) || true
done < "$comparison_dir/incomplete-in-both.txt"
fi
# --- MANUAL INVESTIGATION: In archive but not prod ---
if [[ -f "$comparison_dir/in-archive-not-prod.txt" ]]; then
while IFS= read -r line; do
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
repo=$(echo "$line" | extract_repo)
npub=$(echo "$line" | extract_npub)
key="${repo}|${npub}"
if grep -qF "$key" "$tmp_dir/processed.txt" 2>/dev/null; then
continue
fi
archive_status=$(echo "$line" | grep -oP 'archive=\K[^ ]+' || echo "unknown")
# Check if it was deleted
if grep -qF "$key" "$tmp_dir/all_deletions.txt" 2>/dev/null; then
echo "$repo | $npub | in archive not prod, deletion exists | verify deletion was intentional" >> "$manual_inv"
else
echo "$repo | $npub | in archive ($archive_status) but not in prod | may be new announcement or deleted from prod" >> "$manual_inv"
fi
echo "$key" >> "$tmp_dir/processed.txt"
((count_manual++)) || true
done < "$comparison_dir/in-archive-not-prod.txt"
fi
# --- ACTION REQUIRED: Parse failures not yet processed ---
while IFS='|' read -r repo npub; do
[[ -z "$repo" ]] && continue
key="${repo}|${npub}"
if grep -qF "$key" "$tmp_dir/processed.txt" 2>/dev/null; then
continue
fi
echo "$repo | $npub | parse failure logged | fix event format or request user to re-announce" >> "$action_req"
echo "$key" >> "$tmp_dir/processed.txt"
((count_action++)) || true
done < "$tmp_dir/parse_failures.txt"
# --- MANUAL INVESTIGATION: Prod category 3/4 not yet processed ---
for cat_file in "$tmp_dir/prod_cat3.txt" "$tmp_dir/prod_cat4.txt"; do
[[ ! -f "$cat_file" ]] && continue
cat_name=$(basename "$cat_file" .txt | sed 's/prod_//')
while IFS='|' read -r repo npub; do
[[ -z "$repo" ]] && continue
key="${repo}|${npub}"
if grep -qF "$key" "$tmp_dir/processed.txt" 2>/dev/null; then
continue
fi
if [[ "$cat_name" == "cat3" ]]; then
echo "$repo | $npub | partial match in prod, not in comparison results | investigate git ref mismatch" >> "$manual_inv"
else
echo "$repo | $npub | no match in prod (git exists but refs don't match) | investigate git data corruption" >> "$manual_inv"
fi
echo "$key" >> "$tmp_dir/processed.txt"
((count_manual++)) || true
done < "$cat_file"
done
# =========================================================================
# STEP 5: Count final results
# =========================================================================
count_no_action=$(count_lines "$no_action")
count_action=$(count_lines "$action_req")
count_manual=$(count_lines "$manual_inv")
# Ensure counts are valid integers
count_no_action=${count_no_action:-0}
count_action=${count_action:-0}
count_manual=${count_manual:-0}
local total=$((count_no_action + count_action + count_manual))
# Handle division by zero
if [[ $total -eq 0 ]]; then
total=1 # Avoid division by zero in percentage calculations
log_warn "No repos were classified. Check input files."
fi
# =========================================================================
# STEP 6: Generate summary
# =========================================================================
log_info "Generating summary..."
cat > "$summary" << EOF
# Migration Classification Summary
Generated: $(date -Iseconds)
Analysis Directory: $analysis_dir
## Overview
| Category | Count | Percentage |
|----------|-------|------------|
| No Action Required | $count_no_action | $(awk "BEGIN {printf \"%.1f\", ($count_no_action/$total)*100}")% |
| Action Required | $count_action | $(awk "BEGIN {printf \"%.1f\", ($count_action/$total)*100}")% |
| Manual Investigation | $count_manual | $(awk "BEGIN {printf \"%.1f\", ($count_manual/$total)*100}")% |
| **Total** | **$total** | **100%** |
## No Action Required ($count_no_action repos)
These repositories are ready for migration or don't need migration:
EOF
# Breakdown of no-action reasons
echo "| Reason | Count |" >> "$summary"
echo "|--------|-------|" >> "$summary"
grep -v '^#' "$no_action" 2>/dev/null | awk -F' \\| ' '{print $3}' | sort | uniq -c | sort -rn > "$tmp_dir/no_action_breakdown.txt" || true
while read -r cnt reason; do
echo "| $reason | $cnt |" >> "$summary"
done < "$tmp_dir/no_action_breakdown.txt"
cat >> "$summary" << EOF
## Action Required ($count_action repos)
These repositories need intervention before migration:
EOF
# Breakdown of action reasons
echo "| Reason | Count | Suggested Action |" >> "$summary"
echo "|--------|-------|------------------|" >> "$summary"
grep -v '^#' "$action_req" 2>/dev/null | awk -F' \\| ' '{print $3 "|" $4}' | sort | uniq -c | sort -rn > "$tmp_dir/action_breakdown.txt" || true
while read -r cnt reason_action; do
reason=$(echo "$reason_action" | cut -d'|' -f1)
action=$(echo "$reason_action" | cut -d'|' -f2)
echo "| $reason | $cnt | $action |" >> "$summary"
done < "$tmp_dir/action_breakdown.txt"
cat >> "$summary" << EOF
## Manual Investigation ($count_manual repos)
These repositories have conflicting or unexpected states requiring human review:
EOF
# Breakdown of manual investigation reasons
echo "| Reason | Count |" >> "$summary"
echo "|--------|-------|" >> "$summary"
grep -v '^#' "$manual_inv" 2>/dev/null | awk -F' \\| ' '{print $3}' | sort | uniq -c | sort -rn > "$tmp_dir/manual_breakdown.txt" || true
while read -r cnt reason; do
echo "| $reason | $cnt |" >> "$summary"
done < "$tmp_dir/manual_breakdown.txt"
# Pre-compute counts from temp files before they might be cleaned up
local prod_del_count archive_del_count
local prod_cat1_count prod_cat2_count prod_cat3_count prod_cat4_count
local archive_cat1_count archive_cat2_count archive_cat3_count archive_cat4_count
local parse_fail_count purgatory_count
prod_del_count=$(wc -l < "$tmp_dir/prod_deletions.txt" 2>/dev/null | tr -d ' ') || prod_del_count=0
archive_del_count=$(wc -l < "$tmp_dir/archive_deletions.txt" 2>/dev/null | tr -d ' ') || archive_del_count=0
prod_cat1_count=$(wc -l < "$tmp_dir/prod_cat1.txt" 2>/dev/null | tr -d ' ') || prod_cat1_count=0
prod_cat2_count=$(wc -l < "$tmp_dir/prod_cat2.txt" 2>/dev/null | tr -d ' ') || prod_cat2_count=0
prod_cat3_count=$(wc -l < "$tmp_dir/prod_cat3.txt" 2>/dev/null | tr -d ' ') || prod_cat3_count=0
prod_cat4_count=$(wc -l < "$tmp_dir/prod_cat4.txt" 2>/dev/null | tr -d ' ') || prod_cat4_count=0
archive_cat1_count=$(wc -l < "$tmp_dir/archive_cat1.txt" 2>/dev/null | tr -d ' ') || archive_cat1_count=0
archive_cat2_count=$(wc -l < "$tmp_dir/archive_cat2.txt" 2>/dev/null | tr -d ' ') || archive_cat2_count=0
archive_cat3_count=$(wc -l < "$tmp_dir/archive_cat3.txt" 2>/dev/null | tr -d ' ') || archive_cat3_count=0
archive_cat4_count=$(wc -l < "$tmp_dir/archive_cat4.txt" 2>/dev/null | tr -d ' ') || archive_cat4_count=0
parse_fail_count=$(wc -l < "$tmp_dir/parse_failures.txt" 2>/dev/null | tr -d ' ') || parse_fail_count=0
purgatory_count=$(wc -l < "$tmp_dir/purgatory_expired.txt" 2>/dev/null | tr -d ' ') || purgatory_count=0
cat >> "$summary" << EOF
## Input Data Summary
### Phase 1 (Events)
- Prod deletions: $prod_del_count
- Archive deletions: $archive_del_count
### Phase 3 (Categories)
**Prod:**
- Category 1 (complete): $prod_cat1_count
- Category 2 (empty): $prod_cat2_count
- Category 3 (partial): $prod_cat3_count
- Category 4 (no match): $prod_cat4_count
**Archive:**
- Category 1 (complete): $archive_cat1_count
- Category 2 (empty): $archive_cat2_count
- Category 3 (partial): $archive_cat3_count
- Category 4 (no match): $archive_cat4_count
### Phase 4 (Logs)
- Parse failures: $parse_fail_count
- Purgatory expired: $purgatory_count
## Recommended Next Steps
1. **Review action-required.txt** - Address these repos before migration
2. **Review manual-investigation.txt** - Investigate unusual states
3. **Verify no-action-required.txt** - Spot-check a few repos to confirm
4. **Plan migration window** - Schedule cutover when action items are resolved
## Output Files
- \`results/no-action-required.txt\` - $count_no_action repos ready for migration
- \`results/action-required.txt\` - $count_action repos needing intervention
- \`results/manual-investigation.txt\` - $count_manual repos needing human review
- \`results/summary.txt\` - This summary file
EOF
# =========================================================================
# STEP 7: Display results
# =========================================================================
echo ""
log_info "=== Classification Complete ==="
echo ""
log_success "No Action Required: $count_no_action repos"
log_warn "Action Required: $count_action repos"
log_error "Manual Investigation: $count_manual repos"
echo ""
log_info "Total: $total repos classified"
echo ""
log_info "Output files:"
echo " $no_action"
echo " $action_req"
echo " $manual_inv"
echo " $summary"
echo ""
# Show top action items
if [[ $count_action -gt 0 ]]; then
log_info "Top action items:"
grep -v '^#' "$action_req" 2>/dev/null | awk -F' \\| ' '{print $3}' | sort | uniq -c | sort -rn | head -5 | while read -r cnt reason; do
echo " - $reason: $cnt repos"
done
echo ""
fi
# Show top investigation items
if [[ $count_manual -gt 0 ]]; then
log_info "Top investigation items:"
grep -v '^#' "$manual_inv" 2>/dev/null | awk -F' \\| ' '{print $3}' | sort | uniq -c | sort -rn | head -5 | while read -r cnt reason; do
echo " - $reason: $cnt repos"
done
echo ""
fi
log_info "See $summary for full details and recommended next steps."
}
main "$@"