← Phase 1 Phase 1.5 • Day 10 Safe Refactor

Local SE Agent Masterclass

MacBook M3 16GB • Complete Implementation Standard
📍 ส่วนที่ 1: หมุดตั้งโจทย์ & วัตถุประสงค์หลัก (Title Connection)

Chapter 10 — Safe Refactor & Unified Diff Patching

ทำไมต้องชื่อนี้และเรียนไปเพื่ออะไร?

ตั้งแต่ Day 1 ถึง Day 9 Agent ของเรามีความสามารถในการ **"อ่านโค้ด (Read-Only)"** และ **"รันเทส (Verify)"** ได้อย่างแม่นยำ แต่มันยังไม่สามารถ **"เขียนหรือแก้ไขโค้ดจริง (Write/Mutate)"** ได้เลย!

ปัญหาคอขาดบาดตายระดับองค์กร: ถ้าเราปล่อยให้ AI เขียนทับไฟล์ตรงๆ แบบทึ่มๆ (Blind Overwrite) เช่น file.write(new_code) แล้วถ้า LLM เกิดอาการหลอนทำโค้ดครึ่งไฟล์หาย หรือเกิด syntax error โค้ดต้นฉบับจะพังทลายทันทีและไม่มีทางกู้คืน!

Day 10 เราจึงสร้างเครื่องมือชิ้นที่ 3: Safe Refactor Tool โดยผสาน 3 กลไกความปลอดภัยระดับวิศวกรซอฟต์แวร์อาวุโส:
1. Automated Backup (.bak): สำเนาไฟล์เดิมเก็บไว้ทันทีก่อนแตะต้องไฟล์
2. Unified Diff Preview: คำนวณความต่างแบบมาตรฐาน Git (`--- a/` และ `+++ b/`) เพื่อแสดงให้มนุษย์เห็นว่าลบหรือเพิ่มบรรทัดไหนก่อนบันทึก
3. Instant Rollback Mechanism: กู้คืนไฟล์เดิมได้ใน 1 มิลลิวินาทีหากพบว่าผลลัพธ์ไม่ถูกต้อง!

✓ Day 1-7: Core Engine & CLI ✓ Day 8: Sliding Window Memory ✓ Day 9: ChromaDB Vector Store Day 10: Safe Refactor & Unified Diff 🛡️⚡
📂 ส่วนที่ 2: ผังโครงสร้างโปรเจกต์จริง & ตำแหน่งไฟล์ (Project File Structure)

โค้ดประจำ Day 10 จะเพิ่มเครื่องมือชิ้นที่ 3 ใน core/tools/code_refactor.py และสคริปต์ทดสอบ test_safe_refactor.py พร้อมส่งออกใน core/tools/__init__.py:

se_agent/
├── requirements.txt                       # ollama, chromadb (ไม่ต้องลงเพิ่ม ใช้ difflib ในตัว)
├── main.py                               # Personal CLI Entrypoint
├── test_safe_refactor.py                   # [ไฟล์ใหม่ Day 10] ทดสอบการ Patch โค้ด, ดู Diff, ทดลอง Rollback
├── memory/
│   ├── __init__.py
│   ├── context.py                          # Sliding Window Context (Day 8)
│   └── vector_store.py                     # Local ChromaDB Store (Day 9)
├── core/
│   ├── __init__.py
│   ├── client.py
│   ├── parser.py
│   ├── dispatcher.py
│   ├── engine.py
│   └── tools/
│       ├── __init__.py                     # [อัปเดต Day 10] Export: read_file, run_maven_test, apply_code_patch, rollback_file
│       ├── file_reader.py                  # Safe File Reader Tool (Day 4)
│       ├── test_runner.py                  # Maven Subprocess Tool (Day 5)
│       └── code_refactor.py                # [ไฟล์ใหม่ Day 10] Tool 3: Safe Refactor & Unified Diff Engine
└── sample_maven_project/
📍 core/tools/code_refactor.py ทำหน้าที่อะไร?

ทำหน้าที่เป็น Safe Mutation Layer ป้องกันการเขียนทับอันตราย โดยมีฟังก์ชัน apply_code_patch() ทำหน้าที่สำรองไฟล์ .bak คำนวณ Unified Diff ด้วย difflib และบันทึกไฟล์ใหม่ พร้อมฟังก์ชัน rollback_file() สำหรับดึงไฟล์เดิมกลับมาเมื่อเทสไม่ผ่าน

