The "Where & Why": วิศวกรรม Dynamic Exemplar Mining และการขุดค้น Best Practices สดจาก Repo
SKILL.md และใน Day 8 เราติดตั้งเลเยอร์ references/ เพื่อเก็บ Schemas ขนาดใหญ่ปัญหาคอขวดที่พบในโปรเจกต์จริง: Static Few-Shot ใน Prompt เกิดอาการ Exemplar Rot อย่างรวดเร็วเมื่อทีมปรับแต่ง Custom Test Helpers, In-house Assertions, หรือ Domain Fixture Builders การ Hardcode โค้ดตัวอย่างไว้ใน Skill จึงไม่สอดคล้องกับสไตล์โค้ดปัจจุบันของ Repo
เป้าหมายของ Day 9: เราจะติดตั้งเครื่องมือ Dynamic Exemplar Mining ใน
scripts/harvest-exemplars.py ซึ่งทำหน้าที่ค้นหาและคัดกรอง "Golden Unit Test" ที่คอมไพล์ผ่านจริงในโปรเจกต์ปัจจุบัน มาแปลงเป็น PRS Context Slice ขนาดไม่เกิน 250 Tokens เพื่อป้อนให้ Host Agent ลอกเลียนแบบสไตล์โค้ดจริงของทีมแบบ On-the-Fly
┌────────────────────────────────────────────────────────────────────────┐
│ HOST AGENT RECEIVES SCAFFOLDING REQUEST │
│ Target: src/main/kotlin/.../SettlementReconciliationService.kt │
└───────────────────────────────────┬────────────────────────────────────┘
│
▼ (Pre-flight Tool Call: Dynamic Harvesting)
┌────────────────────────────────────────────────────────────────────────┐
│ 🎯 SCRIPTS/HARVEST-EXEMPLARS.PY [TODAY'S ARTIFACT] │
│ 1. Scans existing test suites in src/test/kotlin/ │
│ 2. Applies Heuristic Quality Scoring: │
│ + MockK syntax verified + AAA structure present │
│ - Mockito detected (PRUNED) - SpringRunner detected (PRUNED) │
│ 3. Selects Top-1 Golden Test Method in sibling package │
│ 4. Slices out method body & creates dense PRS Exemplar Card (< 250 Tok)│
└───────────────────────────────────┬────────────────────────────────────┘
│
▼ (Dynamic In-Context Injection)
┌────────────────────────────────────────────────────────────────────────┐
│ HOST AGENT INGESTS FRESH REPO-NATIVE EXEMPLAR │
│ Employs the project's exact custom builders, naming, and assertions! │
│ Zero Prompt Rot • 100% Repository-Idiomatic Alignment │
└────────────────────────────────────────────────────────────────────────┘
Live Repo Alignment
ไม่จำเป็นต้องคอยอัปเดตตัวอย่างโค้ดใน SKILL.md ตัวสคริปต์จะดึงโค้ดสดที่ผ่านการยอมรับจากเพื่อนร่วมทีมมาเป็นแม่แบบเสมอ
Anti-Drift Quality Gates
ป้องกันปัญหา Garbage In, Garbage Out ด้วยการคัดกรองไฟล์ที่มี Mockito หรือ @SpringBootTest ทิ้งก่อนนำมาเป็นตัวอย่าง
< 250 Token Economy
ไม่ส่งไฟล์ทดสอบความยาว 500 บรรทัดทั้งไฟล์ แต่สกัดเฉพาะเมธอดตัวอย่างที่ดีที่สุด 1 เมธอดพร้อม Setup Statement เท่านั้น
วิศวกรรม Dynamic Exemplar Mining: กฎเกณฑ์การให้คะแนน (Heuristic Scoring) และการสกัดแบบแม่นยำ
ในโลกของ LLM In-Context Learning การให้ตัวอย่างโค้ดที่เป็น Generic Example จากโลกภายนอกมักทำให้เกิดปัญหาเรื่อง "Style Incompatibility" เช่น AI เขียน assertion ด้วยไวยากรณ์มาตรฐาน แต่โปรเจกต์ของบริษัทมี Custom Extension Functions หรือ Base Test Classes ประจำทีม การสกัดตัวอย่างสดจากโค้ดจริงจึงเป็นสะพานเชื่อมที่ทำให้ Agent กลมกลืนไปกับวัฒนธรรมวิศวกรรมขององค์กร
| มิติสถาปัตยกรรม (Architectural Dimension) | Hardcoded Static Exemplars (Day 4) | Raw Repository Grep (ค้นหาคำสุ่ม) | Dynamic Exemplar Mining (Day 9) |
|---|---|---|---|
| Repository Idiom Alignment | ต่ำ: โค้ดตัวอย่างมักใช้ Standard Lib ไม่รู้จัก Custom DSL ของทีม | ปานกลาง: ได้โค้ดในโปรเจกต์ แต่อาจเป็นโค้ดเก่าที่เขียนแย่ | สูงสุด: ดึงเฉพาะโค้ดที่มี Quality Score สูงสุดใน Repo |
| Risk of Legacy Contamination | ไม่มีความเสี่ยง: ผ่านการตรวจโดยผู้เขียน Skill | วิกฤต: เผลอดึงไฟล์เทสต์ยุค Java 8 / Mockito มาสอน Agent | เป็นศูนย์: มี Invariant Gates คัดกรอง Anti-patterns ทิ้ง 100% |
| Maintenance Effort | สูง: เมื่อ Framework เปลี่ยนเวอร์ชัน ต้องตามแก้ SKILL.md | ไม่มีการดูแลระบบ | Zero-Maintenance: โค้ดตัวอย่างพัฒนาตามการ Refactor ของทีม |
| Context Token Cost | คงที่: กิน Token 600-800 ตัวใน SKILL.md ตลอดเวลา | บวมมหาศาล: เสียรอบอ่านไฟล์ทั้งก้อน 2,000+ Tokens | ต่ำมาก: โหลดแบบ On-demand เฉพาะเมธอดเดี่ยว (< 250 Tokens) |
สคริปต์ขุดค้นไม่ได้หยิบไฟล์แรกที่เจอ แต่คำนวณ Exemplar Quality Score ($Q$) เพื่อเลือกเฉพาะชุดทดสอบระดับ Masterpiece:
หากไฟล์ใดมีคะแนนติดลบ (จากการละเมิด Negative Boundaries ใน Day 3) สคริปต์จะตัดทิ้งทันที โดยจะเลือกเฉพาะไฟล์ที่ได้คะแนนสูงสุดและตั้งอยู่ใน Package เดียวกันหรือใกล้เคียงกับคลาสเป้าหมายมากที่สุด
การสกัดโค้ดตัวอย่างต้องปฏิบัติตามกฎ Surgical Extraction Contract:
- 1. Dependency Declaration Slice: สกัดเฉพาะบรรทัดประกาศ
val mock: Dependency = mockk()และ constructor injection ของคลาสนั้น - 2. Single Canonical Test Slice: สกัดเฉพาะเมธอดเทสต์ตัวแรกที่มีโครงสร้าง AAA ครบถ้วน ไม่นำเมธอดเทสต์อื่นอีก 15 ตัวมาด้วย
- 3. PRS Wrapping: แปลงโค้ดที่สกัดได้ให้กลายเป็น Markdown บล็อก
### Pattern / Reasoning / Solutionทันทีก่อนส่งให้ Agent
Production Artifact: Dynamic Exemplar Mining Engine
ชุดเครื่องมือตามมาตรฐาน agentskills/agentskills ที่ประกอบด้วยคำสั่งใน SKILL.md, สคริปต์ไพธอนขุดค้นตัวอย่างสด (Zero-Dependency), และตัวห่อหุ้มคำสั่งสำหรับ Agent
.github/skills/spring-boot-unit-tester/
├── SKILL.md ◄── [UPDATED: Added Pre-flight Dynamic Mining Protocol]
├── scripts/
│ ├── run-unit-tests.sh ◄── Day 5-6: Sandboxed Execution & Triage Runner
│ ├── extract-target-ast.py ◄── Day 7: AST Class Skeleton Inspector
│ ├── query-reference.sh ◄── Day 8: Targeted Schema Slicer
│ └── harvest-exemplars.py ◄── [TODAY'S ARTIFACT: Local Test Exemplar Miner]
└── references/
└── billing-domain-contracts.json ◄── Day 8: Canonical DTO Specifications
---
name: spring-boot-unit-tester
schema-version: "1.0.0"
description: >
Generates deterministic, isolated Kotlin unit tests for Spring Boot 3.x
using MockK and AssertJ. MUST NOT load Spring ApplicationContext.
tools:
- name: inspect-class-context
description: Extracts concise AST skeleton from target source file.
file: scripts/extract-target-ast.py
- name: harvest-exemplars
description: Dynamically harvests idiomatic golden unit test exemplars from the local repository.
file: scripts/harvest-exemplars.py
- name: query-reference
description: Queries on-demand domain DTO schemas and fixtures from references/ directory.
file: scripts/query-reference.sh
- name: run-unit-tests
description: Runs sandboxed test execution with automatic stderr triage.
file: scripts/run-unit-tests.sh
metadata:
domain: "backend-quality"
target-framework: "Spring Boot 3.x / Kotlin 1.9+"
---
# Spring Boot Unit Test Architect Specification
## 1. Architectural Invariants
1. Follow AAA pattern with isolated JVM memory execution (< 50ms per method).
2. MUST NOT use `@SpringBootTest`, `@DataJpaTest`, or `org.mockito.*`.
3. Wrap all `suspend` methods inside `runTest` using `coEvery { ... }`.
## 2. Pre-flight Context Harvesting Protocol
Before authoring any new unit test, the Host Agent MUST execute this sequential pre-flight:
1. **Target AST Skeleton**: Run `python3 scripts/extract-target-ast.py <TARGET_FILE>` to harvest constructor dependencies.
2. **Dynamic Exemplar Mining**: Run `python3 scripts/harvest-exemplars.py <TARGET_FILE>` to mine existing golden test patterns in the repo.
- Adhere to the mined exemplar's exact assertion styling, mock naming, and builder patterns.
3. **Domain References (Conditional)**: If the target method interacts with complex billing DTOs, query `query-reference.sh`.
#!/usr/bin/env python3
"""
scripts/harvest-exemplars.py
Standard: agentskills Standard v1.0 (Phase 3 Reference Architecture)
Scans existing test suites in the repository, filters out anti-patterns,
and emits a concise (< 250 tokens) PRS exemplar card of idiomatic repository code.
"""
import os
import sys
import re
from pathlib import Path
# // [TODAY'S LESSON PLUGS IN HERE: HEURISTIC QUALITY SCORING & SLICING]
def score_test_content(content: str) -> int:
"""Calculates canonical quality score. Disqualifies anti-patterns immediately."""
# Negative Invariant Gate: Disqualify immediately if legacy Mockito or Spring runner present
if re.search(r"org\.mockito|@SpringBootTest|@DataJpaTest|@ExtendWith\(SpringExtension", content):
return -9999
score = 0
if "io.mockk" in content: score += 40
if "confirmVerified" in content: score += 20
if "runTest" in content: score += 15
if "// Arrange" in content and "// Act" in content and "// Assert" in content: score += 30
if "assertThat(" in content: score += 20
return score
def extract_surgical_slice(file_path: Path) -> str:
"""Extracts mock dependencies and the first canonical test method from the candidate file."""
try:
text = file_path.read_text(encoding="utf-8", errors="ignore")
except Exception:
return ""
lines = text.splitlines()
mock_declarations = []
test_method_lines = []
inside_test = False
brace_count = 0
test_captured = False
for line in lines:
stripped = line.strip()
# 1. Capture Mock declarations: private val foo = mockk<Foo>()
if ("mockk<" in stripped or "mockk(" in stripped) and ("val " in stripped or "var " in stripped):
if len(mock_declarations) < 3:
mock_declarations.append(stripped)
# 2. Capture First Canonical @Test Method
if not test_captured:
if "@Test" in stripped:
inside_test = True
test_method_lines.append(line)
brace_count += line.count("{") - line.count("}")
continue
if inside_test:
test_method_lines.append(line)
brace_count += line.count("{") - line.count("}")
if brace_count <= 0 and len(test_method_lines) > 2:
inside_test = False
test_captured = True
if not test_method_lines:
return ""
# Format dense PRS card
card = [
f"### 💎 DYNAMIC REPO EXEMPLAR: `{file_path.name}`",
"#### [P] Target Sibling Pattern:",
f"Mined from existing codebase at `{file_path}`.",
"#### [R] Idiomatic Conventions Observed in Project:",
"- Dependencies instantiated via constructor MockK.",
"- AAA structure followed with explicit assertion assertions.",
"#### [S] Canonical Repository Code Snippet:",
"```kotlin"
]
if mock_declarations:
card.extend(mock_declarations)
card.append("")
card.extend(test_method_lines[:25]) # Truncate method to prevent context flooding
card.append("```")
return "\n".join(card)
def harvest_exemplar(target_path_str: str, search_root: str = "src/test/kotlin") -> str:
root = Path(search_root)
if not root.exists():
root = Path(".")
target_name = Path(target_path_str).stem
candidates = []
# Traverse existing tests
for p in root.rglob("*Test.kt"):
try:
content = p.read_text(encoding="utf-8", errors="ignore")
score = score_test_content(content)
if score > 50: # Only high-scoring canonical candidates
candidates.append((score, p))
except Exception:
continue
if not candidates:
return "### ℹ️ NO LOCAL EXEMPLAR MINED (Follow baseline SKILL.md standards)"
# Sort by quality score descending
candidates.sort(key=lambda x: x[0], reverse=True)
best_candidate = candidates[0][1]
return extract_surgical_slice(best_candidate)
def main():
target_file = sys.argv[1] if len(sys.argv) > 1 else "UnknownService.kt"
slice_card = harvest_exemplar(target_file)
sys.stdout.write(slice_card + "\n")
if __name__ == "__main__":
main()
Enterprise Incident: Custom Domain Assertion Hallucination vs. Repo Idioms
สถานการณ์จริง: ภายใน Microservice ด้านการเงิน ทีมสถาปัตยกรรมได้สร้าง Custom AssertJ Extension ชื่อ assertThatSettlement(result).isFullyReconciled() และกำหนดกฎว่า ห้ามใช้ assertThat(result.status).isEqualTo("RECONCILED") ตรงๆ แต่เมื่อ Host Agent เข้ามาทำงาน ตัวโมเดลไม่เคยเห็น Extension นี้ใน Training Data หรือใน SKILL.md ทั่วไป จึงสร้างโค้ดแบบดั้งเดิมที่ทำให้ Pull Request โดนปฏิเสธจาก CI Linter ประจำทีม
- จำลองชุดทดสอบเดิมในโปรเจกต์
PaymentLedgerServiceTest.ktที่ใช้ Custom Assertion และ MockK อย่างถูกต้อง - รันสคริปต์
harvest-exemplars.pyเพื่อพิสูจน์ว่าระบบสามารถสแกน คัดเลือก และสกัดตัวอย่างโค้ดที่มี Custom Assertion ออกมาเป็น PRS Card ได้อย่างแม่นยำ - ยืนยันว่าสคริปต์ปฏิเสธไฟล์ที่มี Mockito ปนเปื้อน และผลิต Exemplar Card ขนาด < 200 Tokens สำหรับป้อนให้ Agent ทันที
package com.enterprise.trading.ledger
import com.enterprise.testing.assertions.assertThatSettlement
import io.mockk.*
class PaymentLedgerServiceTest {
private val ledgerRepo: LedgerRepository = mockk()
// ... 40 lines of mocks and fixtures ...
@Test
fun `should reconcile batch entries`() = runTest {
// Arrange
val batchId = UUID.randomUUID()
coEvery { ledgerRepo.find(batchId) } returns sampleBatch()
// Act
val result = service.reconcile(batchId)
// Assert: Uses Team's In-house Custom DSL!
assertThatSettlement(result).isFullyReconciled()
confirmVerified(ledgerRepo)
}
// ... 15 other test methods ...
}
### 💎 DYNAMIC REPO EXEMPLAR: `PaymentLedgerServiceTest.kt`
#### [P] Target Sibling Pattern:
Mined from existing codebase at `src/test/kotlin/.../PaymentLedgerServiceTest.kt`.
#### [R] Idiomatic Conventions Observed in Project:
- Dependencies instantiated via constructor MockK.
- AAA structure followed with explicit assertion assertions.
#### [S] Canonical Repository Code Snippet:
```kotlin
private val ledgerRepo: LedgerRepository = mockk()
@Test
fun `should reconcile batch entries`() = runTest {
// Arrange
val batchId = UUID.randomUUID()
coEvery { ledgerRepo.find(batchId) } returns sampleBatch()
// Act
val result = service.reconcile(batchId)
// Assert
assertThatSettlement(result).isFullyReconciled()
confirmVerified(ledgerRepo)
}
```
$ python3 .github/skills/spring-boot-unit-tester/scripts/harvest-exemplars.py src/main/kotlin/com/enterprise/trading/SettlementReconciliationService.kt
### 💎 DYNAMIC REPO EXEMPLAR: `PaymentLedgerServiceTest.kt`
#### [P] Target Sibling Pattern:
Mined from existing codebase at `src/test/kotlin/com/enterprise/trading/ledger/PaymentLedgerServiceTest.kt`.
#### [R] Idiomatic Conventions Observed in Project:
- Dependencies instantiated via constructor MockK.
- AAA structure followed with explicit assertion assertions.
#### [S] Canonical Repository Code Snippet:
```kotlin
private val ledgerRepo: LedgerRepository = mockk()
@Test
fun `should reconcile batch entries`() = runTest {
// Arrange
val batchId = UUID.randomUUID()
coEvery { ledgerRepo.find(batchId) } returns sampleBatch()
// Act
val result = service.reconcile(batchId)
// Assert
assertThatSettlement(result).isFullyReconciled()
confirmVerified(ledgerRepo)
}
```
$ echo $?
0
ทดสอบสถาปัตยกรรม Dynamic Exemplar Mining (Day 9)
ตอบคำถามเชิงสถาปัตยกรรมทั้ง 12 ข้อด้านล่าง ระบบจะตรวจคำตอบและให้ Feedback ทันทีเมื่อคลิกเลือกช้อยส์ พร้อมแสดงคำอธิบายเชิงลึก