syntaxai/tdd.md · main · tools / sama-cli / src / d21_main.sh

d21_main.sh 257 lines · 7818 bytes raw
#!/usr/bin/env bash
# d21 — entry: the SAMA v2 shell-CLI dispatcher. Wires the seven §4
# checks, the doctor diagnostic, and the graph adapter together into
# the user-facing `sama` command. This is the only layer permitted
# to read argv, exit, or write to stdout for user consumption.
#
# Invoked by the `tools/sama-cli/sama` wrapper. The wrapper sources
# this file with SAMA_SRC_DIR pointing at tools/sama-cli/src.

# sama-import: a31_constants.sh
# sama-import: b32_utils.sh
# sama-import: b32_checks.sh
# sama-import: c14_graph.sh

set -u

# SAMA_SRC_DIR is exported by the entry wrapper. Re-source siblings
# defensively in case d21 is invoked directly.
SAMA_SRC_DIR="${SAMA_SRC_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)}"
# shellcheck disable=SC1091
. "$SAMA_SRC_DIR/a31_constants.sh"
# shellcheck disable=SC1091
. "$SAMA_SRC_DIR/b32_utils.sh"
# shellcheck disable=SC1091
. "$SAMA_SRC_DIR/b32_checks.sh"
# shellcheck disable=SC1091
. "$SAMA_SRC_DIR/c14_graph.sh"

# Disable color when stdout isn't a TTY (so cross-verify diffs stay clean).
if [ ! -t 1 ]; then
  sama_color_disable
fi
# Respect the NO_COLOR convention (https://no-color.org/).
if [ -n "${NO_COLOR:-}" ]; then
  sama_color_disable
fi

print_usage() {
  cat <<'EOF'
sama — SAMA v2 verifier (shell implementation)

USAGE
  sama check [check-name]   Run all seven §4 checks (or just one)
  sama graph [out.dot]      Emit graphviz .dot of the import graph
  sama doctor               List tools the verifier depends on
  sama --version            Print the verifier version
  sama --help               This message

CHECK NAMES (for `sama check <name>`)
  sorted, architecture, modeled-tests, modeled-boundary,
  atomic, law, consistency

FLAGS
  --profile=PATH            Path to sama.profile.toml (default: ./sama.profile.toml)
  --src=PATH                Path to src directory (default: ./src)
  --summary                 Emit only the "N / 7" verdict line (for scripting)

EXAMPLES
  sama check                # run all checks in the current repo
  sama check --src=tools/sama-cli/src --profile=tools/sama-cli/sama.profile.toml
  sama graph /tmp/graph.dot
  sama doctor

The shell verifier is the independent oracle for SAMA v2: a second
implementation in a different language that should produce the same
7/7 ✓ verdict as the TypeScript verifier at src/b32_sama_v2_verify.ts.
EOF
}

print_version() {
  echo "sama (sama-cli shell verifier) — SAMA v2.0"
}

# Parse --key=value style flags from argv. Sets the globals
# OPT_PROFILE, OPT_SRC, OPT_SUMMARY, and leaves POSITIONAL args
# in the POSITIONAL array.
OPT_PROFILE=""
OPT_SRC=""
OPT_SUMMARY=0
POSITIONAL=()
parse_flags() {
  POSITIONAL=()
  local arg
  for arg in "$@"; do
    case "$arg" in
      --profile=*) OPT_PROFILE="${arg#--profile=}" ;;
      --src=*)     OPT_SRC="${arg#--src=}" ;;
      --summary)   OPT_SUMMARY=1 ;;
      --no-color)  sama_color_disable ;;
      --help|-h)   print_usage; exit 0 ;;
      --version)   print_version; exit 0 ;;
      --*)         echo "unknown flag: $arg" >&2; exit 2 ;;
      *)           POSITIONAL+=("$arg") ;;
    esac
  done
}

# Resolve repo root + src dir + profile path based on flags or defaults.
resolve_paths() {
  local profile_path="$OPT_PROFILE"
  local src_path="$OPT_SRC"
  if [ -z "$profile_path" ]; then
    profile_path="./sama.profile.toml"
  fi
  if [ -z "$src_path" ]; then
    src_path="./src"
  fi
  # Absolute-ify
  profile_path="$(cd "$(dirname "$profile_path")" 2>/dev/null && pwd)/$(basename "$profile_path")"
  src_path="$(cd "$src_path" 2>/dev/null && pwd)"
  if [ -z "$profile_path" ] || [ ! -f "$profile_path" ]; then
    echo "error: profile not found: $OPT_PROFILE (looked at $profile_path)" >&2
    exit 2
  fi
  if [ -z "$src_path" ] || [ ! -d "$src_path" ]; then
    echo "error: src dir not found: $OPT_SRC (looked at $src_path)" >&2
    exit 2
  fi
  RESOLVED_PROFILE="$profile_path"
  # Repo root = parent of src dir.
  RESOLVED_REPO_ROOT="$(cd "$src_path/.." && pwd)"
  RESOLVED_SRC_REL="${src_path#${RESOLVED_REPO_ROOT}/}"
  RESOLVED_SRC_DIR="$src_path"
}