📍 test_safe_refactor.py ทำหน้าที่อะไร?

สคริปต์รันบน Terminal เพื่อทดลองแก้โค้ดที่มีบั๊กใน Calculator.java ตรวจสอบหน้าตาของ Unified Diff สีสันสดใส และทดสอบฟังก์ชันกู้คืน (Rollback) เพื่อยืนยันว่าโค้ดต้นฉบับจะไม่มีวันสูญหาย

🏛️ ส่วนที่ 3: สถาปัตยกรรม Safe Refactoring Pipeline (Mental Model)
วงจร 4 จังหวะของ Safe Refactoring Engine: Backup ➔ Diff ➔ Apply ➔ Rollback Ready
💾
1. Auto-Backup
`target.java.bak`

ก๊อปปี้ไฟล์ต้นฉบับเก็บไว้ทันทีในดิสก์ก่อนแตะต้องเนื้อหา

🔍
2. Unified Diff
`difflib.unified_diff`

เปรียบเทียบความต่างแบบ Git: เขียว (+) คือเพิ่ม, แดง (-) คือลบ

✍️
3. Safe Write
`Atomic Write (UTF-8)`

บันทึกโค้ดใหม่ลงไฟล์เป้าหมาย พร้อมส่ง Diff กลับให้ LLM/มนุษย์

⏪
4. Instant Rollback
`shutil.copyfile`

หากรันเทสแล้วพัง สั่งกู้คืนไฟล์ .bak ทับกลับได้ใน 1 มิลลิวินาที!

ทำไม Unified Diff ถึงเป็นมาตรฐานสากลที่โปรแกรมเมอร์ทั่วโลกยอมรับ?
❌ ถ้าให้ AI เขียนทับไฟล์ตรงๆ โดยไม่มี Diff:

1. มนุษย์ไม่รู้เลยว่า AI แอบเปลี่ยนอะไรบ้างในไฟล์ 500 บรรทัด
2. AI อาจเผลอลบคอมเมนต์สำคัญ ฟังก์ชันข้างเคียง หรือนำเข้าไลบรารีแปลกปลอม
3. ถ้าเกิดบั๊ก คุณจะต้องเสียเวลาไล่โค้ดทีละบรรทัดด้วยตาเปล่า

✅ เมื่อมี Unified Diff Engine:

1. แสดงชัดเจนเหมือนคำสั่ง git diff: เห็นเฉพาะจุดที่แก้และบริบทแวดล้อม 3 บรรทัด
2. มนุษย์สามารถกวาดสายตาตรวจ (Human-in-the-loop review) ได้ใน 5 วินาที
3. มีไฟล์ .bak สำรองการันตี Zero-Risk ไม่มีวันสูญเสียโค้ดเดิมเด็ดขาด!

💻 ส่วนที่ 4: ซอร์สโค้ดจริง & คำอธิบายทีละบรรทัด (Line-by-Line Implementation)
📄 1. core/tools/__init__.py (อัปเดต Export เครื่องมือใหม่) เปิดเผยฟังก์ชัน Refactor & Rollback สู่ภายนอก
"""
Package รวบรวมเครื่องมือ (Tools) ปฏิบัติการของ Agent
[อัปเดต Day 10]: เพิ่ม Safe Refactoring & Rollback Engine
"""
from core.tools.file_reader import read_file, FileAccessError
from core.tools.test_runner import run_maven_test, BuildExecutionError
from core.tools.code_refactor import apply_code_patch, rollback_file, RefactorExecutionError

__all__ = [
    "read_file",
    "FileAccessError",
    "run_maven_test",
    "BuildExecutionError",
    "apply_code_patch",
    "rollback_file",
    "RefactorExecutionError"
]
📄 2. core/tools/code_refactor.py (ไฟล์ใหม่ประจำ Day 10) เอนจินจัดการแก้โค้ดอย่างปลอดภัย คำนวณ Unified Diff และระบบกู้คืน
import difflib
import shutil
from pathlib import Path
from typing import Dict, Any, Optional

class RefactorExecutionError(Exception):
    """Custom exception สำหรับข้อผิดพลาดระหว่างกระบวนการแก้ไขโค้ด"""
    pass

