#!/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 `) 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 }