teach-me-ai / sources / MP-3
⟵ กลับหน้าแทร็กเสริม
MP-3 · mattpocock/skills

ลงมือทำ + Feedback loops (Implement, TDD, Debug)

⏱️ ~22 นาที 🎯 สร้างงานจาก spec/tickets โดยมี feedback loop ที่ดี 📦 ต้นทาง: skills/implement · tdd · diagnosing-bugs

บทที่แล้ว (MP-2) เราคุยกับ agent จนได้ spec ที่ชัด และแตกเป็น tickets ที่ทำได้จริง บทนี้คือขั้นที่สาม — เอา spec/tickets นั้นมาลงมือทำจริง แต่ประเด็นที่ mattpocock/skills เน้นไม่ใช่แค่ "เขียนโค้ดให้จบ" — สิ่งที่เน้นคือ feedback loop ที่ต้องมีอยู่ทุกขั้นตอน ตั้งแต่เขียนโค้ด ไปจนถึงตอนเจอบั๊ก ถ้าไม่มี feedback loop ที่ดี agent (และตัวคุณเอง) จะกลายเป็น "เขียนแล้วหวังว่าคงถูก" ซึ่งเป็นสาเหตุอันดับต้นๆ ที่งานพังตอน production

1. implement — จาก spec/tickets สู่โค้ดจริง

skills/engineering/implement/SKILL.md สั้นมากแค่ไม่กี่บรรทัด แต่คุมทั้ง workflow ของการลงมือทำงาน สังเกตว่าเป็น skill แบบ disable-model-invocation: true — ต้องถูกเรียกโดยผู้ใช้เอง (เช่นพิมพ์ /implement) ไม่ auto-trigger เอง เพราะการ "เริ่มเขียนโค้ดจริง" ควรเป็นการตัดสินใจที่คนสั่งชัดเจน ไม่ใช่ agent เดาเอาว่าถึงเวลาแล้ว

// skills/engineering/implement/SKILL.md (เนื้อหาทั้งไฟล์)
Implement the work described by the user in the spec or tickets.

Use /tdd where possible, at pre-agreed seams.

Run typechecking regularly, single test files regularly,
and the full test suite once at the end.

Once done, use /code-review to review the work.

Commit your work to the current branch.

แตกเป็นขั้นตอนที่ agent ต้องทำตามลำดับ:

  1. รับ input คือ spec หรือ tickets ที่ตกลงกันไว้แล้ว (ผลจาก MP-2) — ไม่ใช่เดาเอาเองว่าต้องทำอะไร
  2. ขับด้วย /tdd ที่ seam ที่ตกลงไว้ล่วงหน้าเท่านั้น — ไม่ใช่ test ทุกจุดที่นึกออก
  3. รัน typecheck บ่อยๆ ระหว่างเขียน (ไม่ใช่รอจบแล้วค่อยรันทีเดียว)
  4. รัน single test file บ่อยๆ ระหว่างเขียนแต่ละ slice + full test suite ครั้งเดียวตอนจบ ก่อนเคลมว่าเสร็จ
  5. ปิดด้วย /code-review ก่อน commit เสมอ — ไม่ commit งานที่ยังไม่ผ่านสายตาตรวจสอบ
  6. commit ลง branch ปัจจุบัน
💡

สังเกตลำดับ: typecheck + single test ระหว่างทาง (บ่อยๆ, เร็ว) → full suite ครั้งเดียวตอนจบ (ช้ากว่าแต่ครอบคลุม) → code-review → commit นี่คือ pattern ที่ป้องกัน "เขียนไปเรื่อยๆ 2 ชั่วโมงแล้วค่อยรัน test ทีเดียวเจอพังระเบิด"

2. Feedback loop คือหัวใจของทุกขั้นตอน

ทำไม implement ถึงย้ำเรื่องรัน typecheck/test "บ่อยๆ" ขนาดนี้? เพราะสิ่งที่ทำให้ agent (หรือ dev คนหนึ่ง) ไม่หลงทางระหว่างเขียนโค้ด คือการมี สัญญาณที่บอกถูก/ผิดได้ทันที สามแบบหลักที่ผูกกับงานจริงของคุณ:

