Skip to content

feat: 무인자 비교 본인 고정+랜덤 선정 + /경험치 대상 지정 (ADR-0008 개정) - #58

Merged
mindongdong merged 3 commits into
mainfrom
feat/random-fanout
Jul 4, 2026
Merged

feat: 무인자 비교 본인 고정+랜덤 선정 + /경험치 대상 지정 (ADR-0008 개정)#58
mindongdong merged 3 commits into
mainfrom
feat/random-fanout

Conversation

@mindongdong

@mindongdong mindongdong commented Jul 4, 2026

Copy link
Copy Markdown
Owner

요약

그릴링 확정(2026-07-04, docs/random-fanout-work-order.md) 봇 PR1. 튜토리얼의 "대표 레벨 상위 최대 10명" 규칙이 상위권만 반복 노출되는 불공평을 해소한다.

1. 무인자 선정 규칙 개정 — ADR-0008 부분개정

  • 대상: /아이템·/유니온(등록자 풀), /스타포스·/잠재(키 등록자 풀). 상한 K=10 유지.
  • cap_by_level(레벨 상위) → select_with_self(본인 무조건 포함 + 나머지 random.sample 비복원). 매 실행 독립, 테스트만 rng 주입.
  • 본인이 풀에 없으면 조용히 랜덤 10명 + 사유별 안내 한 줄(캐릭터 미등록 → /캐릭터등록 · 키 미등록 → /키등록 · 챌린저스 캐릭 없음).
  • 대상 지정(최대 5명) 실행은 종전대로 상한·랜덤·푸터 미적용.

2. /경험치 — 랜덤 미적용, 대상 지정만 추가

  • 무인자·매일 10시 발송·DM 구독은 레벨 Top-10 기조 그대로 (requested_users=None 기본값으로 payload 불변).
  • 유저1~유저5 슬롯 추가: 지정 시 대표 캐릭터만 순위판+그래프, 미등록·데이터 없음은 제외 + "N명은 미등록/데이터 없음" 한 줄, 전원 불가 시 안내만. 정렬 (레벨, exp%) 유지.

3. 표시 정렬 — 명령별 핵심 지표

  • /아이템: 본인 맨 앞(대상에 있을 때만, _self_first) + 나머지 랜덤/입력 순서.
  • /유니온 유니온레벨↓ · /스타포스 운빨↑% · /잠재 메소↓ · /경험치 (레벨,exp%)는 기존 구현이 이미 충족 — 코드 무변경, ADR에 명시.

4. 무인자 푸터 상시화

  • N≤10: "등록자 전원(N명)을 비교했어요" / N>10: "등록자 N명 중 본인 포함 랜덤 10명을 비교했어요 · 특정 인원은 대상 지정(최대 5명)으로 볼 수 있어요" (스타포스·잠재는 "키 등록자").

5. 카피 통일

  • 4개 명령 + /경험치 description을 작업지시서 문구로 갱신.

검증

  • uv run pytest -q824 passed, 1 deselected (베이스라인 815 + 순증 9)
  • uv run ruff check . / ruff format --check . — clean
  • 코드리뷰 패스(리뷰어 레인) APPROVE — CRITICAL/HIGH/MEDIUM 0건
  • 테스트 랜덤 결정성: rng 시드 주입·monkeypatch, 비복원·본인포함·문구분기 실검증
  • 배포 후 실 디스코드 무인자 1회 + 대상 지정 1회 눈확인

후속

사이트 PR2(튜토리얼 10번 화면·TutorialDemos·commands.json)가 뒤따름 — Vercel 자동배포라 봇 배포와 순서 맞춰 머지.

Summary by CodeRabbit

  • New Features

    • Added support for comparing selected users in /경험치.
    • Updated unspecified comparisons to always include the requester and fill remaining slots randomly up to 10.
    • Footer messaging now always appears and explains when users are omitted or when results are random.
  • Bug Fixes

    • Refined result ordering so the requester is shown first when included.
    • Improved handling for unregistered or unavailable users in comparison and leaderboard views.
  • Documentation

    • Updated command guidance and comparison rules to match the new behavior.

