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

c14_graph.sh 100 lines · 2826 bytes raw
# c14 — adapter: import-graph rendering. Walks ALL_FILES,
# resolves their import edges via collect_imports, emits a graphviz
# .dot file, and (if `dot` is on PATH) renders a PNG. The boundary
# call to `dot` happens here, so this file is correctly Layer 2 —
# its prefix declares it as such, and the §4.4 Modeled (boundary)
# rule permits filesystem + tool boundaries in Layer 2.

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

# Emit a .dot graph of the import topology to stdout.
#
# Node attributes are layer-coloured so the rendered PNG makes the
# layer stratification visible. Edges flow from importer → imported
# (matching the natural reading direction); the .dot `rankdir=LR`
# puts Layer 0 on the left and Layer 3 on the right.
emit_dot() {
  local repo_root="$1"
  local src_dir="$2"

  echo "digraph sama {"
  echo "  rankdir = LR;"
  echo "  node [shape=box, style=\"rounded,filled\", fontname=\"Helvetica\"];"
  echo "  graph [splines=ortho, nodesep=0.4];"

  local color_l0="#cfe8d4"
  local color_l1="#cfd8e8"
  local color_l2="#e8e0c4"
  local color_l3="#e8c4c4"

  # Nodes
  local f
  for f in "${ALL_FILES[@]}"; do
    if is_sama_file "$f" || is_test_file "$f"; then
      :
    else
      continue
    fi
    local label="${f##*/}"
    local info
    local layer="?"
    local color="#dddddd"
    if info="$(declared_layer "$f")"; then
      layer="$(echo "$info" | awk '{print $1}')"
      case "$layer" in
        0) color="$color_l0" ;;
        1) color="$color_l1" ;;
        2) color="$color_l2" ;;
        3) color="$color_l3" ;;
      esac
    fi
    echo "  \"$f\" [label=\"$label\\nL$layer\", fillcolor=\"$color\"];"
  done

  # Edges
  for f in "${ALL_FILES[@]}"; do
    if is_sama_file "$f" || is_test_file "$f"; then
      :
    else
      continue
    fi
    local imp
    while IFS= read -r imp; do
      [ -z "$imp" ] && continue
      # Only emit edges into files we actually loaded.
      local found=0
      local x
      for x in "${ALL_FILES[@]}"; do
        if [ "$x" = "$imp" ]; then found=1; break; fi
      done
      [ "$found" = "1" ] || continue
      echo "  \"$f\" -> \"$imp\";"
    done < <(collect_imports "$f" "$repo_root")
  done

  echo "}"
}

# Wrapper: write .dot to a path; render PNG if dot is available.
render_graph() {
  local repo_root="$1"
  local src_dir="$2"
  local out_dot="${3:-/tmp/sama-graph.dot}"
  local out_png="${4:-/tmp/sama-graph.png}"

  emit_dot "$repo_root" "$src_dir" > "$out_dot"
  echo "Wrote: $out_dot"

  if command -v dot > /dev/null 2>&1; then
    if dot -Tpng "$out_dot" -o "$out_png" 2>/dev/null; then
      echo "Wrote: $out_png"
      return 0
    fi
    echo "warning: \`dot\` failed to render — keeping .dot only" >&2
    return 0
  fi
  echo "info: \`dot\` (graphviz) not installed — wrote .dot only" >&2
  return 0
}