Feedback loopสัญญาณจากอะไรตัวอย่างในงานคุณ
Static types implement: "Run typechecking regularly" tsc --noEmit ใน NestJS, go build/go vet ใน Go
Automated tests implement: "single test files regularly + full suite once" jest retry.test.ts ทีละไฟล์ระหว่างเขียน แล้ว npm test เต็มตอนจบ
CLI / browser access diagnosing-bugs: curl script, CLI invocation, headless browser script curl localhost:3000/api/users, Playwright script เช็ค DOM จริง
เขียนโค้ด ทีละ slice เล็กๆ สัญญาณ feedback type error / test แดง curl / browser output อ่านสัญญาณ แล้วแก้จุดนั้น วนซ้ำทีละ slice — ไม่มี loop นี้ = agent เขียนแล้วเดาเอาว่าถูก
Feedback loop 3 แบบที่ implement ต้องใช้ระหว่างทาง ไม่ใช่รอไปเช็คทีเดียวตอนจบ

3. tdd — red/green loop ที่ทำให้ implement เชื่อถือได้

เมื่อ implement บอกให้ "Use /tdd where possible, at pre-agreed seams" นี่คือจุดที่ไปเปิด skills/engineering/tdd/SKILL.md ข้อสำคัญที่ต่างจาก TDD แบบทั่วไปที่คุณอาจเคยได้ยิน (red-green-refactor) คือ mattpocock/skills แยก refactor ออกจาก loop ชัดเจน:

"Refactoring is not part of the loop. It belongs to the review stage (see the code-review skill), not the red → green implementation cycle."

พูดง่ายๆ คือ ตอนอยู่ใน loop ของ /tdd มีแค่ 2 จังหวะ: RED (เขียน test ให้ fail ก่อน) → GREEN (เขียนโค้ดขั้นต่ำให้ผ่าน) แล้ว "refactor" ไปเกิดตอน /code-review ตอนท้ายของ implement ไม่ใช่แทรกอยู่ระหว่าง cycle เหมือนสูตรคลาสสิก

Seam — ต้องตกลงก่อนเขียน test บรรทัดแรก

Seam คือขอบเขต public ที่ test จะยืนอยู่ — ไฟล์กำชับว่า "Test only at pre-agreed seams" ก่อนเขียน test ต้องเขียนรายการ seam ที่จะ test แล้ว "confirm กับผู้ใช้" ก่อนเสมอ ห้ามเขียน test ที่ seam ที่ยังไม่ได้ตกลง — เหตุผลคือ test ทุกจุดทำไม่ได้ (และไม่คุ้ม) การตกลง seam ล่วงหน้าคือวิธีให้ effort ของการ test ไปตกที่ critical path จริงๆ

ลักษณะของ test ที่ดี vs ไม่ดี

จาก tdd/tests.md: test ที่ดีต้อง ทดสอบผ่าน public interface ไม่ใช่ไปเจาะ internal — "โค้ดข้างในเปลี่ยนได้หมด แต่ test ไม่ควรต้องแก้ตาม"

// GOOD — ทดสอบ observable behavior ผ่าน public API
test("user can checkout with valid cart", async () => {
  const cart = createCart();
  cart.add(product);
  const result = await checkout(cart, paymentMethod);
  expect(result.status).toBe("confirmed");
});

// BAD — ผูกกับ implementation detail (mock internal collaborator)
test("checkout calls paymentService.process", async () => {
  const mockPayment = jest.mock(paymentService);
  await checkout(cart, payment);
  expect(mockPayment.process).toHaveBeenCalledWith(cart.total);
});
// ↑ พอ refactor ภายในโดย behavior ไม่เปลี่ยน test นี้พังทันที

อีก anti-pattern ที่พลาดกันบ่อยคือ tautological test — expected value คำนวณด้วยวิธีเดียวกับโค้ดจริง ทำให้ test ผ่านเสมอโดยไม่ได้พิสูจน์อะไร:

// BAD — expected คำนวณด้วยสูตรเดียวกับโค้ด จึงผ่านเสมอ
const expected = items.reduce((sum, i) => sum + i.price, 0);
expect(calculateTotal(items)).toBe(expected);

// GOOD — expected เป็นตัวเลขจริงที่รู้อยู่แล้ว (independent source of truth)
expect(calculateTotal([{ price: 10 }, { price: 5 }])).toBe(15);

และ anti-pattern ที่สามคือ horizontal slicing — เขียน test ทั้งหมดก่อน แล้วค่อยเขียน implementation ทั้งหมด ไฟล์เตือนว่านี่ทำให้ test กลายเป็นการทดสอบ "รูปร่างที่จินตนาการไว้" ไม่ใช่ behavior จริง แนะนำให้ทำแบบ vertical slice แทน — หนึ่ง test → หนึ่ง implementation → วนซ้ำ แต่ละ test เป็นเหมือน tracer bullet ที่ตอบสนองกับสิ่งที่เพิ่งเรียนรู้จาก cycle ก่อน