def apply_code_patch(
    filepath: str,
    new_code: str,
    workspace_root: Optional[str] = None
) -> Dict[str, Any]:
    """
    เครื่องมือสำหรับแก้ไขโค้ดอย่างปลอดภัย (Safe Code Refactor Tool):
    1. ตรวจสอบ Path Traversal Guardrail
    2. ทำสำเนา Backup ไฟล์เดิม (.bak) ทันที
    3. คำนวณ Unified Diff แสดงความเปลี่ยนแปลงแบบ Git
    4. บันทึกโค้ดใหม่ลงไฟล์เป้าหมาย
    
    :param filepath: ที่อยู่ไฟล์สัมพัทธ์ เช่น 'src/main/java/Calculator.java'
    :param new_code: เนื้อหาโค้ดใหม่ทั้งหมดที่ต้องการนำไปเขียนทับ
    :param workspace_root: โฟลเดอร์รากที่อนุญาต (ป้องกันการแก้ไฟล์นอกโฟลเดอร์)
    :return: ดิกชันนารีสรุปผลลัพธ์พร้อมข้อความ Unified Diff และ path ของไฟล์สำรอง
    """
    try:
        # 1. กำหนด Workspace Root และคำนวณ Absolute Path
        root = Path(workspace_root).resolve() if workspace_root else Path.cwd().resolve()
        target_path = (root / filepath).resolve()

        # 2. [Security Guardrail]: ป้องกัน Path Traversal
        if not str(target_path).startswith(str(root)):
            raise RefactorExecutionError(f"Security Violation: ไม่อนุญาตให้แก้ไขไฟล์นอก Workspace '{filepath}'")

        if not target_path.exists():
            raise RefactorExecutionError(f"ไม่พบไฟล์ที่ต้องการแก้ไข: '{filepath}'")

        if not target_path.is_file():
            raise RefactorExecutionError(f"พิกัด '{filepath}' เป็นโฟลเดอร์ ไม่ใช่ไฟล์")

        # 3. อ่านโค้ดดั้งเดิมออกมา
        original_code = target_path.read_text(encoding="utf-8", errors="replace")

        # 4. [Automated Backup]: สร้างสำเนาไฟล์เดิม เช่น Calculator.java.bak
        backup_path = target_path.with_suffix(target_path.suffix + ".bak")
        shutil.copyfile(target_path, backup_path)

        # 5. [Unified Diff Computation]: คำนวณความต่างของโค้ด
        # splitlines(keepends=True) เพื่อรักษาตัวแบ่งบรรทัด \n ไว้
        original_lines = original_code.splitlines(keepends=True)
        new_lines = new_code.splitlines(keepends=True)

        diff = difflib.unified_diff(
            original_lines,
            new_lines,
            fromfile=f"a/{target_path.name}",
            tofile=f"b/{target_path.name}",
            n=3  # แสดงบริบทแวดล้อม 3 บรรทัดรอบจุดแก้ไข
        )
        diff_text = "".join(diff).strip()

        # หากโค้ดไม่มีการเปลี่ยนแปลงเลย
        if not diff_text:
            return {
                "status": "unchanged",
                "message": "โค้ดใหม่เหมือนเดิมทุกประการ ไม่มีการเปลี่ยนแปลง",
                "filepath": str(target_path),
                "backup_path": str(backup_path),
                "diff": ""
            }

        # 6. บันทึกโค้ดใหม่ลงไฟล์จริง
        target_path.write_text(new_code, encoding="utf-8")

        return {
            "status": "success",
            "message": f"แก้ไขไฟล์ '{target_path.name}' สำเร็จเรียบร้อย",
            "filepath": str(target_path),
            "backup_path": str(backup_path),
            "diff": diff_text
        }

    except RefactorExecutionError:
        raise
    except Exception as err:
        raise RefactorExecutionError(f"เกิดข้อผิดพลาดในการแก้ไขไฟล์ '{filepath}': {str(err)}")