- cap_by_level → select_with_self: 실행 본인 무조건 포함 + 나머지 random.sample 비복원
- 본인 풀 부재 시 조용히 랜덤 10명 + 사유별 안내(미등록/키 미등록/챌 캐릭 없음)
- 무인자 푸터 상시화: N≤10 전원(N명) / N>10 본인 포함 랜덤 10명 문구 분기
- /아이템 본인 맨 앞 정렬(_self_first), 나머지 랜덤/입력 순서 유지
- /아이템·/유니온·/스타포스·/잠재 description 카피 통일
- 지정 시 해당 유저 대표 캐릭터만 순위판+그래프(build_specified_payload)
- 미등록·데이터 없는 지정 유저 제외 + 'N명은 미등록/데이터 없음' 안내, 전원 불가 시 안내만
- 무인자·매일 10시 발송·DM 구독 payload 불변(requested_users=None 기본값으로 무영향)
- description 카피: 대상 지정 시 최대 5명만 비교 명시
@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
maple-mate Ready Ready Preview, Comment Jul 4, 2026 5:13am

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces the deterministic level-top-K target selection for unspecified comparisons with a self-included random selection helper (select_with_self), wires it into item, starforce, potential, and union commands, adds targeted-user comparison support to the experience leaderboard command, and updates ADR documentation and tests accordingly.

Changes

Random fanout and targeted leaderboard

Layer / File(s) Summary
Core selection helper
maple_mate/bot/comparison.py
cap_by_level is replaced with select_with_self, which always includes the executor and randomly samples the remainder; fanout_note now always returns footer text with branching wording.
/아이템 wiring
maple_mate/character/commands.py
Adds _self_first ordering helper, switches to select_with_self, adds self_note/note footer composition, and updates description strings.
/스타포스 wiring
maple_mate/history/commands.py
Switches unspecified selection to select_with_self, adds self_note handling and footer updates, and updates description strings.
/잠재 wiring
maple_mate/history/potential_commands.py
Switches unspecified selection to select_with_self, hides key-unregistered rows, adds self_note handling and footer updates, and updates description strings.
/유니온 wiring
maple_mate/union/commands.py
Switches unspecified selection to select_with_self, adds self_note handling and footer updates, and updates description strings.
/경험치 targeted comparison
maple_mate/leaderboard/broadcast.py, maple_mate/leaderboard/commands.py
Adds build_specified_payload and exclusion-note support to _build_embed/build_targets_payload; extends handle_leaderboard to accept members and adds member1-member5 command options.
Tests
tests/test_fanout_cap.py, tests/test_leaderboard_commands.py
Replaces cap_by_level tests with select_with_self/fanout_note tests, seeds deterministic RNG in handler tests, and adds tests for targeted /경험치 payloads.
Documentation
docs/adr/0008-unspecified-comparison-fanout-cap.md, docs/random-fanout-work-order.md
Updates ADR-0008 with a 2026-07-04 revision describing self-included random selection and always-shown footers; adds a new work-order document detailing the policy.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Handler
  participant select_with_self
  participant fanout_note
  User->>Handler: invoke comparison command (no members)
  Handler->>select_with_self: pass targets, self_id, cap
  select_with_self-->>Handler: selected targets, total, self_included
  Handler->>fanout_note: total, cap
  fanout_note-->>Handler: footer text
  Handler-->>User: embed with self_note/note footer
Loading
sequenceDiagram
  participant User
  participant handle_leaderboard
  participant build_specified_payload
  participant broadcast
  User->>handle_leaderboard: /경험치 member1..member5
  handle_leaderboard->>build_specified_payload: user_ids, realm
  build_specified_payload->>broadcast: build_targets_payload(requested_users)
  broadcast-->>build_specified_payload: payload or None
  build_specified_payload-->>handle_leaderboard: payload
  handle_leaderboard-->>User: comparison embed or _MSG_TARGETS_NONE
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: self-included random selection for unspecified comparisons and /경험치 target selection, with ADR-0008 revision context.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/random-fanout

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
maple_mate/bot/comparison.py (1)

178-201: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Selection logic matches the self-first + non-replacement random contract.

Verified against the test suite (test_select_self_first_and_random_rest, test_select_self_absent_fills_full_cap, test_select_random_is_deterministic_with_seed): self is fixed at the front when present, remainder is drawn via random.sample without replacement, and behavior is deterministic when an rng is injected.

