<# .SYNOPSIS src/main/java의 추가·수정 소스에 한글 Javadoc이 붙어 있는지 확인하는 PostToolUse hook. .DESCRIPTION `.agents/skills/verify-mcp-server-change/SKILL.md`의 "필수 한글 소스 주석" 규칙을 지침이 아니라 결정적 검사로 강제한다. 최상위 type 선언과 indent 4의 method/constructor 선언 바로 위에 Javadoc(`*/`로 끝나는 블록)이 있는지만 본다. 파일을 고치지 않으며 build에도 관여하지 않는다. Hook 모드: Claude Code가 stdin으로 넘긴 JSON에서 tool_input.file_path를 읽는다. 누락이 있으면 stderr로 알리고 exit 2로 Claude에게 되돌린다. 수동 모드: -Path 로 파일 또는 디렉터리를 직접 검사한다. 예) pwsh .claude/hooks/check-javadoc.ps1 -Path src/main/java #> param( [string]$Path ) $ErrorActionPreference = 'Stop' function Get-MissingJavadoc { param([string]$File) $lines = Get-Content -LiteralPath $File -Encoding utf8 $findings = @() for ($i = 0; $i -lt $lines.Count; $i++) { $line = $lines[$i] if ([string]::IsNullOrWhiteSpace($line)) { continue } $kind = $null # 최상위 type 선언 (indent 0). 중첩 type은 skill 규칙의 강제 대상이 아니다. if ($line -match '^(?:(?:public|final|abstract|sealed|non-sealed)\s+)*(class|interface|record|enum)\s+\w') { $kind = 'type' } # 정확히 indent 4인 선언만 본다. 8칸 이상은 method 본문이나 이어지는 인자 목록이다. elseif ($line -match '^ {4}\S') { $body = $line.Substring(4) # 중첩 type 선언은 건너뛴다. if ($body -match '^(?:(?:public|protected|private|static|final|abstract|sealed|non-sealed)\s+)*(class|interface|record|enum)\s+\w') { continue } # compact record constructor: `public Protocol {` if ($body -match '^(public|protected|private)\s+\w+\s*\{\s*$') { $kind = 'method' } # method/constructor: `(` 앞에 토큰이 둘 이상이라 enum 상수(`NAME(...)`)와 구분된다. elseif ($body -match '^([^(){};=]*\S)\s*\(' -and $matches[1] -match '\s' -and ($body -match '\{\s*$' -or $body -match ';\s*$' -or $body -match '\(\s*$')) { $kind = 'method' } } if (-not $kind) { continue } # 선언 위로 올라가며 Javadoc 블록의 끝(`*/`)을 찾는다. # 빈 줄, annotation, 그리고 선언보다 깊게 들여쓴 줄(여러 줄 annotation의 이어지는 인자)은 건너뛴다. $declIndent = $line.Length - $line.TrimStart(' ').Length $hasJavadoc = $false $j = $i - 1 while ($j -ge 0) { $prev = $lines[$j] $trimmed = $prev.Trim() if ($trimmed -match '\*/$') { $hasJavadoc = $true; break } $prevIndent = $prev.Length - $prev.TrimStart(' ').Length if ($trimmed -eq '' -or $trimmed -match '^@\w' -or $prevIndent -gt $declIndent) { $j--; continue } break } if (-not $hasJavadoc) { $findings += [pscustomobject]@{ File = $File Line = $i + 1 Kind = $kind Text = $line.Trim() } } } return $findings } function Test-Target { param([string]$Candidate) if ([string]::IsNullOrWhiteSpace($Candidate)) { return $false } if ($Candidate -notmatch '\.java$') { return $false } return ($Candidate -replace '\\', '/') -match 'src/main/java/' } # --- 대상 수집 --- $targets = @() if ($Path) { if (Test-Path -LiteralPath $Path -PathType Container) { $targets = Get-ChildItem -LiteralPath $Path -Recurse -Filter *.java | ForEach-Object { $_.FullName } | Where-Object { Test-Target $_ } } elseif (Test-Target $Path) { $targets = @($Path) } } else { try { $raw = [Console]::In.ReadToEnd() if ([string]::IsNullOrWhiteSpace($raw)) { exit 0 } $event = $raw | ConvertFrom-Json $file = $event.tool_input.file_path if (Test-Target $file) { $targets = @($file) } } catch { # Hook은 편집 흐름을 막지 않는다. 입력을 못 읽으면 조용히 통과시킨다. exit 0 } } if ($targets.Count -eq 0) { exit 0 } $all = @() foreach ($t in $targets) { if (Test-Path -LiteralPath $t) { $all += Get-MissingJavadoc -File $t } } if ($all.Count -eq 0) { if ($Path) { Write-Host "javadoc ok: $($targets.Count) file(s)" } exit 0 } $report = ($all | ForEach-Object { " {0}:{1} [{2}] {3}" -f (Resolve-Path -LiteralPath $_.File -Relative), $_.Line, $_.Kind, $_.Text }) -join "`n" [Console]::Error.WriteLine( "한글 Javadoc 누락 ($($all.Count)건). " + ".agents/skills/verify-mcp-server-change/SKILL.md의 '필수 한글 소스 주석' 규칙에 따라 " + "역할·처리 단계·협력 객체를 설명하는 주석을 선언 바로 위에 추가하세요.`n$report") exit 2