Skip to content

การเขียนสเปคที่ดี ​

คุณแทบไม่เคยเขียนสเปคจากหน้ากระดาษเปล่าเลย คุณอธิบายการเปลี่ยนแปลงด้วยภาษาธรรมดา /opsx:propose จะร่างข้อกำหนดและสถานการณ์ (requirements and scenarios) จากนั้นคุณจึงปรับปรุงให้ดีขึ้น หน้านี้กล่าวถึงขั้นตอนสุดท้ายนั้น — รูปร่างหน้าตาของสิ่งที่เรียกว่า "ดี" และวิธีการชี้นำ AI ไปสู่ผลลัพธ์ดังกล่าว

หน้านี้เป็นเอกสารคู่ไปกับ Reviewing a Change: การทบทวนคือการจับจุดอ่อนในร่าง ในขณะที่การเขียนคือการรู้ว่ามีองค์ประกอบใดบ้างที่ทำให้สเปคมั่นคงแข็งแรง

สเปคคือพฤติกรรม ไม่ใช่โค้ด ​

สเปคบอกว่าคุณระบบ ทำอะไร ในลักษณะที่ใครก็ตามสามารถตรวจสอบได้ — ไม่ใช่บอกว่าสร้างขึ้นมาอย่างไร สเปคประกอบด้วย ข้อกำหนด (requirements) (ข้อความระบุพฤติกรรม) และ สถานการณ์ (scenarios) (ตัวอย่างที่เป็นรูปธรรมที่ยืนยันข้อเท็จจริงเหล่านั้น)

markdown
### Requirement: Session Timeout
The system SHALL expire a session after 30 minutes of inactivity.

#### Scenario: Idle timeout
- GIVEN an authenticated session
- WHEN 30 minutes pass with no activity
- THEN the session is invalidated and the user must re-authenticate

เก็บส่วน วิธีการ — เช่น คิว (queue),ไลบรารี (library), หรือโครงสร้างตาราง (table schema) — ไว้ใน design.md หรือในโค้ด เมื่อพฤติกรรมและการนำไปปฏิบัติถูกผสมอยู่ในข้อกำหนดเดียว ข้อกำหนดนั้นจะไม่สามารถทดสอบได้ และจะเริ่มล้าสมัยทันทีที่มีการเปลี่ยนแปลงโค้ด

อะไรทำให้ข้อกำหนดดี? ​

ข้อกำหนดที่ดีคือข้อกำหนดที่ระบุพฤติกรรมเพียงหนึ่งอย่าง โดยอธิบายอย่างชัดเจนจนคุณสามารถส่งต่อให้คนอื่นไปทดสอบได้

  • หนึ่งข้อความ, หนึ่ง SHALL/MUST. หากข้อกำหนดมีประโยคย่อยที่เชื่อมด้วย "และยัง..." (and also) สามส่วน แสดงว่าเป็นสามข้อกำหนดที่แตกต่างกัน ให้แยกออกมา

  • สังเกตได้ (Observable). คนภายนอกที่ไม่ใช่ผู้เขียนโค้ดควรจะสามารถบอกได้ว่าข้อกำหนดนั้นเป็นจริงหรือไม่ "ระบบ SHALL แสดงแบนเนอร์ข้อผิดพลาดเมื่อขนาดไฟล์อัปโหลดเกิน 10 MB" เป็นสิ่งที่สังเกตได้ แต่ "ระบบ SHALL จัดการกับการอัปโหลดไฟล์ขนาดใหญ่ได้อย่างราบรื่น" ไม่สามารถสังเกตได้โดยตรง

  • ระดับความเข้มงวดที่เหมาะสม OpenSpec ใช้คำศัพท์ตาม RFC 2119 ซึ่งมีความหมายแตกต่างกัน:

    KeywordMeaning
    MUST / SHALLข้อกำหนดที่แข็งขัน (Hard requirement) ไม่มีช่องว่างสำหรับการต่อรอง
    SHOULDคำแนะนำที่แข็งแกร่ง มีช่องว่างสำหรับข้อยกเว้นที่มีเหตุผลรองรับ
    MAYเป็นทางเลือกจริงๆ (Optional)

    ควรใช้ MUST/SHALL เป็นค่าเริ่มต้น ใช้ SHOULD ก็ต่อเมื่อคุณหมายถึง "ยกเว้นหากมีเหตุผลที่ดีที่ไม่ควรทำ" เท่านั้น