cmd_check() {
  resolve_paths
  parse_profile "$RESOLVED_PROFILE"
  collect_input "$RESOLVED_REPO_ROOT" "$RESOLVED_SRC_DIR"

  local target=""
  if [ "${#POSITIONAL[@]}" -gt 1 ]; then
    target="${POSITIONAL[1]}"
  fi

  local passed=0
  local total=0
  local results=()
  # Order matches src/b32_sama_v2_verify.ts.
  local checks_meta=(
    "1::Sorted::run_check_sorted::sorted"
    "2::Architecture::run_check_architecture::architecture"
    "3::Modeled (tests)::run_check_modeled_tests::modeled-tests"
    "4::Modeled (boundary)::run_check_modeled_boundary::modeled-boundary"
    "5::Atomic::run_check_atomic::atomic"
    "6::Law (§1.2)::run_check_law::law"
    "7::Consistency (§3)::run_check_consistency::consistency"
  )

  if [ "$OPT_SUMMARY" = "0" ]; then
    print_section_header "SAMA v2 verifier — profile: $PROFILE_NAME"
    printf "  Source tree: %s\n" "$RESOLVED_SRC_REL"
    printf "  Files examined: %d (sources + tests)\n" "${#ALL_FILES[@]}"
  fi

  local meta
  for meta in "${checks_meta[@]}"; do
    local id name fn short
    id="$(echo "$meta" | awk -F '::' '{print $1}')"
    name="$(echo "$meta" | awk -F '::' '{print $2}')"
    fn="$(echo "$meta" | awk -F '::' '{print $3}')"
    short="$(echo "$meta" | awk -F '::' '{print $4}')"
    if [ -n "$target" ] && [ "$target" != "$short" ]; then
      continue
    fi
    total=$((total + 1))
    "$fn"
    local violations="${#CHECK_VIOLATIONS[@]}"
    if [ "$violations" -eq 0 ]; then
      passed=$((passed + 1))
    fi
    if [ "$OPT_SUMMARY" = "0" ]; then
      print_check_verdict "$id" "$name" "$CHECK_EXAMINED" "$violations"
      local v
      for v in "${CHECK_VIOLATIONS[@]}"; do
        local vf vd
        vf="${v%%::*}"
        vd="${v#*::}"
        print_violation "$vf" "$vd"
      done
    fi
  done

  echo
  if [ "$passed" = "$total" ]; then
    _c "$COLOR_GREEN"
    printf "  %d / %d %s — all checks passed\n" "$passed" "$total" "$GLYPH_PASS"
    _c "$COLOR_RESET"
  else
    _c "$COLOR_RED"
    printf "  %d / %d %s — %d check(s) failed\n" "$passed" "$total" "$GLYPH_FAIL" "$((total - passed))"
    _c "$COLOR_RESET"
  fi
  echo

  if [ "$passed" = "$total" ]; then
    return 0
  fi
  return 1
}

cmd_graph() {
  resolve_paths
  parse_profile "$RESOLVED_PROFILE"
  collect_input "$RESOLVED_REPO_ROOT" "$RESOLVED_SRC_DIR"

  local out_dot="${POSITIONAL[1]:-/tmp/sama-graph.dot}"
  local out_png="${out_dot%.dot}.png"
  render_graph "$RESOLVED_REPO_ROOT" "$RESOLVED_SRC_DIR" "$out_dot" "$out_png"
}

cmd_doctor() {
  print_section_header "sama doctor — tool availability"
  local entry tool req status version
  for entry in $SAMA_CLI_TOOLS; do
    tool="${entry%:*}"
    req="${entry#*:}"
    if command -v "$tool" > /dev/null 2>&1; then
      version="$("$tool" --version 2>/dev/null | head -1)"
      [ -z "$version" ] && version="(version unknown)"
      _c "$COLOR_GREEN"
      printf "  %s %s [%s]\n" "$GLYPH_PASS" "$tool" "$req"
      _c "$COLOR_DIM"
      printf "      %s\n" "$version"
      _c "$COLOR_RESET"
    else
      if [ "$req" = "required" ]; then
        _c "$COLOR_RED"
        printf "  %s %s [required] — NOT FOUND\n" "$GLYPH_FAIL" "$tool"
        _c "$COLOR_RESET"
      else
        _c "$COLOR_YELLOW"
        printf "  - %s [optional] — not installed\n" "$tool"
        _c "$COLOR_RESET"
      fi
    fi
  done
  echo
}

# Entry: dispatch on the first POSITIONAL.
sama_main() {
  parse_flags "$@"
  if [ "${#POSITIONAL[@]}" -eq 0 ]; then
    print_usage
    exit 0
  fi
  local subcmd="${POSITIONAL[0]}"
  case "$subcmd" in
    check)   cmd_check ;;
    graph)   cmd_graph ;;
    doctor)  cmd_doctor ;;
    help)    print_usage ;;
    *)       echo "unknown command: $subcmd" >&2; print_usage >&2; exit 2 ;;
  esac
}