def rollback_file(filepath: str, workspace_root: Optional[str] = None) -> str:
    """
    เครื่องมือฉุกเฉินสำหรับกู้คืนไฟล์เดิมจากสำเนา (.bak)
    ใช้งานเมื่อ Agent รันเทสหลังแก้โค้ดแล้วเทสล้มเหลว
    
    :param filepath: ที่อยู่ไฟล์สัมพัทธ์
    :return: ข้อความยืนยันการกู้คืนสำเร็จ
    """
    try:
        root = Path(workspace_root).resolve() if workspace_root else Path.cwd().resolve()
        target_path = (root / filepath).resolve()
        backup_path = target_path.with_suffix(target_path.suffix + ".bak")

        if not backup_path.exists():
            raise RefactorExecutionError(f"ไม่พบไฟล์สำรอง (.bak) สำหรับ: '{filepath}'")

        # ก๊อปปี้ไฟล์สำรองกลับมาทับไฟล์จริง
        shutil.copyfile(backup_path, target_path)

        return f"⏪ สำเร็จ: กู้คืนไฟล์ '{target_path.name}' จากสำเนาเรียบร้อย โค้ดกลับสู่สถานะปลอดภัยเดิม 100%"

    except Exception as err:
        raise RefactorExecutionError(f"ล้มเหลวในการกู้คืนไฟล์ '{filepath}': {str(err)}")
ชำแหละไวยากรณ์สำคัญใน core/tools/code_refactor.py
backup_path = target_path.with_suffix(target_path.suffix + ".bak")

Fail-Safe Backup: เมธอด with_suffix ใน pathlib.Path ช่วยเติมต่อท้ายนามสกุลไฟล์เดิม เช่น Calculator.java กลายเป็น Calculator.java.bak รับประกันว่ามีจุดเซฟสำรองเสมอ

difflib.unified_diff(original_lines, new_lines, fromfile=..., tofile=..., n=3)

Standard Unified Diff: ไลบรารีมาตรฐานของ Python ที่สร้างข้อความเปรียบเทียบความต่างแบบเดียวกับคำสั่ง git diff แท้ๆ โดยพารามิเตอร์ n=3 จะคง 3 บรรทัดรอบจุดแก้ไขไว้เป็นบริบท

shutil.copyfile(backup_path, target_path)

Instant Rollback: คำสั่งระดับระบบปฏิบัติการที่เขียนทับไฟล์เดิมด้วยไฟล์สำรองในทันที ทำงานรวดเร็วระดับเศษเสี้ยววินาทีเพื่อคืนสภาพความปลอดภัย

📄 3. test_safe_refactor.py (สคริปต์ทดสอบการ Patch, ดู Diff และ Rollback) ทดลองแก้ไขบั๊กใน Calculator.java พร้อมทดสอบระบบกู้คืน
import shutil
from pathlib import Path
from core.tools.code_refactor import apply_code_patch, rollback_file

