diff --git a/.gitignore b/.gitignore
index 01692ee..cd7da9a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,6 @@ dev-only/
# Internal working handoff (not a public artifact)
HANDOFF.md
.gstack/
+
+# 대회 주최측이 배포한 오리엔테이션 자료 — 제3자 저작물이라 재배포하지 않는다
+docs/contest/ot-2026/
diff --git a/scripts/check_assets_separation.py b/scripts/check_assets_separation.py
index 6672f76..009bcd3 100644
--- a/scripts/check_assets_separation.py
+++ b/scripts/check_assets_separation.py
@@ -15,6 +15,7 @@
from __future__ import annotations
import re
+import subprocess
from pathlib import Path
PICTOGRAM_EXTS = {".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp"}
@@ -40,12 +41,50 @@ def _iter_files(root: Path):
yield path
+def _gitignored(root: Path, paths: list[Path]) -> set[Path]:
+ """``paths`` 중 git 이 무시하는 것들의 집합.
+
+ 이 검사가 지키려는 것은 배포되는 트리의 라이선스 분리다. gitignore 된 파일은
+ 정의상 배포물에 들어가지 않으므로 유출일 수 없다 — 로컬 참고자료 스크린샷
+ 따위를 유출로 신고하면 거짓 경보가 나고 게이트가 무뎌진다.
+
+ git 이 없거나 저장소가 아니면 **빈 집합**을 돌려준다. 즉 아무것도 면제되지
+ 않고 예전처럼 전부 검사한다 — 폴백이 게이트를 끄면 안 된다.
+ """
+ if not paths:
+ return set()
+ try:
+ # -z 는 필수다. 이것 없이는 git 이 비ASCII 경로를 따옴표로 감싸고
+ # 8진 이스케이프해서 내보내(예: "docs/\354\235\274\354\240\225.png")
+ # 원래 경로와 대조가 되지 않는다 — 한글 경로가 조용히 전부 어긋난다.
+ # -z 를 주면 입출력 모두 NUL 구분이 되고, 공백·개행이 든 경로도 안전하다.
+ proc = subprocess.run(
+ ["git", "-C", str(root), "check-ignore", "-z", "--stdin"],
+ input="\0".join(str(p) for p in paths),
+ capture_output=True,
+ text=True,
+ timeout=60,
+ )
+ except (OSError, subprocess.SubprocessError):
+ return set()
+ # 0 = 일부가 무시됨, 1 = 무시되는 것 없음. 그 밖(128 = git 저장소 아님 등)은
+ # 판단 불가로 보고 아무것도 면제하지 않는다.
+ if proc.returncode not in (0, 1):
+ return set()
+ return {Path(entry) for entry in proc.stdout.split("\0") if entry}
+
+
def find_asset_leaks(repo_root: Path | str) -> list[str]:
root = Path(repo_root).resolve()
assets_dir = root / "assets"
leaks: list[str] = []
- for path in _iter_files(root):
+ candidates = list(_iter_files(root))
+ ignored = _gitignored(root, candidates)
+
+ for path in candidates:
+ if path in ignored:
+ continue
rel = path.relative_to(root)
in_assets = assets_dir in path.parents
diff --git a/tests/test_assets_separation.py b/tests/test_assets_separation.py
index 7bbaf06..c10c45e 100644
--- a/tests/test_assets_separation.py
+++ b/tests/test_assets_separation.py
@@ -1,10 +1,25 @@
+import subprocess
from pathlib import Path
+import pytest
+
from scripts.check_assets_separation import find_asset_leaks
ROOT = Path(__file__).resolve().parent.parent
+def _git_repo(tmp_path: Path, gitignore: str) -> Path:
+ """`.gitignore` 를 가진 최소 git 저장소를 만든다. git 이 없으면 skip."""
+ try:
+ subprocess.run(["git", "init", "-q", str(tmp_path)], check=True,
+ capture_output=True, timeout=30)
+ except (OSError, subprocess.SubprocessError) as exc: # pragma: no cover
+ pytest.skip(f"git unavailable: {exc}")
+ (tmp_path / ".gitignore").write_text(gitignore, encoding="utf-8")
+ (tmp_path / "assets").mkdir(exist_ok=True)
+ return tmp_path
+
+
def test_assets_dir_exists_with_readme():
assert (ROOT / "assets").is_dir()
readme = (ROOT / "assets" / "README.md").read_text(encoding="utf-8")
@@ -108,3 +123,63 @@ def test_skip_dirs_use_relative_parts(tmp_path):
assert not any("output.svg" in p for p in leaks)
# scripts/logo.png MUST appear
assert any("logo.png" in p for p in leaks)
+
+
+# ---------------------------------------------------------------------------
+# gitignore 존중 (2026-07-29)
+#
+# 이 스캐너가 지키려는 것은 "배포되는 트리에서 CC BY-SA 픽토그램과 Apache 코드가
+# 섞이지 않는가" 다. gitignore 된 파일은 정의상 배포물에 들어가지 않는다 —
+# 참고자료 스크린샷, 내려받은 자료 같은 로컬 작업 파일까지 유출로 신고하면
+# 거짓 경보가 나고, 진짜 유출과 구분이 안 되어 게이트가 무뎌진다.
+# ---------------------------------------------------------------------------
+
+
+def test_gitignored_pictogram_is_not_a_leak(tmp_path):
+ """gitignore 된 이미지는 배포되지 않으므로 유출이 아니다."""
+ repo = _git_repo(tmp_path, "docs/scratch/\n")
+ (repo / "docs" / "scratch").mkdir(parents=True)
+ (repo / "docs" / "scratch" / "reference-shot.png").write_bytes(b"\x89PNG\r\n")
+ assert find_asset_leaks(repo) == []
+
+
+def test_tracked_pictogram_outside_assets_is_still_a_leak(tmp_path):
+ """gitignore 되지 않은 이미지는 여전히 유출이다 — 게이트를 무디게 하지 않는다."""
+ repo = _git_repo(tmp_path, "docs/scratch/\n")
+ (repo / "ttobak").mkdir()
+ (repo / "ttobak" / "stray_glyph.svg").write_text("", encoding="utf-8")
+ leaks = find_asset_leaks(repo)
+ assert any("stray_glyph.svg" in p for p in leaks)
+
+
+def test_gitignored_data_uri_is_not_a_leak(tmp_path):
+ """규칙 2(base64 인라인)도 같은 기준을 따른다."""
+ repo = _git_repo(tmp_path, "ttobak/generated/\n")
+ (repo / "ttobak" / "generated").mkdir(parents=True)
+ (repo / "ttobak" / "generated" / "out.html").write_text(
+ '
\n', encoding="utf-8"
+ )
+ assert find_asset_leaks(repo) == []
+
+
+def test_non_git_directory_still_scans_everything(tmp_path):
+ """git 저장소가 아니면 예전처럼 전부 검사한다(폴백이 게이트를 끄면 안 된다)."""
+ (tmp_path / "assets").mkdir()
+ (tmp_path / "docs").mkdir()
+ (tmp_path / "docs" / "shot.png").write_bytes(b"\x89PNG\r\n")
+ leaks = find_asset_leaks(tmp_path)
+ assert any("shot.png" in p for p in leaks)
+
+
+def test_gitignored_non_ascii_path_is_matched(tmp_path):
+ """한글 등 비ASCII 경로도 정확히 대조된다.
+
+ `git check-ignore` 는 -z 없이 실행하면 비ASCII 경로를 따옴표로 감싸고 8진
+ 이스케이프해서 출력한다("docs/\\354\\235\\274...png"). 그러면 원래 경로와
+ 매칭에 실패해 무시 설정이 조용히 통째로 먹히지 않는다 — 한글 경로가 많은
+ 이 저장소에서는 사실상 기능이 없는 것과 같다.
+ """
+ repo = _git_repo(tmp_path, "문서/자료/\n")
+ (repo / "문서" / "자료").mkdir(parents=True)
+ (repo / "문서" / "자료" / "배점표.png").write_bytes(b"\x89PNG\r\n")
+ assert find_asset_leaks(repo) == []