เกณฑ์ทดสอบข้อกำหนด: ผู้ทดสอบที่ไม่เคยเห็นโค้ดมาก่อน สามารถบอกได้หรือไม่ว่าข้อกำหนดนั้นผ่านหรือล้มเหลว? หากตอบไม่ได้ แสดงว่าข้อกำหนดนั้นยังไม่คมชัดพอ

อะไรทำให้สถานการณ์ดี? ​

สถานการณ์คือส่วนที่ข้อกำหนดพิสูจน์คุณค่าของตนเอง แต่ละสถานการณ์คือรูปแบบ GIVEN / WHEN / THEN ที่เป็นรูปธรรมซึ่งสามารถแปลงเป็นการทดสอบอัตโนมัติได้

  • ครอบคลุมข้อกำหนดนั้นๆ สถานการณ์ที่เพียงแค่พูดซ้ำข้อกำหนดด้วยคำอื่น ไม่ได้ทดสอบอะไรเลย ให้สร้างสถานการณ์เฉพาะเจาะจงพร้อมผลลัพธ์ที่ชัดเจน
  • ครอบคลุมกรณีที่สำคัญ ไม่ใช่แค่กรณีปกติ (Happy path) การเข้าสู่ระบบที่ถูกต้องเป็นเรื่องง่าย แต่กรณีข้อมูลนำเข้าว่างเปล่า โทเค็นหมดอายุ การคลิกครั้งที่สอง หรือเหตุการณ์ผิดปกติต่างๆ คือที่ที่บั๊กมักซ่อนอยู่ และนั่นคือจุดที่สถานการณ์มีค่ามากที่สุด
  • ตั้งชื่อกรณีในหัวข้อ "Scenario: Rejects an expired token" บอกผู้ทบทวนทันทีว่าครอบคลุมเรื่องอะไร ในขณะที่ "Scenario: Test 2" ไม่ได้บอกอะไรเลย

นิสัยที่ดีประการหนึ่ง: ก่อนอนุมัติ ให้ถามตัวเองว่า กรณีไหนที่ฉันจะรู้สึกเสียใจหากมันพัง? — และตรวจสอบให้แน่ใจว่ามีสถานการณ์ที่ระบุกรณีนั้นไว้

เลือกประเภท Delta ที่เหมาะสม ​

การเปลี่ยนแปลงอธิบายการแก้ไขสเปคด้วยประเภทส่วน (section types) สามแบบ การใช้ประเภทที่ถูกต้องช่วยให้สเปคที่เก็บถาวรมีความน่าเชื่อถือ:

  • ## ADDED Requirements — พฤติกรรมใหม่ทั้งหมดที่ไม่มีมาก่อน
  • ## MODIFIED Requirements — พฤติกรรมที่มีอยู่แล้วและกำลังมีการเปลี่ยนแปลง ให้ใส่เวอร์ชันใหม่ทั้งหมด; หมายเหตุสั้นๆ เกี่ยวกับสิ่งที่เปลี่ยนแปลงจะช่วยผู้ทบทวน
  • ## REMOVED Requirements — พฤติกรรมที่กำลังถูกลบออก พร้อมบรรทัดอธิบายเหตุผล