def main():
    print("🚀 เริ่มทดสอบ Day 10: Safe Refactor & Unified Diff Patching บน Mac M3...")

    mock_dir = Path("./temp_refactor_test").resolve()
    mock_dir.mkdir(parents=True, exist_ok=True)
    target_file = mock_dir / "Calculator.java"

    try:
        # 1. สร้างไฟล์โค้ดต้นฉบับที่มีบั๊ก (ลดราคาคูณ 2 ผิดสูตร)
        initial_code = (
            "public class Calculator {\n"
            "    public double applyDiscount(double price, double discount) {\n"
            "        // บั๊กเดิม: ลบส่วนลด 2 เท่า\n"
            "        return price - (discount * 2.0);\n"
            "    }\n"
            "}\n"
        )
        target_file.write_text(initial_code, encoding="utf-8")
        print(f"📁 1. สร้างไฟล์ทดสอบที่: {target_file.name}")

        # 2. เตรียมโค้ดแก้ไขที่ถูกต้อง
        fixed_code = (
            "public class Calculator {\n"
            "    public double applyDiscount(double price, double discount) {\n"
            "        // แก้ไขถูกต้อง: ลบส่วนลดตามจริง\n"
            "        return price - discount;\n"
            "    }\n"
            "}\n"
        )

        print("\n" + "=" * 60)
        print("🛠️ 2. ดำเนินการแก้ไขโค้ดด้วย apply_code_patch():")
        print("=" * 60)

        # 3. สั่ง Patch โค้ด
        result = apply_code_patch(
            filepath="Calculator.java",
            new_code=fixed_code,
            workspace_root=str(mock_dir)
        )

        print(f"✅ สถานะ: {result['status']}")
        print(f"💾 สำเนาถูกสร้างที่: {Path(result['backup_path']).name}")
        print("\n🔍 แสดง Unified Diff Preview (สิ่งที่เปลี่ยนไป):")
        print("-" * 50)
        # แสดง Diff สีสันจำลอง
        for line in result["diff"].splitlines():
            if line.startswith("+"):
                print(f"\033[92m{line}\033[0m")  # สีเขียว = เพิ่ม
            elif line.startswith("-"):
                print(f"\033[91m{line}\033[0m")  # สีแดง = ลบ
            else:
                print(line)
        print("-" * 50)

        # ตรวจสอบว่าโค้ดใหม่ถูกเขียนจริง
        current_content = target_file.read_text(encoding="utf-8")
        assert "return price - discount;" in current_content, "❌ โค้ดใหม่ไม่ถูกบันทึก!"
        assert (mock_dir / "Calculator.java.bak").exists(), "❌ ไม่พบไฟล์ Backup!"
        print("🎉 Patch โค้ดสำเร็จสมบูรณ์ 100%!")

        # 4. ทดสอบระบบ Rollback ฉุกเฉิน
        print("\n" + "=" * 60)
        print("⏪ 3. ทดสอบระบบ Rollback ฉุกเฉิน (สมมติว่าเทสล้มเหลว):")
        print("=" * 60)
        rollback_msg = rollback_file("Calculator.java", workspace_root=str(mock_dir))
        print(rollback_msg)

        # ตรวจสอบว่าโค้ดกลับมาเป็นแบบเดิมจริงไหม
        restored_content = target_file.read_text(encoding="utf-8")
        assert "return price - (discount * 2.0);" in restored_content, "❌ การ Rollback ล้มเหลว!"
        print("✅ การันตี 100%: โค้ดเดิมถูกกู้คืนกลับมาอย่างแม่นยำ ปลอดภัยไร้ความเสี่ยง!")

    finally:
        # ทำความสะอาดโฟลเดอร์ทดสอบ
        shutil.rmtree(mock_dir, ignore_errors=True)

if __name__ == "__main__":
    main()
⚡ ส่วนที่ 5: ปฏิบัติการจริงบน Terminal Mac M3 (Step-by-Step Lab)

เปิด Terminal บน MacBook M3 แล้วทำตาม 4 ขั้นตอนนี้เพื่อติดตั้งเครื่องมือ Safe Refactor และทดสอบความปลอดภัยทันที:

ขั้นตอนที่ 1: เข้าโฟลเดอร์โปรเจกต์และเปิด Virtual Environment
cd ~/se_agent
source .venv/bin/activate
ขั้นตอนที่ 2: สร้างไฟล์ใหม่สำหรับ Day 10
touch core/tools/code_refactor.py test_safe_refactor.py

หมายเหตุ: ฟังก์ชัน Diff ใช้ไลบรารีมาตรฐาน difflib และ shutil ที่มีใน Python อยู่แล้ว ไม่ต้องติดตั้งแพ็กเกจภายนอกเพิ่ม

ขั้นตอนที่ 3: วางโค้ดลงในแต่ละไฟล์

- อัปเดตไฟล์ core/tools/__init__.py ตาม ส่วนที่ 4 (ไฟล์ที่ 1)
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 2) ไปวางใน core/tools/code_refactor.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 3) ไปวางใน test_safe_refactor.py

ขั้นตอนที่ 4: สั่งรันสคริปต์ทดสอบผลลัพธ์
python test_safe_refactor.py

🎉 ผลลัพธ์: คุณจะเห็นหน้าจอ Terminal แสดงผลลัพธ์ Unified Diff แถบสีเขียว (+) และสีแดง (-) ชัดเจนว่าบรรทัดไหนถูกลบและเพิ่ม พร้อมทั้งเห็นการกู้คืน (Rollback) ไฟล์กลับสู่สภาพเดิมได้ในเสี้ยววินาที!

📝 ส่วนที่ 6: ชุดควิซประเมินความรู้เชิงวิศวกรรม 15 ข้อ (ตอบปุ๊บรู้ผลทันที)