One theoretical edge case worth a defensive thought: if targets ever contained more than one entry with the same self_id (duplicate rows for the same user), take = cap - len(self_targets) could underflow, and self_targets + chosen could exceed cap. This relies on the invariant that the resolved target pool has at most one row per discord_user_id, which appears to hold upstream — flagging only as a defensive note, not a live bug.

💡 Optional defensive guard
     self_targets = [t for t in targets if t.discord_user_id == self_id]
     others = [t for t in targets if t.discord_user_id != self_id]
     self_included = bool(self_targets)
-    take = cap - len(self_targets)
+    take = max(cap - len(self_targets), 0)
     chosen = picker.sample(others, take)
-    return self_targets + chosen, total, self_included
+    return (self_targets[:cap] if len(self_targets) > cap else self_targets) + chosen, total, self_included
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@maple_mate/bot/comparison.py` around lines 178 - 201, The selection contract
is correct, but add a defensive guard in select_with_self for duplicate entries
with the same discord_user_id. Ensure self_targets is deduplicated or capped so
that take cannot become negative and the returned list never exceeds cap, even
if targets contains multiple rows for self_id. Keep the existing self-first
behavior and random.sample logic intact, and update the select_with_self path to
handle this edge case safely.
maple_mate/character/commands.py (1)

380-393: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated note/self_note/footer wiring across 4 command handlers.

The select_with_selffanout_noteself_note → footer-prepend loop pattern here is repeated almost verbatim in handle_starforce (history/commands.py), handle_potential (potential_commands.py), and handle_union (union/commands.py), differing only in the noun/absent-message text. Consider extracting a shared helper in comparison.py, e.g. resolve_unspecified_selection(targets, self_id, *, absent_message, noun="등록자") -> (selected, note, self_note) and a prepend_notes(footer, *lines) helper, to avoid drift across the four call sites as this logic evolves.

♻️ Illustrative refactor sketch
# maple_mate/bot/comparison.py
def resolve_unspecified_selection(targets, self_id, *, absent_message, noun="등록자"):
    selected, total, self_included = select_with_self(targets, self_id)
    note = fanout_note(total, noun=noun)
    self_note = None if self_included else absent_message
    return selected, note, self_note

def prepend_notes(footer, *lines):
    for line in lines:
        if line:
            footer = f"{line}\n{footer}"
    return footer
-    note: str | None = None
-    self_note: str | None = None
-    if not members:
-        targets, total, self_included = comparison.select_with_self(
-            targets, interaction.user.id
-        )
-        note = comparison.fanout_note(total)
-        if not self_included:
-            self_note = (
-                "본인은 챌린저스 캐릭터가 없어 포함되지 않았어요."
-                if realm is Realm.CHALLENGERS
-                else "본인은 미등록이라 포함되지 않았어요. `/캐릭터등록` 부터 해주세요!"
-            )
+    note: str | None = None
+    self_note: str | None = None
+    if not members:
+        absent_message = (
+            "본인은 챌린저스 캐릭터가 없어 포함되지 않았어요."
+            if realm is Realm.CHALLENGERS
+            else "본인은 미등록이라 포함되지 않았어요. `/캐릭터등록` 부터 해주세요!"
+        )
+        targets, note, self_note = comparison.resolve_unspecified_selection(
+            targets, interaction.user.id, absent_message=absent_message
+        )

Also applies to: 405-411

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@maple_mate/character/commands.py` around lines 380 - 393, The self-selection
note/footer wiring in this handler is duplicated across the command handlers, so
extract the repeated select_with_self/fanout_note/self_note flow into a shared
helper in comparison.py and reuse it here. Update the handle flow to call a
helper like resolve_unspecified_selection for targets/self_included handling,
and centralize footer prepending so the noun and absent-message variations are
passed as arguments instead of reimplemented in each command.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@maple_mate/bot/comparison.py`:
- Around line 178-201: The selection contract is correct, but add a defensive
guard in select_with_self for duplicate entries with the same discord_user_id.
Ensure self_targets is deduplicated or capped so that take cannot become
negative and the returned list never exceeds cap, even if targets contains
multiple rows for self_id. Keep the existing self-first behavior and
random.sample logic intact, and update the select_with_self path to handle this
edge case safely.

In `@maple_mate/character/commands.py`:
- Around line 380-393: The self-selection note/footer wiring in this handler is
duplicated across the command handlers, so extract the repeated
select_with_self/fanout_note/self_note flow into a shared helper in
comparison.py and reuse it here. Update the handle flow to call a helper like
resolve_unspecified_selection for targets/self_included handling, and centralize
footer prepending so the noun and absent-message variations are passed as
arguments instead of reimplemented in each command.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91ce0c16-4a46-4f4b-9094-bce3b79e970e

📥 Commits

Reviewing files that changed from the base of the PR and between b178d4e and 6f526fc.

📒 Files selected for processing (11)
  • docs/adr/0008-unspecified-comparison-fanout-cap.md
  • docs/random-fanout-work-order.md
  • maple_mate/bot/comparison.py
  • maple_mate/character/commands.py
  • maple_mate/history/commands.py
  • maple_mate/history/potential_commands.py
  • maple_mate/leaderboard/broadcast.py
  • maple_mate/leaderboard/commands.py
  • maple_mate/union/commands.py
  • tests/test_fanout_cap.py
  • tests/test_leaderboard_commands.py

@mindongdong

mindongdong commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai 두 니트픽 검토 결과, 둘 다 의도적으로 미적용합니다.

1. select_with_self 중복 self 방어 가드 — 미적용. 대상 풀은 두 리졸버 모두 유저당 정확히 1행을 구조적으로 보장합니다: get_targetsby_user: dict[int, list[Character]] 키 순회로 유저당 Target 1개(registration/service.py), get_history_targets는 Registration 행(길드+유저 유니크) 단위 순회로 HistoryTarget 1개(history/service.py). 불변식이 깨지면 현재 코드는 random.sample의 ValueError로 크게 실패해 상류 버그를 즉시 드러내는 반면, max(…, 0) 가드는 그 위반을 조용히 삼킵니다. 프로젝트 원칙(불가능한 시나리오의 방어 코드 금지, fail fast)에 따라 현행 유지합니다.

2. 4개 핸들러 배선 헬퍼 추출 — 미적용. 드리프트가 위험한 실제 로직(선정 규칙·푸터 문구)은 이미 select_with_self/fanout_note로 comparison.py에 단일 소스화돼 있고, 핸들러에 남은 것은 사유 문구·noun·realm 분기가 제각각인 얇은 배선(각 ~8줄)입니다. 지금 추출하면 변형 파라미터 3개를 나르는 간접화 비용이 중복 제거 이득보다 큽니다. 배선이 더 자라면 그때 추출을 재검토하겠습니다.

@mindongdong
mindongdong merged commit 98cbcf7 into main Jul 4, 2026
6 checks passed
@mindongdong
mindongdong deleted the feat/random-fanout branch July 4, 2026 05:26
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@mindongdong 두 판단 모두 타당합니다.

  1. select_with_self 가드 미적용: get_targets(registration/service.py)와 get_history_targets(history/service.py)가 각각 유저당 정확히 1개의 Target/HistoryTarget만 생성하는 구조적 보장이 있고, 이 불변식이 깨지면 random.sampleValueError로 즉시 실패하는 것이 조용히 삼키는 가드보다 상류 버그 발견에 유리하다는 설명에 동의합니다. Fail-fast 원칙에 부합하는 선택입니다.

  2. 배선 헬퍼 추출 미적용: 선정 규칙과 푸터 문구라는 핵심 로직이 이미 select_with_self/fanout_note로 단일화되어 있고, 각 핸들러에 남은 것은 사유 문구·noun·realm 분기뿐이라 지금 추출하는 것이 오히려 간접화 비용을 늘린다는 점도 합리적입니다. 배선이 늘어나면 그때 재검토하신다는 방향도 좋습니다.

두 니트픽 모두 철회하겠습니다.

(=^・ω・^=)


✏️ Learnings added
Learnt from: mindongdong
URL: https://github.com/mindongdong/maple-mate/pull/58

Timestamp: 2026-07-04T05:26:50.144Z
Learning: Repository: maple-mate. Project convention (per maintainer mindongdong): avoid defensive code (e.g., `max(..., 0)` guards) for structurally impossible scenarios; prefer fail-fast behavior (e.g., letting `random.sample` raise `ValueError`) so upstream invariant violations are surfaced immediately rather than silently swallowed. Example: in `maple_mate/bot/comparison.py`'s `select_with_self`, the target pools from `get_targets` (maple_mate/registration/service.py, via `by_user: dict[int, list[Character]]` keyed iteration) and `get_history_targets` (maple_mate/history/service.py, via unique guild+user Registration rows) structurally guarantee exactly one Target/HistoryTarget per user, so no duplicate-self defensive guard is added.

Learnt from: mindongdong
URL: https://github.com/mindongdong/maple-mate/pull/58

Timestamp: 2026-07-04T05:26:50.144Z
Learning: Repository: maple-mate. Project convention (per maintainer mindongdong): avoid extracting shared "wiring" helpers across handlers (e.g., `/아이템`, `/유니온`, `/스타포스`, `/잠재` in maple_mate/character/commands.py, maple_mate/union/commands.py, maple_mate/history/commands.py, maple_mate/history/potential_commands.py) when the core reusable logic (selection rule via `select_with_self`, footer text via `fanout_note`) is already centralized in maple_mate/bot/comparison.py. The remaining per-handler code is thin (~8 lines) and varies by reason text, noun, and realm branching, so extracting a helper adds more indirection cost than it saves; revisit only if the wiring grows further.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

mindongdong added a commit that referenced this pull request Jul 4, 2026
- 튜토리얼 10번 화면 제목·본문: 공평성 기조(본인 항상 포함 + 랜덤) + /경험치 Top 10 예외 명시
- TutorialDemos TargetDemo 라벨 동기
- commands.json: 아이템·유니온·스타포스·잠재 요약문 갱신(스타포스·잠재 '키 등록자' 유지),
  경험치 tip에 대상 지정(최대 5명, 대표 캐릭터만) 추가
- 봇 PR #58(select_with_self·/경험치 대상 지정)과 짝, 드리프트 가드·빌드 그린
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@mindongdong, 두 판단 모두 타당합니다. get_targets(registration/service.py)와 get_history_targets(history/service.py)가 유저당 1행을 구조적으로 보장하고, 이 불변식이 깨지면 random.sampleValueError로 즉시 실패하는 편이 max(..., 0) 가드로 조용히 삼키는 것보다 프로젝트의 fail-fast 원칙에 부합한다는 설명에 동의합니다. 배선 추출 관련해서도, 이미 select_with_self/fanout_note로 핵심 로직이 단일화된 상태에서 얇은 배선까지 추출하면 간접화 비용이 더 크다는 판단이 합리적입니다. 배선이 늘어나면 그때 재검토하신다는 방향도 좋습니다.

(=^・ω・^=) ノ


✏️ Learnings added
Learnt from: mindongdong
URL: https://github.com/mindongdong/maple-mate/pull/58

Timestamp: 2026-07-04T05:26:57.781Z
Learning: In maple-mate, `registration/service.py`'s `get_targets` and `history/service.py`'s `get_history_targets` structurally guarantee exactly one row per user (via `by_user: dict[int, list[Character]]` key iteration and unique guild+user Registration row iteration, respectively). Because of this, `comparison.select_with_self` (maple_mate/bot/comparison.py) intentionally omits a duplicate-self defensive guard (e.g. `max(..., 0)`); if the invariant is ever violated, the code should fail fast via `random.sample`'s `ValueError` rather than silently swallowing the violation. This follows the project's fail-fast principle: avoid defensive code for scenarios considered structurally impossible.

Learnt from: mindongdong
URL: https://github.com/mindongdong/maple-mate/pull/58

Timestamp: 2026-07-04T05:26:57.781Z
Learning: In maple-mate, the wiring code in the four handlers (`maple_mate/character/commands.py`, `maple_mate/union/commands.py`, `maple_mate/history/commands.py`, `maple_mate/history/potential_commands.py`) that calls `comparison.select_with_self`/`comparison.fanout_note` and builds reason-specific `self_note` messages was intentionally not extracted into a shared helper, since each handler's wiring is thin (~8 lines) with handler-specific noun/realm/reason variations, and the core selection/footer logic is already centralized in `maple_mate/bot/comparison.py`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant