#!/usr/bin/env bash set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" RULES_FILE="${HOMELAB_IMPACT_RULES:-${REPO_ROOT}/infra/jeannie-impact/rules.tsv}" usage() { cat <<'EOF' Usage: ./jeannie impact [--since REF] [PATH...] Explain which homelab areas are touched by changed files or explicit paths. Default mode analyzes the current working tree diff. EOF } since="" paths=() while [ "$#" -gt 0 ]; do case "$1" in --since) since="${2:-}" shift 2 ;; -h|--help|help) usage exit 0 ;; *) paths+=("$1") shift ;; esac done if [ ! -s "$RULES_FILE" ]; then echo "Missing impact rules: $RULES_FILE" >&2 exit 1 fi if [ "${#paths[@]}" -eq 0 ]; then if [ -n "$since" ]; then mapfile -t paths < <(git -C "$REPO_ROOT" diff --name-only "$since"...HEAD) else mapfile -t paths < <(git -C "$REPO_ROOT" status --short | awk '{print $NF}') fi fi if [ "${#paths[@]}" -eq 0 ]; then echo "No changed files or paths to analyze." exit 0 fi python3 - "$RULES_FILE" "${paths[@]}" <<'PY' import csv import sys rules_file = sys.argv[1] paths = [path.strip() for path in sys.argv[2:] if path.strip()] with open(rules_file, encoding="utf-8", newline="") as handle: rules = list(csv.DictReader(handle, delimiter="\t")) matches = [] for path in paths: normalized = path.removeprefix("./") for rule in rules: pattern = rule["pattern"] if normalized == pattern.rstrip("/") or normalized.startswith(pattern): matches.append((normalized, rule)) print("Jeannie Impact") print("==============") print("Files:") for path in paths: print(f" {path}") if not matches: print("\nNo specific impact rule matched.") print("Validate:") print(" ./jeannie validate") raise SystemExit(0) seen = set() print("\nImpacted Areas") for path, rule in matches: key = (rule["area"], rule["risk"]) if key in seen: continue seen.add(key) print(f"\n- {rule['area']}") print(f" risk: {rule['risk']}") print(" touches:") for item in rule["touches"].split(";"): print(f" - {item}") print(" validate:") for command in rule["validate"].split(";"): print(f" {command}") if rule["apply"] != "none": print(" apply path:") for command in rule["apply"].split(";"): print(f" {command}") else: print(" apply path: none") PY