ข้อที่ 1: วัตถุประสงค์หลักของการสร้าง "Safe Refactor & Unified Diff Tool" ใน Day 10 คืออะไร?

ข้อที่ 2: ความเสี่ยงสูงสุดของการปล่อยให้ AI เขียนทับไฟล์ตรงๆ ด้วยคำสั่ง `file.write()` โดยไม่มีระบบความปลอดภัยคืออะไร?

ข้อที่ 3: ไลบรารีมาตรฐานใดใน Python ที่ใช้สำหรับคำนวณความแตกต่างของข้อความในรูปแบบ Unified Diff?

ข้อที่ 4: สัญลักษณ์ `---` และ `+++` ในผลลัพธ์ของ Unified Diff บ่งบอกถึงสิ่งใด?

ข้อที่ 5: พารามิเตอร์ `n=3` ในฟังก์ชัน `difflib.unified_diff()` มีความหมายอย่างไร?

ข้อที่ 6: การสร้างสำเนาไฟล์ด้วยนามสกุล `.bak` (เช่น `Calculator.java.bak`) ทำหน้าที่เป็นอะไรในสถาปัตยกรรม?

ข้อที่ 7: ฟังก์ชัน `rollback_file()` ควรถูกเรียกใช้งานในจังหวะใดของลูป ReAct?

ข้อที่ 8: เหตุใดใน `apply_code_patch` เราจึงต้องใช้ `code.splitlines(keepends=True)` แทนการใช้ `splitlines()` ธรรมดา?

ข้อที่ 9: การตรวจสอบ `if not str(target_path).startswith(str(root)):` ในฟังก์ชันแก้ไขโค้ดมีประโยชน์อย่างไร?

ข้อที่ 10: หากโค้ดใหม่ที่ AI ส่งมามีเนื้อหาเหมือนเดิมทุกประการ ฟังก์ชัน `apply_code_patch` ควรตอบสนองอย่างไร?

ข้อที่ 11: เมื่อ Agent แก้ไขไฟล์สำเร็จแล้วส่ง Diff กลับมา การแสดงผล Diff ให้โปรแกรมเมอร์มนุษย์ตรวจก่อนสอดคล้องกับแนวคิดใด?

ข้อที่ 12: การแยกโมดูล `core/tools/code_refactor.py` ออกจาก `file_reader.py` และ `test_runner.py` สอดคล้องกับหลักการใด?

ข้อที่ 13: ในการพัฒนาซอฟต์แวร์ระดับ Enterprise เครื่องมือ Refactoring ของ Agent จะถูกยกระดับใน Day 22 ไปสู่สิ่งใด?

ข้อที่ 14: ทำไมการรัน Safe Refactor บน Mac M3 16GB ถึงเร็วและกินแรมน้อยมาก?

ข้อที่ 15: ในฐานะวิศวกรซอฟต์แวร์ เมื่อคุณติดอาวุธ Safe Refactor + Unified Diff ให้ Agent แก้โค้ดจริงได้สำเร็จ รู้สึกอย่างไร?

🏁 สรุปหมุดเชื่อมโยง Day 10 (Chapter Wrap-up)

ใน Day 10 — Safe Refactor & Unified Diff Patching คุณได้สร้างโมดูล core/tools/code_refactor.py ติดตั้งเครื่องมือตัวที่ 3 ให้ Agent มี "มือสำหรับแก้ไขโค้ดจริง (Mutation Layer)" โดยมีระบบความปลอดภัยมาตรฐานวิศวกรรมระดับสูง: Auto-Backup (.bak), Unified Diff Preview (Git-style), และ Instant Rollback ทำให้การแก้โค้ดไม่มีความเสี่ยงต่อการพังทลายของโปรเจกต์ ตอนนี้ Agent ของคุณมีอวัยวะครบเครื่องแล้ว ทั้งตาอ่านโค้ด (Day 4), เท้าสั่งรันเทส (Day 5), สมองลูปอัตโนมัติ (Day 6), ความจำระยะสั้น (Day 8), ความจำระยะยาว (Day 9), และมือแก้โค้ด (Day 10) พร้อมก้าวเข้าสู่ Day 11: CLI & Interactive Terminal Interface เพื่ออัปเกรดหน้าจอห้องบังคับการให้สวยงามระดับโปรดักชันแล้วครับ!