# b32 — logic: the seven SAMA v2 §4 conformance checks, implemented # as pure shell functions over an in-memory file list. Mirrors # src/b32_sama_v2_verify.ts. Each check populates the globals # CHECK_EXAMINED + CHECK_VIOLATIONS so the d21 dispatcher can render # verdicts uniformly. No I/O beyond reading the explicitly-listed # files; the file walker (list_repo_files) lives in b32_utils. # sama-import: a31_constants.sh # sama-import: b32_utils.sh # Output globals — reset by every run_check_* function. CHECK_EXAMINED=0 CHECK_VIOLATIONS=() # Cached SAMA_FILES + TEST_FILES — populated by collect_input. SAMA_FILES=() TEST_FILES=() ALL_FILES=() ALL_FILES_FULLPATHS=() REPO_ROOT="" SRC_DIR="" # — collect_input ------------------------------------------------- # Walks , populates SAMA_FILES / TEST_FILES / ALL_FILES. # Also records the full filesystem path in ALL_FILES_FULLPATHS so # checks can stat / cat files without re-resolving paths. collect_input() { REPO_ROOT="$1" SRC_DIR="$2" SAMA_FILES=() TEST_FILES=() ALL_FILES=() ALL_FILES_FULLPATHS=() local rel while IFS= read -r rel; do [ -z "$rel" ] && continue ALL_FILES+=("$rel") ALL_FILES_FULLPATHS+=("$REPO_ROOT/$rel") if is_sama_file "$rel"; then SAMA_FILES+=("$rel") elif is_test_file "$rel"; then TEST_FILES+=("$rel") fi done < <(list_repo_files "$REPO_ROOT" "$SRC_DIR") } # Resolve a relative ".ts" import like "./c14_git.ts" from a file's # directory to the repo-relative path (e.g. "src/c14_git.ts"). # Implemented in shell so the Law/Consistency checks share it. _resolve_import() { local from_path="$1" local imp="$2" local dir="${from_path%/*}" local rel="${imp#./}" echo "${dir}/${rel}" } # Returns 0 if is in ALL_FILES. _in_files() { local target="$1" local f for f in "${ALL_FILES[@]}"; do [ "$f" = "$target" ] && return 0 done return 1 } # — Check 1: Sorted ----------------------------------------------- # Every file carries a profile-recognised prefix; lex prefix order # equals layer order. run_check_sorted() { CHECK_EXAMINED=0 CHECK_VIOLATIONS=() local n=${#PROFILE_PREFIXES[@]} local i j for i in $(seq 0 $((n - 1))); do for j in $(seq 0 $((n - 1))); do [ "$i" = "$j" ] && continue local la="${PROFILE_LAYERS[$i]}" local pa="${PROFILE_PREFIXES[$i]}" local lb="${PROFILE_LAYERS[$j]}" local pb="${PROFILE_PREFIXES[$j]}" if [ "$la" -lt "$lb" ] && [ "$pa" \> "$pb" ]; then CHECK_VIOLATIONS+=("$pa::prefix \`$pa\` (layer $la) sorts after \`$pb\` (layer $lb) — lex order must equal layer order") fi done done local f for f in "${SAMA_FILES[@]}"; do CHECK_EXAMINED=$((CHECK_EXAMINED + 1)) if ! declared_layer "$f" > /dev/null; then CHECK_VIOLATIONS+=("$f::no profile-recognised prefix") fi done } # — Check 2: Architecture ----------------------------------------- run_check_architecture() { CHECK_EXAMINED=0 CHECK_VIOLATIONS=() local f for f in "${ALL_FILES[@]}"; do if is_sama_file "$f" || is_test_file "$f"; then : else continue fi CHECK_EXAMINED=$((CHECK_EXAMINED + 1)) local matches matches="$(all_prefix_matches "$f")" if [ -z "$matches" ]; then CHECK_VIOLATIONS+=("$f::unprefixed — does not match any profile prefix") continue fi local distinct_layers distinct_layers="$(echo "$matches" | awk '{print $1}' | sort -u | wc -l)" if [ "$distinct_layers" -gt 1 ]; then local detail detail="$(echo "$matches" | awk '{printf "%s→L%s, ", $2, $1}' | sed 's/, $//')" CHECK_VIOLATIONS+=("$f::ambiguous — matches multiple layers: $detail") fi done } # — Check 3: Modeled (tests) -------------------------------------- run_check_modeled_tests() { CHECK_EXAMINED=0 CHECK_VIOLATIONS=() local ext="$PROFILE_EXTENSION" local f for f in "${SAMA_FILES[@]}"; do local info info="$(declared_layer "$f")" || continue local layer layer="$(echo "$info" | awk '{print $1}')" if [ "$layer" != "1" ] && [ "$layer" != "2" ]; then continue fi CHECK_EXAMINED=$((CHECK_EXAMINED + 1)) # Sibling = same path with extension swapped to .test. local sibling="${f%${ext}}.test${ext}" if ! _in_files "$sibling"; then CHECK_VIOLATIONS+=("$f::no sibling test at \`$sibling\` — Layer $layer requires one") fi done } # — Check 4: Modeled (boundary) ----------------------------------- run_check_modeled_boundary() { CHECK_EXAMINED=0 CHECK_VIOLATIONS=() local f for f in "${SAMA_FILES[@]}"; do local info info="$(declared_layer "$f")" || continue local layer layer="$(echo "$info" | awk '{print $1}')" CHECK_EXAMINED=$((CHECK_EXAMINED + 1)) [ "$layer" = "2" ] && continue local matches matches="$(parse_boundary_matches "$REPO_ROOT/$f")" [ -z "$matches" ] && continue local pat while IFS= read -r pat; do [ -z "$pat" ] && continue CHECK_VIOLATIONS+=("$f::boundary pattern \`$pat\` found in Layer $layer — parsing belongs in Layer 2") done <<< "$matches" done } # — Check 5: Atomic ----------------------------------------------- run_check_atomic() { CHECK_EXAMINED=0 CHECK_VIOLATIONS=() local f for f in "${ALL_FILES[@]}"; do if is_sama_file "$f" || is_test_file "$f"; then : else continue fi CHECK_EXAMINED=$((CHECK_EXAMINED + 1)) local fullpath="$REPO_ROOT/$f" local lc lc="$(count_lines "$fullpath")" if [ "$lc" -gt "$MAX_LINES" ]; then CHECK_VIOLATIONS+=("$f::$lc lines (over the ${MAX_LINES}-line cap — split per UI/data domain)") fi if is_barrel "$fullpath"; then CHECK_VIOLATIONS+=("$f::barrel re-export file (all lines are \`export … from\`)") fi done } # — Check 6: The Law (§1.2) --------------------------------------- # Build adjacency, then for every edge A→B require layer(B) < # layer(A), or same layer with sublayer.index(B) <= sublayer.index(A). # Plus DFS cycle detection. run_check_law() { CHECK_EXAMINED=0 CHECK_VIOLATIONS=() # Adjacency: ADJ_KEYS holds file paths in encounter order; the # adjacency for ADJ_KEYS[i] is ADJ_VALUES[i] (space-separated). ADJ_KEYS=() ADJ_VALUES=() local f for f in "${ALL_FILES[@]}"; do if is_sama_file "$f" || is_test_file "$f"; then : else continue fi CHECK_EXAMINED=$((CHECK_EXAMINED + 1)) local out="" local imp while IFS= read -r imp; do [ -z "$imp" ] && continue # Only follow edges into files we actually loaded. if _in_files "$imp"; then out="$out $imp" fi done < <(collect_imports "$f" "$REPO_ROOT") ADJ_KEYS+=("$f") ADJ_VALUES+=("${out# }") done # Edge-by-edge check. local i for i in "${!ADJ_KEYS[@]}"; do local from="${ADJ_KEYS[$i]}" local outs="${ADJ_VALUES[$i]}" [ -z "$outs" ] && continue local a_info a_layer a_index a_info="$(declared_layer "$from")" || continue a_layer="$(echo "$a_info" | awk '{print $1}')" a_index="$(echo "$a_info" | awk '{print $3}')" local to for to in $outs; do local b_info b_layer b_index b_name b_info="$(declared_layer "$to")" || continue b_layer="$(echo "$b_info" | awk '{print $1}')" b_index="$(echo "$b_info" | awk '{print $3}')" b_name="$(echo "$b_info" | awk '{print $2}')" if [ "$b_layer" -lt "$a_layer" ]; then continue fi if [ "$b_layer" -gt "$a_layer" ]; then CHECK_VIOLATIONS+=("$from::imports \`$to\` — Layer $a_layer → Layer $b_layer (upward, breaks §1.2)") continue fi # Same layer: sublayer ordering — target must be earlier-or-equal. if [ "$b_index" -gt "$a_index" ]; then local a_name a_name="$(echo "$a_info" | awk '{print $2}')" CHECK_VIOLATIONS+=("$from::imports \`$to\` — same layer $a_layer but sublayer order reversed ($a_name index $a_index → $b_name index $b_index)") fi done done # DFS cycle detection on the same graph. _detect_cycles_law } # Cycle detector. Uses parallel arrays + a recursive bash function. # Emits violations directly into CHECK_VIOLATIONS. _detect_cycles_law() { CYCLE_COLOR_KEYS=() CYCLE_COLOR_VALS=() CYCLE_STACK=() local k for k in "${ADJ_KEYS[@]}"; do CYCLE_COLOR_KEYS+=("$k") CYCLE_COLOR_VALS+=("0") # 0=white, 1=gray, 2=black done local i for i in "${!CYCLE_COLOR_KEYS[@]}"; do if [ "${CYCLE_COLOR_VALS[$i]}" = "0" ]; then _dfs "${CYCLE_COLOR_KEYS[$i]}" || true fi done } _color_get() { local node="$1" local i for i in "${!CYCLE_COLOR_KEYS[@]}"; do if [ "${CYCLE_COLOR_KEYS[$i]}" = "$node" ]; then echo "${CYCLE_COLOR_VALS[$i]}" return 0 fi done echo "0" } _color_set() { local node="$1" local val="$2" local i for i in "${!CYCLE_COLOR_KEYS[@]}"; do if [ "${CYCLE_COLOR_KEYS[$i]}" = "$node" ]; then CYCLE_COLOR_VALS[$i]="$val" return 0 fi done CYCLE_COLOR_KEYS+=("$node") CYCLE_COLOR_VALS+=("$val") } _adj_for() { local node="$1" local i for i in "${!ADJ_KEYS[@]}"; do if [ "${ADJ_KEYS[$i]}" = "$node" ]; then echo "${ADJ_VALUES[$i]}" return 0 fi done } _dfs() { local node="$1" _color_set "$node" "1" CYCLE_STACK+=("$node") local outs outs="$(_adj_for "$node")" local next for next in $outs; do local c c="$(_color_get "$next")" if [ "$c" = "1" ]; then # Found a back-edge — extract cycle from stack. local found_idx=-1 local i for i in "${!CYCLE_STACK[@]}"; do if [ "${CYCLE_STACK[$i]}" = "$next" ]; then found_idx="$i" break fi done if [ "$found_idx" -ge 0 ]; then local cycle_str="" for i in $(seq "$found_idx" $((${#CYCLE_STACK[@]} - 1))); do if [ -z "$cycle_str" ]; then cycle_str="${CYCLE_STACK[$i]}" else cycle_str="$cycle_str → ${CYCLE_STACK[$i]}" fi done cycle_str="$cycle_str → $next" CHECK_VIOLATIONS+=("${CYCLE_STACK[$found_idx]}::import cycle: $cycle_str") fi _color_set "$node" "2" unset 'CYCLE_STACK[${#CYCLE_STACK[@]}-1]' return 1 fi if [ "$c" = "0" ]; then _dfs "$next" || true fi done _color_set "$node" "2" unset 'CYCLE_STACK[${#CYCLE_STACK[@]}-1]' return 0 } # — Check 7: Consistency (§3) ------------------------------------- # For each file: find the max layer of any in-tree import. If that # max > declared layer, the prefix lies. (Same-layer with bad # sublayer order is the Law's concern; Consistency only fires when # the layer ceiling itself is breached.) run_check_consistency() { CHECK_EXAMINED=0 CHECK_VIOLATIONS=() local f for f in "${SAMA_FILES[@]}"; do local a_info a_layer a_prefix a_info="$(declared_layer "$f")" || continue a_layer="$(echo "$a_info" | awk '{print $1}')" a_prefix="$(echo "$a_info" | awk '{print $4}')" CHECK_EXAMINED=$((CHECK_EXAMINED + 1)) local ceiling=-1 local ceiling_file="" local imp while IFS= read -r imp; do [ -z "$imp" ] && continue local b_info b_layer b_info="$(declared_layer "$imp")" || continue b_layer="$(echo "$b_info" | awk '{print $1}')" if [ "$b_layer" -gt "$ceiling" ]; then ceiling="$b_layer" ceiling_file="$imp" fi done < <(collect_imports "$f" "$REPO_ROOT") if [ "$ceiling" -gt "$a_layer" ]; then CHECK_VIOLATIONS+=("$f::declared Layer $a_layer (prefix \`$a_prefix\`) but imports reach Layer $ceiling via \`$ceiling_file\` — the prefix claims something the imports contradict") fi done }