C3: Wrapper Anchor Penalty — Implementation Plan
C3: Wrapper Anchor Penalty — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Push anchor_ok from 3/4 to 4/4 by penalizing file-level wrapper anchors in choose_anchor.
Architecture: Add _is_wrapper_anchor detection helper + _WRAPPER_ANCHOR_PENALTY = 5 constant. Apply penalty inside choose_anchor’s scoring loop, after _anchor_quality_penalty. The wrapper detection uses token-set inclusion: single-token anchor whose token appears in all siblings.
Tech Stack: Python 3.12, unittest, uv run python -m pytest, vib bench --patch
File map:
| File | Responsibility |
|---|---|
vibelign/core/patch_suggester.py |
_is_wrapper_anchor helper + _WRAPPER_ANCHOR_PENALTY constant + choose_anchor modification |
tests/test_wrapper_anchor_penalty.py |
Unit tests for wrapper detection and scoring effect |
tests/test_patch_accuracy_scenarios.py |
Tighten add_email_domain_check anchor assertions |
tests/benchmark/patch_accuracy_baseline.json |
Updated via CLI |
Task 1: Write failing unit tests (TDD red)
Files:
-
Create:
tests/test_wrapper_anchor_penalty.py -
Step 1:
_is_wrapper_anchordetection tests
"""Unit tests for wrapper anchor detection and penalty (C3).
Covers _is_wrapper_anchor detection logic and the scoring effect
of _WRAPPER_ANCHOR_PENALTY in choose_anchor.
"""
import unittest
class IsWrapperAnchorTest(unittest.TestCase):
def test_single_token_prefix_of_all_siblings_is_wrapper(self):
from vibelign.core.patch_suggester import _is_wrapper_anchor
anchors = ["SIGNUP", "SIGNUP_RENDER_SIGNUP_FORM", "SIGNUP_HANDLE_SIGNUP"]
self.assertTrue(_is_wrapper_anchor("SIGNUP", anchors))
def test_multi_token_anchor_is_not_wrapper(self):
from vibelign.core.patch_suggester import _is_wrapper_anchor
anchors = ["SIGNUP", "SIGNUP_RENDER_SIGNUP_FORM", "SIGNUP_HANDLE_SIGNUP"]
self.assertFalse(_is_wrapper_anchor("SIGNUP_HANDLE_SIGNUP", anchors))
def test_single_anchor_file_has_no_wrapper(self):
from vibelign.core.patch_suggester import _is_wrapper_anchor
anchors = ["CONFIG"]
self.assertFalse(_is_wrapper_anchor("CONFIG", anchors))
def test_single_token_not_in_all_siblings_is_not_wrapper(self):
from vibelign.core.patch_suggester import _is_wrapper_anchor
anchors = ["AUTH", "AUTH_LOGIN_USER", "HELPERS_HASH"]
self.assertFalse(_is_wrapper_anchor("AUTH", anchors))
- Step 2: Scoring effect test
Append to the same file:
class WrapperAnchorScoringTest(unittest.TestCase):
def test_wrapper_penalty_lets_leaf_win_over_wrapper(self):
"""The exact add_email_domain_check scenario.
Without C3, SIGNUP (score 0) beats SIGNUP_HANDLE_SIGNUP (score -4).
With C3, SIGNUP gets -5 penalty → -5, so SIGNUP_HANDLE_SIGNUP (-4) wins.
"""
from vibelign.core.patch_suggester import choose_anchor, tokenize
anchors = ["SIGNUP", "SIGNUP_RENDER_SIGNUP_FORM", "SIGNUP_HANDLE_SIGNUP"]
request = "회원가입 시 허용된 이메일 도메인만 통과하도록 검사 추가"
tokens = tokenize(request)
chosen, _ = choose_anchor(anchors, tokens, None)
self.assertEqual(chosen, "SIGNUP_HANDLE_SIGNUP")
def test_wrapper_penalty_does_not_regress_verb_match_winner(self):
"""change_error_msg: LOGIN_HANDLE_LOGIN (+5) must still beat LOGIN (-5)."""
from vibelign.core.patch_suggester import choose_anchor, tokenize
anchors = ["LOGIN", "LOGIN_RENDER_LOGIN_FORM", "LOGIN_HANDLE_LOGIN", "LOGIN_RENDER_LOGIN_ERROR"]
request = "로그인 에러 메시지 변경 — 사용자 경험 개선"
tokens = tokenize(request)
chosen, _ = choose_anchor(anchors, tokens, None)
self.assertEqual(chosen, "LOGIN_HANDLE_LOGIN")
if __name__ == "__main__":
unittest.main()
- Step 3: Run tests to verify they fail
Run: uv run python -m pytest tests/test_wrapper_anchor_penalty.py -v
Expected: 4 _is_wrapper_anchor tests FAIL with ImportError: cannot import name '_is_wrapper_anchor'. 2 scoring tests may PASS or FAIL depending on current behavior (LOGIN test should PASS since it already works, SIGNUP test should FAIL).
- Step 4: Commit red stage
git add tests/test_wrapper_anchor_penalty.py
git commit -m "$(cat <<'EOF'
test(c3): wrapper anchor penalty unit tests (red stage)
6 tests: 4 for _is_wrapper_anchor detection, 2 for scoring effect.
Detection tests fail with ImportError pending implementation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"
Task 2: Implement _is_wrapper_anchor and penalty (TDD green)
Files:
-
Modify:
vibelign/core/patch_suggester.py:923(beforechoose_anchor) -
Step 1: Add constant and helper before
choose_anchor
Insert immediately before def choose_anchor( (line 923):
_WRAPPER_ANCHOR_PENALTY = 5
def _is_wrapper_anchor(anchor: str, all_anchors: list[str]) -> bool:
if len(all_anchors) < 2:
return False
tokens = _path_tokens(anchor)
if len(tokens) != 1:
return False
token = next(iter(tokens))
return all(
token in _path_tokens(other)
for other in all_anchors
if other != anchor
)
- Step 2: Apply penalty inside
choose_anchorscoring loop
In choose_anchor, after line score -= _anchor_quality_penalty(anchor_tokens) (currently line 942), add:
if _is_wrapper_anchor(anchor, anchors):
score -= _WRAPPER_ANCHOR_PENALTY
rationale.append("파일 전체를 감싸는 wrapper 앵커라 우선순위 낮춤")
- Step 3: Run unit tests
Run: uv run python -m pytest tests/test_wrapper_anchor_penalty.py -v
Expected: 6/6 PASS.
- Step 4: Run existing anchor-related tests for regression
Run: uv run python -m pytest tests/test_patch_suggested_anchor.py tests/test_patch_anchor_priority.py tests/test_patch_verb_cluster.py -v
Expected: All PASS.
- Step 5: Commit green stage
git add vibelign/core/patch_suggester.py
git commit -m "$(cat <<'EOF'
feat(anchor): wrapper anchor penalty for C3
Adds _is_wrapper_anchor detection and _WRAPPER_ANCHOR_PENALTY = 5.
File-scope wrapper anchors (single-token prefix of all siblings)
are penalized so that more-specific leaf anchors win even when
they carry a verb-cluster mismatch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"
Task 3: Tighten scenario regression guard
Files:
-
Modify:
tests/test_patch_accuracy_scenarios.py:51-79 -
Step 1: Change
startswith("SIGNUP")to exact match
Replace in test_add_email_domain_check_routes_to_signup_page:
self.assertTrue(
result.target_anchor.startswith("SIGNUP"),
f"expected a SIGNUP* anchor, got {result.target_anchor!r}",
)
→
self.assertEqual(result.target_anchor, "SIGNUP_HANDLE_SIGNUP")
Replace in test_add_email_domain_check_ai_mode_also_routes_to_signup:
self.assertTrue(
result.target_anchor.startswith("SIGNUP"),
f"expected a SIGNUP* anchor, got {result.target_anchor!r}",
)
→
self.assertEqual(result.target_anchor, "SIGNUP_HANDLE_SIGNUP")
- Step 2: Run scenario tests
Run: uv run python -m pytest tests/test_patch_accuracy_scenarios.py -v
Expected: 7/7 PASS.
- Step 3: Commit
git add tests/test_patch_accuracy_scenarios.py
git commit -m "$(cat <<'EOF'
test(c3): tighten add_email_domain_check anchor to exact match
C3 wrapper penalty now ensures SIGNUP_HANDLE_SIGNUP is chosen over
the outer SIGNUP wrapper. Upgrade from startswith guard to assertEqual.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"
Task 4: Update baseline
Files:
-
Modify:
tests/benchmark/patch_accuracy_baseline.json(via CLI) -
Step 1: Reinstall vib tool
Run: uv tool install --reinstall --force .
Expected: Success message with vibelign==1.7.2.
- Step 2: Check current state
Run: vib bench --patch
Expected: anchor_ok shows 4/4 (+1) for both det/ai. improvements includes add_email_domain_check anchor_ok: False -> True. Zero regressions.
- Step 3: Update baseline
Run: vib bench --patch --update-baseline
Expected: Exit code 0.
- Step 4: Verify clean baseline
Run: vib bench --patch
Expected: All (=), regressions: none, improvements: none. Totals: files_ok: 5/5, anchor_ok: 4/4, recall@3: 5/5.
- Step 5: Commit baseline
git add tests/benchmark/patch_accuracy_baseline.json
git commit -m "$(cat <<'EOF'
chore(bench): update patch accuracy baseline for C3
Post-C3 baseline: det/ai 5/5 files, 4/4 anchor, 5/5 recall@3.
add_email_domain_check anchor_ok flipped from false to true via
wrapper anchor penalty. All scenarios now at maximum achievable
scores within the current 5-scenario framework.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"
Task 5: Full verification + cleanup
Files:
-
Run-only. No code changes.
-
Step 1: Full test suite
Run: uv run python -m pytest tests/ -v 2>&1 | tail -20
Expected: All tests pass (613+ — 607 existing + 6 new).
- Step 2: Commit chain review
Run: git log --oneline -6
Expected:
docs(spec): patch accuracy C3 wrapper anchor penalty designtest(c3): wrapper anchor penalty unit tests (red stage)feat(anchor): wrapper anchor penalty for C3test(c3): tighten add_email_domain_check anchor to exact matchchore(bench): update patch accuracy baseline for C3
- Step 3: Acceptance criteria check (spec §5)
Verify:
vib bench --patch: anchor_ok == 4/4 ✓- files_ok == 5/5 ✓
- recall@3 == 5/5 ✓
- regressions == [] ✓
- add_email_domain_check anchor assertion is exact match ✓
- _is_wrapper_anchor has ≥4 test cases ✓
Rollback Plan
All commits are independent. git revert <hash> for any single commit. Task 2 (implementation) is the only one that changes production code — reverting it restores pre-C3 behavior and the unit tests in Task 1 will fail (expected).
Self-Review
- Spec coverage: §3.1 wrapper detection → Task 2 Step 1. §3.2 penalty → Task 2 Step 2. §3.3 helper → Task 2 Step 1. §4 edge cases → Task 1 Step 1. §5 acceptance → Task 5 Step 3. §6 files → all tasks. ✓
- Placeholder scan: No TBD/TODO/placeholders. All code blocks complete. ✓
- Type consistency:
_is_wrapper_anchor(anchor: str, all_anchors: list[str]) -> boolsignature matches across Task 1 tests and Task 2 implementation._WRAPPER_ANCHOR_PENALTY = 5used in Task 2 only.choose_anchorandtokenizeimports consistent. ✓