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 10 จะเพิ่มเครื่องมือชิ้นที่ 3 ใน core/tools/code_refactor.py และสคริปต์ทดสอบ test_safe_refactor.py พร้อมส่งออกใน core/tools/__init__.py:
ทำหน้าที่เป็น Safe Mutation Layer ป้องกันการเขียนทับอันตราย โดยมีฟังก์ชัน apply_code_patch() ทำหน้าที่สำรองไฟล์ .bak คำนวณ Unified Diff ด้วย difflib และบันทึกไฟล์ใหม่ พร้อมฟังก์ชัน rollback_file() สำหรับดึงไฟล์เดิมกลับมาเมื่อเทสไม่ผ่าน
สคริปต์รันบน Terminal เพื่อทดลองแก้โค้ดที่มีบั๊กใน Calculator.java ตรวจสอบหน้าตาของ Unified Diff สีสันสดใส และทดสอบฟังก์ชันกู้คืน (Rollback) เพื่อยืนยันว่าโค้ดต้นฉบับจะไม่มีวันสูญหาย
ก๊อปปี้ไฟล์ต้นฉบับเก็บไว้ทันทีในดิสก์ก่อนแตะต้องเนื้อหา
เปรียบเทียบความต่างแบบ Git: เขียว (+) คือเพิ่ม, แดง (-) คือลบ
บันทึกโค้ดใหม่ลงไฟล์เป้าหมาย พร้อมส่ง Diff กลับให้ LLM/มนุษย์
หากรันเทสแล้วพัง สั่งกู้คืนไฟล์ .bak ทับกลับได้ใน 1 มิลลิวินาที!
1. มนุษย์ไม่รู้เลยว่า AI แอบเปลี่ยนอะไรบ้างในไฟล์ 500 บรรทัด
2. AI อาจเผลอลบคอมเมนต์สำคัญ ฟังก์ชันข้างเคียง หรือนำเข้าไลบรารีแปลกปลอม
3. ถ้าเกิดบั๊ก คุณจะต้องเสียเวลาไล่โค้ดทีละบรรทัดด้วยตาเปล่า
1. แสดงชัดเจนเหมือนคำสั่ง git diff: เห็นเฉพาะจุดที่แก้และบริบทแวดล้อม 3 บรรทัด
2. มนุษย์สามารถกวาดสายตาตรวจ (Human-in-the-loop review) ได้ใน 5 วินาที
3. มีไฟล์ .bak สำรองการันตี Zero-Risk ไม่มีวันสูญเสียโค้ดเดิมเด็ดขาด!
"""
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"
]
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: คำสั่งระดับระบบปฏิบัติการที่เขียนทับไฟล์เดิมด้วยไฟล์สำรองในทันที ทำงานรวดเร็วระดับเศษเสี้ยววินาทีเพื่อคืนสภาพความปลอดภัย
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()
เปิด Terminal บน MacBook M3 แล้วทำตาม 4 ขั้นตอนนี้เพื่อติดตั้งเครื่องมือ Safe Refactor และทดสอบความปลอดภัยทันที:
cd ~/se_agent
source .venv/bin/activate
touch core/tools/code_refactor.py test_safe_refactor.py
หมายเหตุ: ฟังก์ชัน Diff ใช้ไลบรารีมาตรฐาน difflib และ shutil ที่มีใน Python อยู่แล้ว ไม่ต้องติดตั้งแพ็กเกจภายนอกเพิ่ม
- อัปเดตไฟล์ core/tools/__init__.py ตาม ส่วนที่ 4 (ไฟล์ที่ 1)
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 2) ไปวางใน core/tools/code_refactor.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 3) ไปวางใน test_safe_refactor.py
python test_safe_refactor.py
🎉 ผลลัพธ์: คุณจะเห็นหน้าจอ Terminal แสดงผลลัพธ์ Unified Diff แถบสีเขียว (+) และสีแดง (-) ชัดเจนว่าบรรทัดไหนถูกลบและเพิ่ม พร้อมทั้งเห็นการกู้คืน (Rollback) ไฟล์กลับสู่สภาพเดิมได้ในเสี้ยววินาที!
ข้อที่ 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 เพื่ออัปเกรดหน้าจอห้องบังคับการให้สวยงามระดับโปรดักชันแล้วครับ!