⚠️

กฎของ loop จาก SKILL.md โดยตรง: "Red before green" — เขียน test ที่ fail ก่อนเสมอ แล้วเขียนโค้ดขั้นต่ำให้ผ่านเท่านั้น ห้ามคาดเดา test ในอนาคตหรือใส่ feature ที่ยังไม่มีใครขอ (YAGNI) และ "One seam, one test, one minimal implementation per cycle" — ทีละก้าวเท่านั้น

4. diagnosing-bugs — เมื่อ implement ไปเจอบั๊ก

ระหว่าง implement ถ้าเจอบั๊กที่ไม่ใช่แค่ "เขียน test ยังไม่ผ่าน" ธรรมดา (เช่น bug เก่าที่ซ่อนอยู่ หรือ regression) จะไปใช้ skills/engineering/diagnosing-bugs/SKILL.md ซึ่งมี 6 phase แต่ phase แรกคือ หัวใจของ skill ทั้งตัว:

"This is the skill. Everything else is mechanical. If you have a tight pass/fail signal for the bug ... you will find the cause ... If you don't have one, no amount of staring at code will save you."
Phaseทำอะไรเสร็จเมื่อ
1. Build feedback loop สร้างคำสั่งเดียวที่ pass/fail ได้ทันทีบนบั๊กนี้ — failing test, curl script, CLI invocation, headless browser script, replay trace, ฯลฯ Red-capable + deterministic + fast (วินาที ไม่ใช่นาที) + agent-runnable
2. Reproduce + minimise รัน loop จนเห็น fail ตรงกับอาการที่ผู้ใช้บอกจริง แล้วตัด input/step ทีละอย่างจนเหลือแค่สิ่งที่ load-bearing ตัดอะไรออกอีกไม่ได้แล้ว (ตัดแล้ว loop เขียวทันที)
3. Hypothesise ตั้ง 3–5 สมมติฐาน เรียงลำดับ แต่ละข้อต้อง falsifiable ("ถ้า X คือสาเหตุ เปลี่ยน Y แล้วบั๊กหาย") โชว์ ranked list ให้ผู้ใช้ดูก่อนเริ่มทดสอบ
4. Instrument debugger/REPL ก่อน (breakpoint ดีกว่า log สิบบรรทัด) รองลงมาคือ targeted log ที่ tag ด้วย prefix เฉพาะ เช่น [DEBUG-a4f2] แยกได้ว่าสมมติฐานไหนถูก เปลี่ยนทีละตัวแปรเท่านั้น
5. Fix + regression test เขียน regression test จาก repro ที่ minimise แล้ว "ก่อน" แก้จริง — แต่ต้องมี seam ที่ถูกต้องเท่านั้น test แดงก่อนแก้ → แก้ → test เขียว → รี loop เดิม (ที่ยังไม่ minimise) อีกครั้ง
6. Cleanup + post-mortem ลบ log ที่ tag ไว้ทั้งหมด (grep prefix เจอหมด), ลบ throwaway harness, สรุปสมมติฐานที่ถูกไว้ใน commit message repro เดิม (ตัวเต็ม ไม่ใช่ minimised) ไม่ reproduce ซ้ำอีกแล้ว

จุดที่คนมักข้าม: SKILL.md เตือนตรงๆ ว่า "If you catch yourself reading code to build a theory before this command exists, stop — jumping straight to a hypothesis is the exact failure this skill prevents." พูดง่ายๆ คือ ห้ามเดา root cause ก่อนมี loop ที่รันซ้ำได้จริง ต่อให้มั่นใจแค่ไหนก็ตาม

💡

ผูกกับงานจริง: PostgreSQL constraint fail แบบงงๆ หรือ MongoDB query คืนค่าผิด — วิธีที่ SKILL.md แนะนำลำดับแรกคือ เขียน failing test (unit/integration) ที่ยิง query เดิม ถ้าทำไม่ได้ให้ลองต่อ curl/CLI script ยิง endpoint จริง ก่อนจะไล่ log ทีละบรรทัด — "log ทุกอย่างแล้ว grep" คือสิ่งที่ SKILL.md บอกชัดว่า "Never"

