From e25b9591d1f2cb4f54649d728104993e11e53d24 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Mon, 26 Jan 2026 08:53:46 +0000 Subject: Add classification script with Option B redesign Implements the redesigned migration analysis classification system: Tier 1 - Ready for Migration (no action required): - Complete in both prod and archive - Deleted by user (kind 5 event) - Empty in prod (cat2) - always no action, regardless of archive - Archive-only (not in prod) - Purgatory-only (not in prod) Tier 2 - Needs Re-sync (action required): - Complete in prod, missing/incomplete in archive - Includes purgatory context (expired vs never-tried) Tier 3 - Manual Review (investigation needed): - Partial in prod (cat3) - No-match in prod (cat4) - Parse failures with complete prod Key fixes: - Use safe arithmetic ($((x + 1))) instead of ((x++)) with set -e - Batch nak hex-to-npub conversions for deletion processing - Handle NDJSON format for deletion files Output: 352 ready, 295 resync, 46 review (693 total) --- scripts/40-classify-actions.sh | 588 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 588 insertions(+) create mode 100755 scripts/40-classify-actions.sh (limited to 'scripts') diff --git a/scripts/40-classify-actions.sh b/scripts/40-classify-actions.sh new file mode 100755 index 0000000..021a2da --- /dev/null +++ b/scripts/40-classify-actions.sh @@ -0,0 +1,588 @@ +#!/usr/bin/env bash +# +# 40-classify-actions.sh - Classify repos by migration action required +# +# Implements the redesigned classification system (Option B) with user feedback: +# +# Tier 1: No Action Required (ready-for-migration.txt) +# - Complete in both (prod=cat1, archive=cat1) +# - Deleted by user (kind 5 event) +# - Empty in prod (prod=cat2, any archive status) +# - Archive-only (archive=any, prod=missing) +# - Not in prod (purgatory-only, prod=missing) +# +# Tier 2: Action Required (needs-resync.txt) +# - Complete in prod, missing from archive (with purgatory context) +# - Complete in prod, incomplete in archive (with purgatory context) +# +# Tier 3: Manual Investigation (manual-review.txt) +# - Partial in prod (prod=cat3) +# - No-match in prod (prod=cat4) +# - Parse failures +# - Conflicting states +# +# Usage: ./40-classify-actions.sh +# +# Output format: repo | npub | prod_status | archive_status | context | action +# + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_success() { echo -e "${GREEN}[OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# Check arguments +if [[ $# -lt 1 ]]; then + echo "Usage: $0 " + echo "Example: $0 work/migration-analysis-20260123-200701" + exit 1 +fi + +ANALYSIS_DIR="$1" + +# Validate analysis directory +if [[ ! -d "$ANALYSIS_DIR" ]]; then + log_error "Analysis directory not found: $ANALYSIS_DIR" + exit 1 +fi + +# Define paths +PROD_DIR="$ANALYSIS_DIR/prod" +ARCHIVE_DIR="$ANALYSIS_DIR/archive" +COMPARISON_DIR="$ANALYSIS_DIR/comparison" +LOGS_DIR="$ANALYSIS_DIR/logs" +RESULTS_DIR="$ANALYSIS_DIR/results" + +# Validate required directories +for dir in "$PROD_DIR" "$ARCHIVE_DIR" "$COMPARISON_DIR" "$LOGS_DIR"; do + if [[ ! -d "$dir" ]]; then + log_error "Required directory not found: $dir" + exit 1 + fi +done + +# Create results directory +mkdir -p "$RESULTS_DIR" + +# Output files +READY_FILE="$RESULTS_DIR/ready-for-migration.txt" +RESYNC_FILE="$RESULTS_DIR/needs-resync.txt" +REVIEW_FILE="$RESULTS_DIR/manual-review.txt" +SUMMARY_FILE="$RESULTS_DIR/summary.txt" + +# Temporary files for processing +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT + +log_info "Starting classification with revised system (Option B)" +log_info "Analysis directory: $ANALYSIS_DIR" + +# ============================================================================ +# Phase 1: Build lookup tables from source data +# ============================================================================ + +log_info "Building lookup tables..." + +# Build prod category lookup: repo|npub -> category +declare -A PROD_CAT +while IFS='|' read -r repo npub rest; do + repo=$(echo "$repo" | xargs) + npub=$(echo "$npub" | xargs) + [[ -z "$repo" || -z "$npub" ]] && continue + PROD_CAT["$repo|$npub"]="cat1" +done < "$PROD_DIR/category1-complete-match.txt" + +while IFS='|' read -r repo npub rest; do + repo=$(echo "$repo" | xargs) + npub=$(echo "$npub" | xargs) + [[ -z "$repo" || -z "$npub" ]] && continue + PROD_CAT["$repo|$npub"]="cat2" +done < "$PROD_DIR/category2-empty-blank.txt" + +while IFS='|' read -r repo npub rest; do + repo=$(echo "$repo" | xargs) + npub=$(echo "$npub" | xargs) + [[ -z "$repo" || -z "$npub" ]] && continue + PROD_CAT["$repo|$npub"]="cat3" +done < "$PROD_DIR/category3-partial-match.txt" + +while IFS='|' read -r repo npub rest; do + repo=$(echo "$repo" | xargs) + npub=$(echo "$npub" | xargs) + [[ -z "$repo" || -z "$npub" ]] && continue + PROD_CAT["$repo|$npub"]="cat4" +done < "$PROD_DIR/category4-no-match.txt" + +log_info "Loaded ${#PROD_CAT[@]} prod entries" + +# Build archive category lookup: repo|npub -> category +declare -A ARCHIVE_CAT +while IFS='|' read -r repo npub rest; do + repo=$(echo "$repo" | xargs) + npub=$(echo "$npub" | xargs) + [[ -z "$repo" || -z "$npub" ]] && continue + ARCHIVE_CAT["$repo|$npub"]="cat1" +done < "$ARCHIVE_DIR/category1-complete-match.txt" + +while IFS='|' read -r repo npub rest; do + repo=$(echo "$repo" | xargs) + npub=$(echo "$npub" | xargs) + [[ -z "$repo" || -z "$npub" ]] && continue + ARCHIVE_CAT["$repo|$npub"]="cat2" +done < "$ARCHIVE_DIR/category2-empty-blank.txt" + +while IFS='|' read -r repo npub rest; do + repo=$(echo "$repo" | xargs) + npub=$(echo "$npub" | xargs) + [[ -z "$repo" || -z "$npub" ]] && continue + ARCHIVE_CAT["$repo|$npub"]="cat3" +done < "$ARCHIVE_DIR/category3-partial-match.txt" + +while IFS='|' read -r repo npub rest; do + repo=$(echo "$repo" | xargs) + npub=$(echo "$npub" | xargs) + [[ -z "$repo" || -z "$npub" ]] && continue + ARCHIVE_CAT["$repo|$npub"]="cat4" +done < "$ARCHIVE_DIR/category4-no-match.txt" + +log_info "Loaded ${#ARCHIVE_CAT[@]} archive entries" + +# Build purgatory lookup: repo|npub -> 1 (if purgatory expired) +declare -A PURGATORY +if [[ -f "$LOGS_DIR/purgatory-expired.txt" ]]; then + while IFS=$'\t' read -r repo npub timestamp reason; do + # Skip comments and empty lines + [[ "$repo" =~ ^# ]] && continue + [[ -z "$repo" || -z "$npub" ]] && continue + PURGATORY["$repo|$npub"]=1 + done < "$LOGS_DIR/purgatory-expired.txt" +fi +log_info "Loaded ${#PURGATORY[@]} purgatory entries" + +# Build parse failure lookup: repo|npub -> 1 (if parse failure logged) +# Parse failures file format: event_idkindreasonreponpub +declare -A PARSE_FAIL +if [[ -f "$LOGS_DIR/parse-failures.txt" ]]; then + while IFS=$'\t' read -r event_id kind reason repo npub; do + # Skip comments and empty lines + [[ "$event_id" =~ ^# ]] && continue + [[ -z "$repo" || -z "$npub" ]] && continue + PARSE_FAIL["$repo|$npub"]=1 + done < "$LOGS_DIR/parse-failures.txt" +fi +log_info "Loaded ${#PARSE_FAIL[@]} parse failure entries" + +# Build deletion lookup: repo|npub -> 1 (if kind 5 deletion event) +# Deletions are in NDJSON format with "a" tags like "30617:pubkey_hex:repo" +# We need to convert hex pubkeys to npub format using nak +declare -A DELETED + +# Helper function to process deletion file (NDJSON format) +# Extracts unique pubkey_hex:repo pairs and converts to npub +process_deletions() { + local file="$1" + [[ ! -f "$file" ]] && return + + # Extract unique pubkey_hex|repo pairs from NDJSON + # Each line is a JSON object, extract "a" tags + local pairs + pairs=$(jq -r '.tags[] | select(.[0] == "a") | .[1]' "$file" 2>/dev/null | \ + sed 's/^30617://' | awk -F: '{print $1 "|" $2}' | sort -u) + + # Get unique hex pubkeys for batch conversion + local hex_keys + hex_keys=$(echo "$pairs" | cut -d'|' -f1 | sort -u) + + # Build hex->npub lookup via batch nak call + declare -A HEX_TO_NPUB + while read -r hex; do + [[ -z "$hex" ]] && continue + local npub + npub=$(nak encode npub "$hex" 2>/dev/null || echo "") + [[ -n "$npub" ]] && HEX_TO_NPUB["$hex"]="$npub" + done <<< "$hex_keys" + + # Now process pairs with cached npub values + while IFS='|' read -r pubkey_hex repo; do + [[ -z "$repo" || -z "$pubkey_hex" ]] && continue + local npub="${HEX_TO_NPUB[$pubkey_hex]:-}" + [[ -z "$npub" ]] && continue + DELETED["$repo|$npub"]=1 + done <<< "$pairs" +} + +# Process prod and archive deletions +process_deletions "$PROD_DIR/raw/deletions.json" +process_deletions "$ARCHIVE_DIR/raw/deletions.json" +log_info "Loaded ${#DELETED[@]} deletion entries" + +# ============================================================================ +# Phase 2: Build unique repo list from all sources +# ============================================================================ + +log_info "Building unique repo list..." + +declare -A ALL_REPOS +for key in "${!PROD_CAT[@]}"; do + ALL_REPOS["$key"]=1 +done +for key in "${!ARCHIVE_CAT[@]}"; do + ALL_REPOS["$key"]=1 +done +for key in "${!PURGATORY[@]}"; do + ALL_REPOS["$key"]=1 +done + +log_info "Total unique repos: ${#ALL_REPOS[@]}" + +# ============================================================================ +# Phase 3: Classify each repo according to revised decision tree +# ============================================================================ + +log_info "Classifying repos..." + +# Counters for summary +declare -A COUNTS +COUNTS[ready_complete_both]=0 +COUNTS[ready_deleted]=0 +COUNTS[ready_empty_prod]=0 +COUNTS[ready_archive_only]=0 +COUNTS[ready_not_in_prod]=0 +COUNTS[resync_missing_archive]=0 +COUNTS[resync_incomplete_archive]=0 +COUNTS[review_partial_prod]=0 +COUNTS[review_nomatch_prod]=0 +COUNTS[review_parse_failure]=0 +COUNTS[review_conflicting]=0 + +# Output arrays +declare -a READY_LINES +declare -a RESYNC_LINES +declare -a REVIEW_LINES + +# Helper function to get context string +get_context() { + local key="$1" + local prod_status="$2" + local archive_status="$3" + local context="" + + # Check purgatory + if [[ -n "${PURGATORY[$key]:-}" ]]; then + context="purgatory-expired" + fi + + # Check parse failure + if [[ -n "${PARSE_FAIL[$key]:-}" ]]; then + if [[ -n "$context" ]]; then + context="$context, parse-failure" + else + context="parse-failure" + fi + fi + + # Add archive context for unexpected states + if [[ "$prod_status" == "empty" && "$archive_status" != "missing" && "$archive_status" != "empty" ]]; then + if [[ -n "$context" ]]; then + context="$context, archive-has-data" + else + context="archive-has-data" + fi + fi + + echo "${context:-none}" +} + +# Helper to convert category to human-readable status +cat_to_status() { + case "$1" in + cat1) echo "complete" ;; + cat2) echo "empty" ;; + cat3) echo "partial" ;; + cat4) echo "no-match" ;; + missing) echo "missing" ;; + *) echo "$1" ;; + esac +} + +LOOP_COUNT=0 +for key in "${!ALL_REPOS[@]}"; do + LOOP_COUNT=$((LOOP_COUNT + 1)) + [[ $((LOOP_COUNT % 100)) -eq 0 ]] && log_info "Processed $LOOP_COUNT repos..." + IFS='|' read -r repo npub <<< "$key" + + prod_cat="${PROD_CAT[$key]:-missing}" + archive_cat="${ARCHIVE_CAT[$key]:-missing}" + prod_status=$(cat_to_status "$prod_cat") + archive_status=$(cat_to_status "$archive_cat") + + # Decision tree implementation + + # 1. Is there a kind 5 deletion event? + if [[ -n "${DELETED[$key]:-}" ]]; then + context=$(get_context "$key" "$prod_status" "$archive_status") + READY_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | deleted by user") + COUNTS[ready_deleted]=$((COUNTS[ready_deleted] + 1)) + continue + fi + + # 2. What is the prod status? + case "$prod_cat" in + missing) + # Not in prod + if [[ "$archive_cat" != "missing" ]]; then + # In archive but not in prod -> no action (archive-only) + context=$(get_context "$key" "$prod_status" "$archive_status") + READY_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | archive-only (not in prod)") + COUNTS[ready_archive_only]=$((COUNTS[ready_archive_only] + 1)) + elif [[ -n "${PURGATORY[$key]:-}" ]]; then + # Purgatory only, not in prod -> no action + context="purgatory-expired" + READY_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | purgatory-only (not in prod)") + COUNTS[ready_not_in_prod]=$((COUNTS[ready_not_in_prod] + 1)) + fi + # Otherwise skip (not a real repo - no data anywhere) + ;; + + cat2) + # Empty in prod -> ALWAYS no action required + context=$(get_context "$key" "$prod_status" "$archive_status") + READY_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | empty in prod (user never pushed)") + COUNTS[ready_empty_prod]=$((COUNTS[ready_empty_prod] + 1)) + ;; + + cat1) + # Complete in prod + if [[ "$archive_cat" == "cat1" ]]; then + # Complete in both -> no action + context=$(get_context "$key" "$prod_status" "$archive_status") + READY_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | complete in both") + COUNTS[ready_complete_both]=$((COUNTS[ready_complete_both] + 1)) + else + # Complete in prod, missing/incomplete in archive + # Check for parse failure - if so, needs manual review + if [[ -n "${PARSE_FAIL[$key]:-}" ]]; then + context=$(get_context "$key" "$prod_status" "$archive_status") + REVIEW_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | complete in prod with parse failure") + COUNTS[review_parse_failure]=$((COUNTS[review_parse_failure] + 1)) + else + # Needs resync - include purgatory context + context=$(get_context "$key" "$prod_status" "$archive_status") + if [[ "$archive_cat" == "missing" ]]; then + RESYNC_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | trigger re-sync to archive") + COUNTS[resync_missing_archive]=$((COUNTS[resync_missing_archive] + 1)) + else + RESYNC_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | trigger re-sync (archive incomplete)") + COUNTS[resync_incomplete_archive]=$((COUNTS[resync_incomplete_archive] + 1)) + fi + fi + fi + ;; + + cat3) + # Partial in prod -> ALWAYS manual investigation + context=$(get_context "$key" "$prod_status" "$archive_status") + REVIEW_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | partial in prod (investigate git data)") + COUNTS[review_partial_prod]=$((COUNTS[review_partial_prod] + 1)) + ;; + + cat4) + # No-match in prod -> ALWAYS manual investigation + context=$(get_context "$key" "$prod_status" "$archive_status") + REVIEW_LINES+=("$repo | $npub | $prod_status | $archive_status | $context | no-match in prod (git corruption)") + COUNTS[review_nomatch_prod]=$((COUNTS[review_nomatch_prod] + 1)) + ;; + esac +done + +# ============================================================================ +# Phase 4: Write output files +# ============================================================================ + +log_info "Writing output files..." + +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S+00:00") + +# Write ready-for-migration.txt +{ + echo "# Ready for Migration - No action required" + echo "# Generated: $TIMESTAMP" + echo "# Format: repo | npub | prod_status | archive_status | context | reason" + echo "#" + for line in "${READY_LINES[@]}"; do + echo "$line" + done +} > "$READY_FILE" + +# Write needs-resync.txt +{ + echo "# Needs Re-sync - Action required" + echo "# Generated: $TIMESTAMP" + echo "# Format: repo | npub | prod_status | archive_status | context | action" + echo "#" + echo "# Context meanings:" + echo "# purgatory-expired = archive tried to sync but failed (30min timeout)" + echo "# none = archive never tried or announcement missing" + echo "#" + for line in "${RESYNC_LINES[@]}"; do + echo "$line" + done +} > "$RESYNC_FILE" + +# Write manual-review.txt +{ + echo "# Manual Review Required - Investigation needed" + echo "# Generated: $TIMESTAMP" + echo "# Format: repo | npub | prod_status | archive_status | context | reason" + echo "#" + for line in "${REVIEW_LINES[@]}"; do + echo "$line" + done +} > "$REVIEW_FILE" + +# ============================================================================ +# Phase 5: Generate summary +# ============================================================================ + +log_info "Generating summary..." + +TOTAL_READY=${#READY_LINES[@]} +TOTAL_RESYNC=${#RESYNC_LINES[@]} +TOTAL_REVIEW=${#REVIEW_LINES[@]} +TOTAL=$((TOTAL_READY + TOTAL_RESYNC + TOTAL_REVIEW)) + +# Calculate percentages +if [[ $TOTAL -gt 0 ]]; then + PCT_READY=$(awk "BEGIN {printf \"%.1f\", ($TOTAL_READY / $TOTAL) * 100}") + PCT_RESYNC=$(awk "BEGIN {printf \"%.1f\", ($TOTAL_RESYNC / $TOTAL) * 100}") + PCT_REVIEW=$(awk "BEGIN {printf \"%.1f\", ($TOTAL_REVIEW / $TOTAL) * 100}") +else + PCT_READY="0.0" + PCT_RESYNC="0.0" + PCT_REVIEW="0.0" +fi + +{ + echo "# Migration Classification Summary" + echo "Generated: $TIMESTAMP" + echo "Analysis Directory: $ANALYSIS_DIR" + echo "" + echo "## Overview" + echo "" + echo "| Category | Count | Percentage |" + echo "|----------|-------|------------|" + echo "| Ready for Migration | $TOTAL_READY | $PCT_READY% |" + echo "| Needs Re-sync | $TOTAL_RESYNC | $PCT_RESYNC% |" + echo "| Manual Review | $TOTAL_REVIEW | $PCT_REVIEW% |" + echo "| **Total** | **$TOTAL** | **100%** |" + echo "" + echo "## Tier 1: Ready for Migration ($TOTAL_READY repos)" + echo "" + echo "These repositories are ready for migration or don't need migration:" + echo "" + echo "| Reason | Count |" + echo "|--------|-------|" + echo "| complete in both prod and archive | ${COUNTS[ready_complete_both]} |" + echo "| deleted by user | ${COUNTS[ready_deleted]} |" + echo "| empty in prod (user never pushed) | ${COUNTS[ready_empty_prod]} |" + echo "| archive-only (not in prod) | ${COUNTS[ready_archive_only]} |" + echo "| purgatory-only (not in prod) | ${COUNTS[ready_not_in_prod]} |" + echo "" + echo "## Tier 2: Needs Re-sync ($TOTAL_RESYNC repos)" + echo "" + echo "These repositories need re-sync to archive before migration:" + echo "" + echo "| Reason | Count | Action |" + echo "|--------|-------|--------|" + echo "| complete in prod, missing from archive | ${COUNTS[resync_missing_archive]} | trigger re-sync |" + echo "| complete in prod, incomplete in archive | ${COUNTS[resync_incomplete_archive]} | trigger re-sync |" + echo "" + echo "### Purgatory Context" + echo "" + echo "Repos in needs-resync.txt include purgatory context:" + echo "- **purgatory-expired**: Archive tried to sync but failed (30min timeout)" + echo "- **none**: Archive never tried or announcement missing" + echo "" + echo "## Tier 3: Manual Review ($TOTAL_REVIEW repos)" + echo "" + echo "These repositories require human investigation:" + echo "" + echo "| Reason | Count |" + echo "|--------|-------|" + echo "| partial in prod (cat3) | ${COUNTS[review_partial_prod]} |" + echo "| no-match in prod (cat4) | ${COUNTS[review_nomatch_prod]} |" + echo "| complete in prod with parse failure | ${COUNTS[review_parse_failure]} |" + echo "" + echo "## Input Data Summary" + echo "" + echo "### Prod Categories" + echo "- Category 1 (complete): $(wc -l < "$PROD_DIR/category1-complete-match.txt")" + echo "- Category 2 (empty): $(wc -l < "$PROD_DIR/category2-empty-blank.txt")" + echo "- Category 3 (partial): $(wc -l < "$PROD_DIR/category3-partial-match.txt")" + echo "- Category 4 (no match): $(wc -l < "$PROD_DIR/category4-no-match.txt")" + echo "" + echo "### Archive Categories" + echo "- Category 1 (complete): $(wc -l < "$ARCHIVE_DIR/category1-complete-match.txt")" + echo "- Category 2 (empty): $(wc -l < "$ARCHIVE_DIR/category2-empty-blank.txt")" + echo "- Category 3 (partial): $(wc -l < "$ARCHIVE_DIR/category3-partial-match.txt")" + echo "- Category 4 (no match): $(wc -l < "$ARCHIVE_DIR/category4-no-match.txt")" + echo "" + echo "### Logs" + echo "- Parse failures: $(grep -c -v '^#' "$LOGS_DIR/parse-failures.txt" 2>/dev/null || echo 0)" + echo "- Purgatory expired: $(grep -c -v '^#' "$LOGS_DIR/purgatory-expired.txt" 2>/dev/null || echo 0)" + echo "" + echo "## Output Files" + echo "" + echo "- \`results/ready-for-migration.txt\` - $TOTAL_READY repos ready for migration" + echo "- \`results/needs-resync.txt\` - $TOTAL_RESYNC repos needing re-sync" + echo "- \`results/manual-review.txt\` - $TOTAL_REVIEW repos needing investigation" + echo "- \`results/summary.txt\` - This summary file" + echo "" + echo "## Recommended Next Steps" + echo "" + echo "1. **Review needs-resync.txt** - Trigger re-sync for these repos" + echo "2. **Review manual-review.txt** - Investigate unusual states" + echo "3. **Verify ready-for-migration.txt** - Spot-check a few repos" + echo "4. **Plan migration window** - Schedule cutover when action items resolved" +} > "$SUMMARY_FILE" + +# ============================================================================ +# Phase 6: Print summary to console +# ============================================================================ + +echo "" +log_success "Classification complete!" +echo "" +echo "=== Summary ===" +echo "Ready for Migration: $TOTAL_READY ($PCT_READY%)" +echo " - Complete in both: ${COUNTS[ready_complete_both]}" +echo " - Deleted by user: ${COUNTS[ready_deleted]}" +echo " - Empty in prod: ${COUNTS[ready_empty_prod]}" +echo " - Archive-only: ${COUNTS[ready_archive_only]}" +echo " - Purgatory-only: ${COUNTS[ready_not_in_prod]}" +echo "" +echo "Needs Re-sync: $TOTAL_RESYNC ($PCT_RESYNC%)" +echo " - Missing from archive: ${COUNTS[resync_missing_archive]}" +echo " - Incomplete in archive: ${COUNTS[resync_incomplete_archive]}" +echo "" +echo "Manual Review: $TOTAL_REVIEW ($PCT_REVIEW%)" +echo " - Partial in prod: ${COUNTS[review_partial_prod]}" +echo " - No-match in prod: ${COUNTS[review_nomatch_prod]}" +echo " - Parse failures: ${COUNTS[review_parse_failure]}" +echo "" +echo "Total: $TOTAL repos" +echo "" +echo "Output files:" +echo " $READY_FILE" +echo " $RESYNC_FILE" +echo " $REVIEW_FILE" +echo " $SUMMARY_FILE" -- cgit v1.2.3