D7
Standard: agentskills/agentskills

Day 7: AST & Static Analysis Scripts

12-Day Series
🗺️ SECTION 1 / SYSTEM BLUEPRINT & INTEGRATION

The "Where & Why": การดึง Live Project Context ด้วย AST สู่การสร้าง Scaffolding ที่แม่นยำ

🎯 Mental Map (148 Words): ใน Day 5-6 เราสร้าง Execution Sandbox และ Stderr Triage เพื่อซ่อมแซมโค้ดเมื่อรันพัง

ปัญหาคอขวดที่พบ: ก่อนจะถึงขั้นตอนรันเทสต์ Host Agent มักเกิด Blind Signature Hallucination เช่น สุ่มเดาว่าคลาสเป้าหมายมี Constructor รับตัวแปรใดบ้าง, ฟังก์ชันคืนค่าประเภทไหน, หรือเมธอดเป็น suspend หรือไม่ การส่งไฟล์ซอร์สโค้ดดิบ 500-1,000 บรรทัดให้ Agent อ่านจะผลาญ Token และเพิ่ม Noise
เป้าหมายของ Day 7: เราจะติดตั้งเครื่องมือ AST (Abstract Syntax Tree) & Static Analysis Tooling ใน scripts/extract-target-ast.py ที่สกัดโครงสร้างคลาสเฉพาะ Constructor, Method Signatures, และ Annotations ออกมาเป็น Skeleton Contract ขนาดไม่เกิน 120 Tokens เพื่อให้ Agent เขียนเทสต์ได้ตรงเป้าตั้งแต่รอบแรก (Zero-Shot Accuracy)
Static AST Inspection Pipeline & Scaffolding Flow AST Protocol v1.0
┌────────────────────────────────────────────────────────────────────────┐
│ HOST AGENT RECEIVES TARGET CLASS PATH                                  │
│ Target: src/main/kotlin/.../OrderSettlementService.kt (650 Lines)      │
└───────────────────────────────────┬────────────────────────────────────┘
                                    │
                                    ▼ (Tool Call: Non-interactive Inspector)