ผูกงานจริง: vertical slice ใน NestJS ด้วย Jest

สมมติ ticket จาก MP-2 บอกว่า "เพิ่ม endpoint คืนสถานะ order ว่า SHIPPED เมื่อมี tracking number แล้ว" ก่อนเขียน test ต้องยืนยัน seamก่อน (ตามกฎ tdd) — ในที่นี้ตกลงกันว่า seam คือ service method getOrderStatus() ไม่ใช่ล้วงเข้าไปเช็ค repository โดยตรง

// RED — order.service.spec.ts (เขียนก่อน ยังไม่มี logic นี้)
test("returns SHIPPED when order has a tracking number", async () => {
  const order = buildOrder({ trackingNumber: "TH123" });

  const status = await service.getOrderStatus(order.id);

  expect(status).toBe("SHIPPED");
});

// $ npx jest order.service.spec.ts
// FAIL — getOrderStatus ยังคืน "PENDING" เสมอ  ← fail ถูกเหตุผล
// GREEN — order.service.ts (เขียนขั้นต่ำแค่พอผ่าน)
async getOrderStatus(id: string): Promise<OrderStatus> {
  const order = await this.repo.findById(id);
  if (order.trackingNumber) return "SHIPPED";
  return "PENDING";
}
// $ npx jest order.service.spec.ts
// PASS (1/1)  →  ต่อด้วย slice ถัดไป เช่น "returns CANCELLED when ..."

สังเกตว่านี่คือทีละ slice — ไม่ใช่เขียน test 10 เคสรวดเดียวก่อนเขียน implementation (นั่นคือ horizontal slicing ที่ SKILL.md เตือนไว้) พอ slice นี้เขียวแล้วค่อยไป slice ถัดไป และตลอดทางให้รัน tsc --noEmit เป็นระยะ (feedback loop ที่ 1) ก่อนจะรัน npm test เต็มตอนจบ แล้วเรียก /code-review ก่อน commit — ปิด loop ตาม implement ครบ

ฝั่ง Go หลักการเดียวกัน: เขียน _test.go ก่อน รัน go test ./... ให้เห็น FAIL ที่มีเหตุผลชัด (เช่น ค่า status ผิด ไม่ใช่ compile error) แล้วค่อยเขียนฟังก์ชันจริง รันซ้ำจนเขียว แล้ว go vet/go build เป็น feedback loop ฝั่ง static types แทน tsc

⚠️

ไม่มี feedback loop = agent บินตาบอด — ทั้ง implement, tdd, และ diagnosing-bugs ผูกกันด้วยหลักการเดียว: ทุกก้าวต้องมีอะไรสักอย่างบอกได้ทันทีว่า "ถูกหรือผิด" ถ้า agent เขียนโค้ดยาวๆ โดยไม่รัน typecheck/test คั่นเป็นระยะ หรือพยายามแก้บั๊กโดยยังไม่มี loop ที่ reproduce ได้จริง — นั่นคือสัญญาณให้หยุดแล้วถามว่า "รันคำสั่งอะไรพิสูจน์ได้บ้างว่าที่ทำอยู่ถูก"

🎯 แบบฝึกหัด
  1. หยิบ ticket เล็กๆ จากบท MP-2 (หรือของโปรเจกต์คุณเอง) แล้วก่อนเขียน test บรรทัดแรก ให้เขียนลงมาก่อนว่า "seam ที่จะ test คืออะไร" (เช่น service method ไหน, endpoint ไหน) แล้วลองถามตัวเอง/เพื่อนร่วมทีมว่าเห็นด้วยไหม
  2. ทำ vertical slice จริง 1 รอบ: เขียน test ให้ fail ก่อน (แคป output FAIL มาด้วย) แล้วเขียนโค้ดขั้นต่ำให้ผ่าน (แคป output PASS มาด้วย) — ห้ามเขียน test เคสที่สองก่อนที่เคสแรกจะเขียว
  3. นึกถึงบั๊กล่าสุดที่คุณเจอใน NestJS/Go/MongoDB/PostgreSQL แล้วลองเขียนตาม Phase 1 ของ diagnosing-bugs ว่า "คำสั่งเดียว" อะไรที่ทำให้เห็นบั๊กนั้น red ได้ทันที ถ้านึกไม่ออก ลองไล่ตามลิสต์ 10 วิธีในไฟล์ (failing test → curl → CLI → headless browser → ...) ว่าอันไหนใช้ได้กับเคสของคุณ