เมื่อทำการ Archive (เก็บถาวร) ส่วน ADDED จะถูกเพิ่มเข้าไปในสเปคหลัก ส่วน MODIFIED จะแทนที่เวอร์ชันเก่า และส่วน REMOVED จะถูกลบออกจากสเปค หากคุณลบข้อกำหนดสุดท้ายของความสามารถหนึ่งๆ ถือว่าคุณได้ยุบเลิก (retire) ความสามารถนั้นแล้ว: แทนที่จะทิ้งสเปคไว้โดยไม่มีเนื้อหาใดๆ ระบบ archive จะลบไฟล์ openspec/specs/<capability>/spec.md เนื่องจากนี่เป็นขั้นตอนเดียวของ archive ที่มีการลบไฟล์ จึงต้องมีการร้องขออย่างชัดเจน — โดยเพิ่ม retire_capabilities: true ลงใน .openspec.yaml ของการเปลี่ยนแปลงนั้น alongside กับ schema: ที่ไฟล์ดังกล่าวจำเป็นต้องมีอยู่แล้ว หากไม่มีการตั้งค่านี้ ระบบ archive จะยกเลิกการทำงานและแจ้งให้คุณทราบ การยุบเลิกจะลบไฟล์ทั้งหมด ดังนั้นระบบจึงปฏิเสธการดำเนินการนี้หากสเปคยังมีเนื้อหาอื่นๆ นอกเหนือจากหัวข้อ ## Purpose และบล็อกข้อกำหนดของคุณ — เช่น ส่วน ## Notes หรือความคิดเห็นใต้ข้อกำหนด ระบบจะแจ้งบรรทัดเหล่านั้นออกมา คุณสามารถย้ายเนื้อหาเหล่านั้นไปยัง ## Purpose หรือข้อกำหนด หรือลบสเปคนั้นด้วยตนเอง สำหรับสเปคในโฟลเดอร์ของผู้เรียกใช้งาน (caller's checkout) ผลลัพธ์จากการ archive ยังระบุคำสั่ง git checkout ที่ใช้ในการกู้คืนไฟล์ที่คอมมิตแล้วอีกด้วย สำหรับที่เก็บข้อมูลแบบเลือกสรร (selected stores) จะได้รับคำแนะนำในการกู้คืนที่ scoped ตามการเช็คเอาท์ หากคุณระบุการเปลี่ยนแปลงจริงว่าเป็น ADDED คุณจะจบลงด้วยข้อกำหนดที่แข่งขันกันสองชุด; หากคุณอธิบายพฤติกรรมใหม่ว่าเป็น MODIFIED จะไม่มีอะไรให้แทนที่ หากไม่แน่ใจ ให้เปิดสเปคปัจจุบันเพื่อดูว่าข้อกำหนดนั้นมีอยู่แล้วหรือไม่

อีกส่วนหนึ่งที่ควรรู้จักคือ เมื่อ delta ของคุณสร้างความสามารถ (capability) ใหม่ที่ยังไม่มีอยู่ ให้เริ่มด้วย ## Purpose — ประโยคหรือสองประโยคที่อธิบายว่าความสามารถนี้มีไว้เพื่ออะไร ระบบ archive จะใช้ส่วนนี้เป็น Purpose ของสเปคหลักที่สร้างขึ้น; หากข้ามส่วนนี้ คุณจะ得到一个 TBD placeholder ที่ต้องกรอกด้วยตนเอง สเปคที่มีอยู่แล้วมี Purpose อยู่แล้ว ดังนั้น Purpose จาก delta จะถูกมองข้ามในที่นั้น — ให้แก้ไข openspec/specs/<capability-path>/spec.md โดยตรงเพื่อเปลี่ยนส่วนนี้ ที่นี่ <capability-path> คือไดเรกทอรีที่สัมพันธ์กับ specs/ เช่น user-auth ในโปรเจกต์แบบ flat หรือ identity/user-auth ในโปรเจกต์ที่จัดระเบียบตามโดเมน

ปรับขนาดการเปลี่ยนแปลงให้เหมาะสม ​

ข้อผิดพลาดด้านการเขียนที่พบบ่อยที่สุดไม่ใช่ข้อกำหนดที่เขียนไว้อาจเขลา — แต่เป็นการเปลี่ยนแปลงที่พยายามจะเป็นสามการเปลี่ยนแปลงในครั้งเดียว

