Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions .claude/rules/04-error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,19 @@ except FileNotFoundError:
class GraphLMError(Exception):
"""GraphLM 패키지의 모든 사용자 정의 예외 base."""


class DataLoadError(GraphLMError):
"""데이터 로드 실패."""


class ConfigError(GraphLMError):
"""잘못된 실험 설정."""


class ModelError(GraphLMError):
"""모델 정의/forward 단계 오류."""


class ReproducibilityError(GraphLMError):
"""시드 고정 실패 / 결정론 깨짐 감지."""
```
Expand All @@ -84,13 +88,13 @@ base 클래스가 있으면 호출자가 `except GraphLMError` 로 라이브러
```python
from tenacity import retry, stop_after_attempt, wait_exponential


@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
reraise=True,
)
def download_dataset(url: str) -> Path:
...
def download_dataset(url: str) -> Path: ...
```

학습 루프 안의 NaN/Inf 감지 같은 "재시도하면 의미 없는" 케이스는 retry 대신 즉시 fail.
Expand Down Expand Up @@ -121,6 +125,7 @@ import logging

logger = logging.getLogger(__name__)


def train_epoch(model, loader, optimizer) -> dict:
logger.info("epoch 시작", extra={"n_batches": len(loader)})
for batch_idx, batch in enumerate(loader):
Expand Down Expand Up @@ -149,12 +154,15 @@ def train_epoch(model, loader, optimizer) -> dict:

```python
# Good
logger.info("eval 완료", extra={
"split": "test",
"accuracy": 0.873,
"loss": 0.42,
"n_samples": 1024,
})
logger.info(
"eval 완료",
extra={
"split": "test",
"accuracy": 0.873,
"loss": 0.42,
"n_samples": 1024,
},
)

# Bad — 검색·집계 불가
logger.info(f"eval 완료: test split, acc=0.873, loss=0.42, n=1024")
Expand Down Expand Up @@ -202,9 +210,7 @@ def assert_deterministic(seed: int, run_fn) -> None:
set_seed(seed)
result_b = run_fn()
if not torch.allclose(result_a, result_b):
raise ReproducibilityError(
f"동일 시드 ({seed}) 에서 결과 불일치 — 결정론 깨짐"
)
raise ReproducibilityError(f"동일 시드 ({seed}) 에서 결과 불일치 — 결정론 깨짐")
```

CI 또는 디버깅 모드에서 활성화. 매 학습마다는 비용 과다.
Expand Down
8 changes: 4 additions & 4 deletions .claude/rules/05-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,18 +121,18 @@ def small_graph():

```python
@pytest.mark.slow
def test_full_epoch_runs():
...
def test_full_epoch_runs(): ...


@pytest.mark.gpu
def test_cuda_forward():
if not torch.cuda.is_available():
pytest.skip("CUDA 미사용")
...


@pytest.mark.network
def test_hf_hub_download():
...
def test_hf_hub_download(): ...
```

`pyproject.toml` 에 마커 등록:
Expand Down
24 changes: 14 additions & 10 deletions .claude/rules/06-code-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def normalize_features(
) -> Tensor:
return x / (x.norm(dim=-1, keepdim=True) + eps)


# Bad — 타입 없음
def normalize_features(x, eps=1e-6):
return x / (x.norm(dim=-1, keepdim=True) + eps)
Expand All @@ -70,8 +71,8 @@ def normalize_features(x, eps=1e-6):
```python
from typing import TypeAlias

