Initial commit

This commit is contained in:
2026-08-05 14:20:01 +09:00
commit 71523c98e1
178 changed files with 12481 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
<#
.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