การเปลี่ยนแปลงที่ดีควรมีวัตถุประสงค์เดียวที่คุณสามารถสรุปเป็นประโยคได้ เช่น "เพิ่มปุ่มสลับโหมดมืด", "จำกัดอัตราการเข้าถึง (rate-limit) endpoint เข้าสู่ระบบ", "ย้ายเซสชันออกจากคุกกี้" หากการอธิบายการเปลี่ยนแปลงต้องใช้คำว่า "และยัง..." เยอะ นั่นเป็นสัญญาณว่าคุณควรแยกมันออก

สัญญาณที่แสดงว่าการเปลี่ยนแปลงใหญ่เกินไป:

  • ขอบเขตของข้อเสนออ่านเหมือนรายการฟีเจอร์ที่ไม่เกี่ยวข้องกัน
  • การทบทวนจะใช้เวลานานถึงครึ่งวัน จนไม่มีใครอยากทำ
  • สองคนไม่สามารถทำงานบนการเปลี่ยนแปลงนี้ได้โดยไม่ชนกัน
  • ครึ่งหนึ่งของงานสามารถปล่อยออกมาเป็นของตัวเองได้

การเปลี่ยนแปลงขนาดเล็กจะทบทวนได้ง่ายกว่า สร้างเสร็จ在一次โฟกัสเซสชันได้ง่ายกว่า และเข้าใจได้ง่ายกว่าหกเดือนต่อมาเมื่อ archive เป็นสิ่งเดียวที่เหลืออยู่ คุณสามารถรันการเปลี่ยนแปลงหลายรายการขนานกันได้เสมอ — ดู Editing & iterating และ Workflows

ในทางกลับกันก็เกิดขึ้นได้เช่นกัน: การแก้ไขตัวพิมพ์ผิดหนึ่งบรรทัดไม่จำเป็นต้องมีสามข้อกำหนดและเอกสารออกแบบ ให้ปรับระดับพิธีกรรม (ceremony) ให้สอดคล้องกับความเสี่ยง

วิธีการชี้นำ AI ไปสู่ร่างที่ดี ​

เนื่องจาก /opsx:propose ทำหน้าที่ร่างครั้งแรก คุณภาพของสิ่งที่ได้รับกลับมาจึงขึ้นอยู่กับคุณภาพของสิ่งที่มอบให้ คุณไม่จำเป็นต้องเขียนข้อกำหนดด้วยมือ — คุณต้องกำหนดทิศทาง AI ให้ดี:

  • ระบุ Intent และขอบเขต "เพิ่มปุ่มสลับโหมดมืดที่ตามการตั้งค่า OS เมื่อโหลดครั้งแรก — อย่าแตะต้อง API ธีมที่มีอยู่" ส่วนที่อยู่นอกขอบเขตสำคัญเท่ากับส่วนที่อยู่ในขอบเขต
  • ระบุกรณีที่คุณสนใจ "ตรวจสอบให้แน่ใจว่ามีสถานการณ์สำหรับผู้ใช้ที่เลือกธีมด้วยตนเองอยู่แล้ว" AI จะครอบคลุมเฉพาะสิ่งที่คุณชี้ให้เห็น
  • จากนั้นแก้ไข มันคือ Markdown ปกติ ตัดทอน SHALL ที่คลุมเครือ ลบสถานการณ์ที่ไม่ได้ทดสอบอะไร เพิ่มกรณีที่ AI ขาดหาย — หรือขอให้ AI ทำ: "ข้อกำหนดเวลาหมดอายุคลุมเครือ ให้ระบุเป็น 30 นาที"

ร่าง, คมขึ้น, ซ้ำอีกครั้ง กระบวนการนี้ไม่กี่รอบจะผลิตสเปคที่คุณไว้วางใจได้ ซึ่งเป็นเป้าหมายหลักทั้งหมด

รายการตรวจสอบด่วน ​

ไปต่อที่ไหน ​

  • Reviewing a Change — การตรวจสอบสองนาทีที่จับจุดที่หลุดรอดไปได้
  • Concepts — โมเดลเชิงลึกเบื้องหลังสเปค การเปลี่ยนแปลง และ Delta
  • Examples & Recipes — การเปลี่ยนแปลงจริงตั้งแต่ต้นจนจบ