#!/usr/bin/env bash # PostToolUse hook: checks a single edited file for: # 1. Unfinished markers (TODO/FIXME/skeleton/empty fn) # 2. Integration chain gaps (based on file type) # Runs after Write/Edit operations only on the modified file. set -euo pipefail PROJECT_DIR="${AGENT_PROJECT_DIR:-$(pwd)}" # Suppress during brainstorming / exploratory sessions if [ -f "${PROJECT_DIR}/.agents/.suppress-hooks" ]; then exit 0 fi f="${AGENT_EDITED_FILE:-$AGENT_FILE}" if [ ! -f "$f" ]; then exit 0 fi issues=() ext="${f##*.}" basename=$(basename "$f") # ============================================================ # PART 1: Unfinished markers scan # ============================================================ if [[ "$ext" =~ ^(ts|tsx|js|jsx|vue)$ ]]; then while IFS=: read -r line_no content; do [ -z "$line_no" ] && continue trimmed=$(echo "$content" | sed 's/^[[:space:]]*//') if echo "$trimmed" | grep -qE '^(TODO|FIXME|HACK|XXX):'; then issues+=(" L${line_no}: TODO/FIXME ${trimmed:0:100}") elif echo "$trimmed" | grep -qi 'not implemented' && echo "$trimmed" | grep -qi 'throw'; then issues+=(" L${line_no}: [SKELETON] ${trimmed:0:100}") elif echo "$trimmed" | grep -qE 'function [a-zA-Z_][a-zA-Z0-9_]*\s*\(\s*\)\s*\{\s*\}'; then if ! echo "$trimmed" | grep -q 'return'; then issues+=(" L${line_no}: [EMPTY FN] ${trimmed:0:100}") fi elif echo "$trimmed" | grep -qE 'const [a-zA-Z_][a-zA-Z0-9_]*\s*=\s*\(\)\s*=>\s*\{\s*\}'; then issues+=(" L${line_no}: [EMPTY ARROW FN] ${trimmed:0:100}") fi done < <(grep -nE 'TODO:|FIXME:|HACK:|XXX:|not implemented|function [a-zA-Z_][a-zA-Z0-9_]*\s*\(\s*\)\s*\{\s*\}|const [a-zA-Z_][a-zA-Z0-9_]*\s*=\s*\(\)\s*=>\s*\{\s*\}' "$f" 2>/dev/null || true) elif [[ "$ext" == "java" ]]; then while IFS=: read -r line_no content; do [ -z "$line_no" ] && continue trimmed=$(echo "$content" | sed 's/^[[:space:]]*//') if echo "$trimmed" | grep -qE '^(TODO|FIXME|HACK|XXX):'; then issues+=(" L${line_no}: TODO/FIXME ${trimmed:0:100}") elif echo "$trimmed" | grep -qE 'UnsupportedOperationException'; then issues+=(" L${line_no}: [SKELETON] UnsupportedOperationException") fi done < <(grep -nE 'TODO:|FIXME:|HACK:|XXX:|UnsupportedOperationException' "$f" 2>/dev/null || true) fi # ============================================================ # PART 2: Integration chain gap detection # ============================================================ # Check 1: Java Controller 变更 → 提取映射路径,检查 Web/UniApp 请求层是否有引用 if [[ "$f" == *"gym-manage-api/"*.java ]] && grep -q '@RestController\|@Controller' "$f" 2>/dev/null; then # 提取 @RequestMapping/@GetMapping/@PostMapping/@PutMapping/@DeleteMapping 的路径 paths=$(grep -oE '@(Request|Get|Post|Put|Delete)Mapping\([^)]*' "$f" 2>/dev/null \ | grep -oE '"/[^"]+"' | tr -d '"' | sort -u || true) if [ -n "$paths" ]; then # 与类级 @RequestMapping 前缀合并 prefix=$(grep -oE '@RequestMapping\([^)]*' "$f" 2>/dev/null | grep -oE '"/[^"]+"' | tr -d '"' | head -1 || true) while IFS= read -r p; do [ -z "$p" ] && continue # 已含 /api 绝对路径的不再加类级前缀,避免双写 case "$p" in /api/*) full="$p" ;; *) full="${prefix}${p}" ;; esac # 去掉 Spring 模板变量({id} 等)用于模糊匹配 key=$(echo "$full" | sed 's/{[^}]*}//g') [ -z "$key" ] && continue hits=$(grep -rl -- "$key" "${PROJECT_DIR}/gym-manage-web/src/api" "${PROJECT_DIR}/gym-manage-uniapp/api" "${PROJECT_DIR}/gym-manage-coach-uniapp/api" 2>/dev/null | wc -l | tr -d ' ' || true) if [ "$hits" -eq 0 ]; then issues+=(" [GAP] 接口 '${full}' 在 Web/UniApp 请求层无引用(gym-manage-web/src/api、uniapp/api、coach-uniapp/api)") fi done <<< "$paths" fi fi # Check 2: Web API 请求层变更 → 检查页面/store 有引用 if [[ "$f" == *"gym-manage-web/src/api/"*".api.ts" ]]; then new_fns=$(grep -oE '^export (async )?function [a-zA-Z_][a-zA-Z0-9_]*|^export const [a-zA-Z_][a-zA-Z0-9_]*\s*=' "$f" 2>/dev/null \ | sed -E 's/^export (async )?function //; s/^export const //; s/[[:space:]]*=.*//' | sort -u || true) if [ -n "$new_fns" ]; then while IFS= read -r fn; do [ -z "$fn" ] && continue hits=$(grep -rl -- "\b${fn}\b" "${PROJECT_DIR}/gym-manage-web/src/views" "${PROJECT_DIR}/gym-manage-web/src/stores" "${PROJECT_DIR}/gym-manage-web/src/components" 2>/dev/null | wc -l | tr -d ' ' || true) if [ "$hits" -eq 0 ]; then issues+=(" [GAP] Web API '${fn}' 在 src/views|stores|components 无调用方") fi done <<< "$new_fns" fi fi # Check 3: UniApp 请求层变更 → 检查 pages 有引用 if [[ "$f" == *"gym-manage-uniapp/api/"*.js ]] || [[ "$f" == *"gym-manage-coach-uniapp/api/"*.js ]]; then uniapp_root="gym-manage-uniapp" [[ "$f" == *"coach-uniapp"* ]] && uniapp_root="gym-manage-coach-uniapp" new_fns=$(grep -oE '^export (async )?function [a-zA-Z_][a-zA-Z0-9_]*|^export const [a-zA-Z_][a-zA-Z0-9_]*\s*=' "$f" 2>/dev/null \ | sed -E 's/^export (async )?function //; s/^export const //; s/[[:space:]]*=.*//' | sort -u || true) if [ -n "$new_fns" ]; then while IFS= read -r fn; do [ -z "$fn" ] && continue hits=$(grep -rl -- "${fn}" "${PROJECT_DIR}/${uniapp_root}/pages" 2>/dev/null | wc -l | tr -d ' ' || true) if [ "$hits" -eq 0 ]; then issues+=(" [GAP] ${uniapp_root} API '${fn}' 在 pages/ 无调用方") fi done <<< "$new_fns" fi fi # ============================================================ # REPORT # ============================================================ if [ ${#issues[@]} -gt 0 ]; then echo "" echo "[COMPLETENESS] $(basename "$f") 检查发现 ${#issues[@]} 个问题:" for issue in "${issues[@]:0:20}"; do echo "$issue" done if [ ${#issues[@]} -gt 20 ]; then echo " ... 及其他 $((${#issues[@]} - 20)) 处" fi echo "" fi