┌────────────────────────────────────────────────────────────────────────┐
│ 🎯 SCRIPTS/EXTRACT-TARGET-AST.PY [TODAY'S ARTIFACT]                    │
│ - Parses Kotlin / Java Abstract Syntax Tree (Regex/Symbol Tokenizer)   │
│ - Extracts Class Name, Package, Injected Constructor Dependencies      │
│ - Extracts Public Methods, Parameter Types, Return Types, Suspend Flags│
│ - Strips Business Logic, Private Helpers, and Method Bodies            │
└───────────────────────────────────┬────────────────────────────────────┘
                                    │
                                    ▼ (Dense AST Skeleton Contract < 120 Tokens)
┌────────────────────────────────────────────────────────────────────────┐
│ IN-CONTEXT SKELETON CARD (Zero Token Waste)                            │
│ `OrderSettlementService(paymentClient: PaymentClient, repo: OrderRepo)`│
│ `+ suspend fun processOrder(orderId: UUID): SettlementResult`          │
└───────────────────────────────────┬────────────────────────────────────┘
                                    │
                                    ▼ (First-Pass Scaffolding Generation)
┌────────────────────────────────────────────────────────────────────────┐
│ HOST AGENT GENERATES 100% ACCURATE MOCKK TEST SUITE                    │
│ Passes Compiler Gates in Day 5-6 without Hallucinating Constructor Args│
└────────────────────────────────────────────────────────────────────────┘
        
PILLAR 01: SKELETON COMPRESSION

90%+ Token Reduction

แปลงไฟล์ซอร์สโค้ดขนาด 30 KB ให้เหลือเฉพาะ Method Signatures และ Types ไม่เกิน 120 Tokens ประหยัด Context Window สูงสุด

PILLAR 02: SYMBOL FIDELITY

Deterministic Types

การันตีว่า Agent รู้จัก Parameter Types, Nullability, และ Suspend modifier ที่แท้จริง หมดปัญหา Type mismatch ตอนคอมไพล์

PILLAR 03: ZERO-DEPENDENCY AST

Lightweight & Fast

พัฒนาด้วย Python Standard Library ทำงานเสร็จในระดับมิลลิวินาที (< 20ms) รันได้ทันทีบนเครื่อง Developer โดยไม่ต้องติดตั้ง Maven/Kotlinc ล่วงหน้า

⚙️ SECTION 2 / ARCHITECTURAL DEEP DIVE

วิศวกรรม AST & Static Analysis: การแปลง Raw Code สู่ Actionable Symbol Contracts

ในกระบวนการเขียนโค้ดของมนุษย์ Language Server Protocol (LSP) ใน IDE (เช่น IntelliJ IDEA หรือ VS Code) คอยให้ข้อมูล Code Completion ผ่าน Symbol Index แต่สำหรับ Host Agent หากเราสั่งให้อ่านไฟล์ซอร์สโค้ดทั้งไฟล์ โมเดลจะอ่าน Implementation Logic นับร้อยบรรทัดที่ไม่เกี่ยวข้องกับการ Mock ทำให้เกิด Attention Distraction การสร้างเครื่องมือสกัด AST เฉพาะทางจึงเป็นสะพานเชื่อมที่มีความแม่นยำสูงสุด

มิติสถาปัตยกรรม (Architectural Dimension) Raw Source Ingestion (อ่านไฟล์ดิบทั้งไฟล์) LLM Summarization (ให้ AI สรุปเอง) Deterministic AST Inspection (Day 7)
Context Token Cost สูงมาก (1,500 - 4,000 Tokens ต่อคลาส) ปานกลาง (เสียรอบคุยซ้ำเพื่อขอสรุป) ต่ำที่สุด (< 120 Tokens ต่อคลาส)
Execution Latency เร็วในการอ่านไฟล์ แต่สร้างโค้ดช้าเพราะ Context ยาว ช้ามาก (เสีย Roundtrip คุยกับ LLM 2 รอบ) เร็วระดับ Real-time (< 25ms บน local disk)
Constructor Parameter Precision ปานกลาง: โมเดลมักสับสนระหว่าง Field กับ Constructor Arg ต่ำ: มีโอกาส Hallucinate ชนิดตัวแปร แม่นยำ 100%: ดึงจาก Primary Constructor AST ตรงๆ
Coroutine & Suspension Awareness มักมองข้ามคำว่า suspend ในเมธอดลึกๆ ขึ้นอยู่กับการเดาของโมเดล ชัดเจน: ติดแท็ก [SUSPEND] นำทาง MockK coEvery ทันที
Abstract Syntax Tree (AST) vs Raw Token Streams

Abstract Syntax Tree คือโครงสร้างลำดับชั้นของซอร์สโค้ดที่ตัดไวยากรณ์ตกแต่ง (เครื่องหมายปีกกา, ช่องว่าง, คอมเมนต์) ออกไป เหลือเพียงความสัมพันธ์เชิงตรรกะของ Node ต่างๆ เช่น ClassDeclaration, ConstructorParameter, และ FunctionDeclaration

Source_Code (30 KB) ──► AST_Extractor ──► Symbol_Skeleton (450 bytes / ~90 tokens)

เมื่อ Host Agent ได้รับเฉพาะ Class Skeleton ตัวโมเดลจะพุ่งความสนใจไปยังการ Mock Dependencies และการ Assert ผลลัพธ์โดยไม่ถูก Implementation Logic รบกวน

Pre-flight Inspection: Feeding Context Before Generation

ความแตกต่างระหว่าง Agent มือสมัครเล่นกับ Agent ระดับ Production คือขั้นตอน Pre-flight Phase:

  • Naive Approach: ป้อนคำสั่งให้สร้างเทสต์ทันที -> โมเดลเดา Constructor -> คอมไพล์พัง -> ซ่อมวนลูป 3 รอบ
  • Specification-First (Day 7): สั่งสคริปต์ inspect-class-context.sh สกัด Skeleton ก่อน -> ป้อนเข้า In-Context Working Memory -> โมเดลสร้างเทสต์ที่คอมไพล์ผ่านในครั้งแรก (Pass@1)
🧩 SECTION 3 / END-TO-END CODE BLUEPRINT

Production Artifact: AST Symbol Extractor & Static Inspector

ชุดเครื่องมือระดับองค์กรตามมาตรฐาน agentskills/agentskills สำหรับสกัดข้อมูลคลาสเป้าหมายในภาษา Kotlin/Java ออกมาเป็น Markdown Skeleton Payload

Skill Directory Placement
.github/skills/spring-boot-unit-tester/
├── SKILL.md                          ◄── [Updated Tool Contract with Pre-flight AST Action]
└── scripts/
    ├── run-unit-tests.sh             ◄── Day 5-6: Sandboxed Execution & Triage Runner
    ├── triage-test-failure.py        ◄── Day 6: Stderr Noise Filter
    ├── extract-target-ast.py         ◄── [TODAY'S ARTIFACT: AST Symbol Extractor]
    └── inspect-class-context.sh      ◄── [TODAY'S ARTIFACT: Shell Wrapper for Host Tooling]
        
FILE: .github/skills/spring-boot-unit-tester/SKILL.md Tool Invocation Contract
---
name: spring-boot-unit-tester
schema-version: "1.0.0"
description: >
  Generates and refactors 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 (constructor dependencies and public methods) from target source file.
    file: scripts/inspect-class-context.sh
  - name: run-unit-tests
    description: Runs static linting and sandboxed Maven test execution with automatic failure 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. Two-Phase Execution Protocol
Whenever generating a new unit test for an existing service class, the Host Agent MUST follow this two-phase sequence:

### Phase 1: Pre-flight AST Context Inspection

1. Invoke tool: `bash .github/skills/spring-boot-unit-tester/scripts/inspect-class-context.sh <TARGET_SOURCE_FILE.kt>`
2. Parse the emitted Markdown Skeleton from stdout:
   - Identify constructor dependencies to instantiate via `mockk<T>()`.
   - Identify methods marked with `[SUSPEND]` to stub using `coEvery { ... }` instead of `every { ... }`.
   - Identify parameter types and return signatures to construct realistic mock fixtures.

### Phase 2: Code Generation & Sandboxed Verification
1. Scaffold the pure unit test conforming to Day 3 Negative Invariants and Day 4 PRS Exemplars.
2. Execute verification runner: `bash .github/skills/spring-boot-unit-tester/scripts/run-unit-tests.sh <TARGET_TEST_FILE.kt>`
3. If non-zero exit occurs, parse stderr triage report and self-correct (Max 3 iterations).
          
FILE: scripts/extract-target-ast.py Zero-Dependency Python 3
#!/usr/bin/env python3
"""
scripts/extract-target-ast.py
Extracts class declaration, constructor dependencies, and public method signatures
from Kotlin/Java source files. Emits a dense Markdown Symbol Skeleton (< 120 tokens).
"""
import sys
import re

# // [TODAY'S LESSON PLUGS IN HERE: AST SYMBOL EXTRACTION]
def extract_skeleton(source_text: str) -> dict:
    skeleton = {
        "package": "Unknown",
        "class_name": "Unknown",
        "class_annotations": [],
        "constructor_params": [],
        "methods": []
    }

    # 1. Package Extractor
    pkg_match = re.search(r"package\s+([a-zA-Z0-9_.]+)", source_text)
    if pkg_match:
        skeleton["package"] = pkg_match.group(1)

    # 2. Class Declaration & Primary Constructor
    # Matches: class OrderSettlementService(val repo: OrderRepository, private val client: PaymentClient)
    class_pattern = re.compile(
        r"(?:@([a-zA-Z0-9_]+)\s+)*"
        r"(?:open\s+|data\s+|sealed\s+)?class\s+([a-zA-Z0-9_]+)"
        r"(?:\s*constructor\s*)?"
        r"\s*(?:\(([\s\S]*?)\))?"
    )
    
    class_match = class_pattern.search(source_text)
    if class_match:
        if class_match.group(1):
            skeleton["class_annotations"] = [class_match.group(1)]
        skeleton["class_name"] = class_match.group(2)
        
        raw_params = class_match.group(3)
        if raw_params:
            # Parse individual constructor parameters
            for param in raw_params.split(","):
                param = param.strip()
                if not param:
                    continue
                # Clean out val/var/private/override keywords
                clean_param = re.sub(r"\b(val|var|private|protected|public|override)\b", "", param).strip()
                if ":" in clean_param:
                    p_name, p_type = clean_param.split(":", 1)
                    skeleton["constructor_params"].append({
                        "name": p_name.strip(),
                        "type": p_type.strip()
                    })

    # 3. Public Method Signatures
    # Matches: [suspend] fun processOrder(orderId: UUID): SettlementResult
    method_pattern = re.compile(
        r"^\s*(?:@([a-zA-Z0-9_]+)\s+)*"
        r"(suspend\s+)?fun\s+([a-zA-Z0-9_]+)\s*\(([\s\S]*?)\)\s*(?::\s*([a-zA-Z0-9_<>,?\s]+))?",
        re.MULTILINE
    )

    for match in method_pattern.finditer(source_text):
        is_suspend = bool(match.group(2))
        method_name = match.group(3)
        raw_args = match.group(4).strip().replace("\n", " ")
        raw_return = match.group(5) or "Unit"
        
        # Clean arguments: strip default values
        clean_args = re.sub(r"\s*=\s*[^,)]+", "", raw_args).strip()
        clean_return = raw_return.strip()

        # Skip private/internal conventions if preceded by private
        line_start = source_text[max(0, match.start() - 25):match.start()]
        if "private" in line_start or "internal" in line_start:
            continue

        skeleton["methods"].append({
            "name": method_name,
            "is_suspend": is_suspend,
            "signature": f"fun {method_name}({clean_args}): {clean_return}"
        })

    return skeleton

def format_markdown_card(skeleton: dict) -> str:
    """Formats skeleton into an ultra-dense markdown block tailored for LLM prompt ingestion."""
    lines = [
        "### 📦 TARGET CLASS SKELETON CONTRACT",
        f"- **Package**: `{skeleton['package']}`",
        f"- **Class**: `{skeleton['class_name']}`",
        "- **Constructor Injected Dependencies (MUST mock via `mockk()`):**"
    ]
    
    if skeleton["constructor_params"]:
        for p in skeleton["constructor_params"]:
            lines.append(f"  - `{p['name']}: {p['type']}`")
    else:
        lines.append("  - *(No constructor parameters / No dependencies)*")

    lines.append("- **Public Method Endpoints:**")
    if skeleton["methods"]:
        for m in skeleton["methods"]:
            tag = " `[SUSPEND]`" if m["is_suspend"] else ""
            lines.append(f"  - `{m['signature']}`{tag}")
    else:
        lines.append("  - *(No public methods detected)*")

    lines.append("")
    lines.append("💡 *Guidance: Mock all constructor dependencies. Wrap `[SUSPEND]` methods inside `runTest` using `coEvery`.*")
    return "\n".join(lines)

def main():
    if len(sys.argv) < 2:
        source_text = sys.stdin.read()
    else:
        with open(sys.argv[1], "r", encoding="utf-8", errors="ignore") as f:
            source_text = f.read()

    skeleton = extract_skeleton(source_text)
    sys.stdout.write(format_markdown_card(skeleton) + "\n")

if __name__ == "__main__":
    main()
          
FILE: scripts/inspect-class-context.sh POSIX Bash Wrapper
#!/usr/bin/env bash
# ==============================================================================
# scripts/inspect-class-context.sh
# Standard: agentskills Standard v1.0 (Phase 2 Deterministic Tooling)
# Inspects target source class and outputs high-density AST skeleton to stdout.
# ==============================================================================
set -euo pipefail

TARGET_FILE="${1:-}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
EXTRACTOR="${SCRIPT_DIR}/extract-target-ast.py"

# 1. Argument & File Verification
if [[ -z "${TARGET_FILE}" ]] || [[ ! -f "${TARGET_FILE}" ]]; then
    echo "### 🚨 SCRIPT EXECUTION ERROR" >&2
    echo "- **Category**: \`FILE_NOT_FOUND\`" >&2
    echo "- **Error**: Target file '${TARGET_FILE}' does not exist or was not specified." >&2
    exit 1
fi

# 2. Execute AST Extraction Pipeline
# Outputs structured skeleton directly to stdout, errors to stderr
python3 "${EXTRACTOR}" "${TARGET_FILE}"
exit 0
          
🛠️ SECTION 4 / HANDS-ON SCENARIO & LAB

Enterprise Incident: The Blind Constructor Guessing Catastrophe

สถานการณ์จริง: Host Agent ถูกมอบหมายให้เขียน Unit Test สำหรับคลาส FxOrderExecutionService.kt ซึ่งเป็นคลาสขนาด 720 บรรทัดที่มีความซับซ้อนสูง เนื่องจาก Agent ไม่ได้ทำ Pre-flight AST Inspection ตัวโมเดลจึง "เดาสุ่ม" จากชื่องานว่า Service นี้น่าจะมี Dependencies เพียง 2 ตัวคือ FxRateRepository และ AuditLogger แต่ความจริงแล้ว คลาสนี้ต้องการ PaymentClient, FxRateRepository, NotificationService, และ MetricsCollector ส่งผลให้เทสต์คอมไพล์พังด้วยข้อผิดพลาด No value passed for parameter 'notificationService' ซ้ำไปซ้ำมา

⚠️ LAB MISSION OBJECTIVES
  1. จำลองไฟล์คลาสเป้าหมาย FxOrderExecutionService.kt ที่มี 4 Injected Dependencies และ 2 เมธอดสาธารณะ (หนึ่งในนั้นเป็น suspend)
  2. รันสคริปต์ inspect-class-context.sh เพื่อพิสูจน์ว่าซอร์สโค้ด 720 บรรทัดถูกบีบอัดเหลือเป็น Symbol Contract ขนาด < 100 Tokens
  3. ตรวจสอบว่าสคริปต์ตรวจจับ Dependency ทั้ง 4 ตัวครบถ้วน และระบุแท็ก [SUSPEND] ได้ถูกต้องแม่นยำ 100%
❌ RAW SOURCE CODE (720 Lines / 28 KB)
package com.enterprise.trading.service

import ... (40 lines of imports)

@Service
class FxOrderExecutionService(
    private val paymentClient: PaymentClient,
    private val fxRateRepo: FxRateRepository,
    private val notificationService: NotificationService,
    private val metrics: MetricsCollector
) {
    // 650 lines of complex internal business logic,
    // private helper methods, calculation loops,
    // database transactions, logging, error handlers...
}
          
✅ FILTERED AST SKELETON (< 85 tokens)
### 📦 TARGET CLASS SKELETON CONTRACT
- **Package**: `com.enterprise.trading.service`
- **Class**: `FxOrderExecutionService`
- **Constructor Injected Dependencies (MUST mock via `mockk()`):**
  - `paymentClient: PaymentClient`
  - `fxRateRepo: FxRateRepository`
  - `notificationService: NotificationService`
  - `metrics: MetricsCollector`
- **Public Method Endpoints:**
  - `fun executeOrder(orderId: UUID, amount: BigDecimal): OrderResult` `[SUSPEND]`
  - `fun getHealthStatus(): ServiceHealth`
          
TERMINAL PROOF: Pre-flight AST Inspection Run Bash Output
$ bash .github/skills/spring-boot-unit-tester/scripts/inspect-class-context.sh src/main/kotlin/com/enterprise/trading/service/FxOrderExecutionService.kt
### 📦 TARGET CLASS SKELETON CONTRACT
- **Package**: `com.enterprise.trading.service`
- **Class**: `FxOrderExecutionService`
- **Constructor Injected Dependencies (MUST mock via `mockk()`):**
  - `paymentClient: PaymentClient`
  - `fxRateRepo: FxRateRepository`
  - `notificationService: NotificationService`
  - `metrics: MetricsCollector`
- **Public Method Endpoints:**
  - `fun executeOrder(orderId: UUID, amount: BigDecimal): OrderResult` `[SUSPEND]`
  - `fun getHealthStatus(): ServiceHealth`

💡 *Guidance: Mock all constructor dependencies. Wrap `[SUSPEND]` methods inside `runTest` using `coEvery`.*

$ echo $?
0
        
🎯 SECTION 5 / INTERACTIVE ARCHITECTURE & VERIFICATION QUIZ

ทดสอบสถาปัตยกรรม AST & Static Analysis Scripts (Day 7)

Current Score
0 / 12

ตอบคำถามเชิงสถาปัตยกรรมทั้ง 12 ข้อด้านล่าง ระบบจะตรวจคำตอบและให้ Feedback ทันทีเมื่อคลิกเลือกช้อยส์ พร้อมแสดงคำอธิบายเชิงลึก