NodeFeatures: TypeAlias = Tensor # shape: [n_nodes, feat_dim]
EdgeIndex: TypeAlias = Tensor # shape: [2, n_edges]
NodeFeatures: TypeAlias = Tensor # shape: [n_nodes, feat_dim]
EdgeIndex: TypeAlias = Tensor # shape: [2, n_edges]
GraphBatch: TypeAlias = dict[str, Tensor]
```

Expand All @@ -90,6 +91,7 @@ def process(item: Item) -> Result:
return item.cache
return compute(item)


# Bad — else 중첩
def process(item: Item) -> Result:
if item.valid:
Expand Down Expand Up @@ -143,8 +145,8 @@ class GraphEncoder(nn.Module):
super().__init__()
...

def forward(self, x: Tensor, edge_index: Tensor) -> Tensor:
...
def forward(self, x: Tensor, edge_index: Tensor) -> Tensor: ...


# Bad — 모델이 데이터 로드까지
class GraphEncoder(nn.Module):
Expand Down Expand Up @@ -287,6 +289,7 @@ def append_item(item, items=[]):
items.append(item)
return items


# Good
def append_item(item, items=None):
if items is None:
Expand Down Expand Up @@ -319,6 +322,7 @@ def is_valid(s: str) -> bool:
result = len(s) > 0
return result


# Good
def is_valid(s: str) -> bool:
return len(s) > 0
Expand Down Expand Up @@ -478,16 +482,16 @@ from graphlm.utils import set_seed
logger = logging.getLogger(__name__)
DEFAULT_HIDDEN_DIM = 128


# 5. Public API (클래스 / 함수)
class GraphEncoder(nn.Module):
...
class GraphEncoder(nn.Module): ...


def aggregate(x: Tensor, edge_index: Tensor) -> Tensor: ...

def aggregate(x: Tensor, edge_index: Tensor) -> Tensor:
...

# 6. Private helpers
def _normalize(x: Tensor) -> Tensor:
...
def _normalize(x: Tensor) -> Tensor: ...
```

## Code Review Checklist
Expand Down
41 changes: 27 additions & 14 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,12 @@ def process(item: Item) -> Result:

```python
# ❌ Magic numbers
if score > 0.85: ...
if score > 0.85:
...
# ✅ Named constants
ACCEPT_THRESHOLD = 0.85
if score > ACCEPT_THRESHOLD: ...
if score > ACCEPT_THRESHOLD:
...
```

```python
Expand All @@ -192,17 +194,21 @@ def append(item, items=None):

```python
# ❌ bare except
try: risky()
except: pass
try:
risky()
except:
pass
# ✅ 구체적 예외
try: risky()
try:
risky()
except (ValueError, RuntimeError) as err:
logger.warning("recovered", exc_info=err)
```

```python
# ❌ wildcard import
from graphlm.models import *

# ✅ 명시적 import
from graphlm.models import GraphEncoder
```
Expand Down Expand Up @@ -267,14 +273,18 @@ except FileNotFoundError as err:

```python
import logging

logger = logging.getLogger(__name__)

# Good — structured
logger.info("eval 완료", extra={
"split": "test",
"accuracy": 0.873,
"n_samples": 1024,
})
logger.info(
"eval 완료",
extra={
"split": "test",
"accuracy": 0.873,
"n_samples": 1024,
},
)

# Bad — 문자열 보간
logger.info(f"eval 완료: acc=0.873, n=1024")
Expand Down Expand Up @@ -307,10 +317,13 @@ def test_load_dataset_returns_expected_size(tmp_path):
### Parametrize

```python
@pytest.mark.parametrize("input_, expected", [
("http://example.com", "https://example.com"),
("https://www.example.com", "https://example.com"),
])
@pytest.mark.parametrize(
"input_, expected",
[
("http://example.com", "https://example.com"),
("https://www.example.com", "https://example.com"),
],
)
def test_normalize_url(input_, expected):
assert normalize_url(input_) == expected
```
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ jobs:
cache: pip

- name: Install ruff
run: pip install ruff
# uv.lock 의 ruff 버전과 맞춘다 — 버전이 다르면 로컬과 CI 의 포맷 판정이 어긋난다 (이슈 #86).
# ruff 를 올릴 때는 uv.lock · 이 줄 · 포맷 결과를 같은 PR 에서 함께 바꾼다.
run: pip install "ruff==0.17.0"

- name: Check ruff format
run: |
Expand Down
Loading