# D · สัญญาข้อมูลและการทดสอบตัวคัด

**พื้นฐานก่อนทำ:** อ่าน [M06](/knowledge/drone-applications/disaster-assessment/), [M10](/knowledge/drone-applications/communications-support/) และ [C02](/knowledge/drone-applications/cases/search-data-kit/) เรื่อง source/revision, event time, rights, offline queue. ใช้ Python 3 กับ CSV มาตรฐานได้ ไม่ต้องมี cloud, simulator, radio หรือ hardware. `expected.csv` เป็นเฉลยกฎ ไม่ใช่ actual.

**ผลลัพธ์ KSEC:** `APP-CSF-D-K01` นิยาม interface/ลำดับกฎ/ระดับหลักฐาน; `APP-CSF-D-S01` ตรวจทุกแถวและกรณีผิด; `APP-CSF-D-E01` จำกัด export ตามสิทธิ์และไม่แต่ง actual; `APP-CSF-D-C01` เก็บ baseline, issue, rollback และ retest. ใช้ [rubric](rubric.md).

## สัญญา `CSF-RULE-v1`

Input คือ UTF-8 CSV ที่มี 14 คอลัมน์ตาม `readme.md`, immutable row order และ logical key `scenario_id/record_id/revision`; `source_row` เริ่ม 2 ตามบรรทัดไฟล์. เวลา parse เป็น ISO 8601 ที่ **ต้องมี offset**; `unknown` เป็น missing ไม่ใช่ 0 หรือเวลาปัจจุบัน. เปรียบเทียบ instant โดยรักษา offset ต้นทางใน output. ประเมิน duplicate ทุก occurrence ก่อนเลือกรุ่น; R01v1 supersedes v0 เมื่อบันทึก relation/เหตุในโจทย์ตรงกัน. หาก revision ใหม่ไม่มีความสัมพันธ์ที่พิสูจน์ได้หรือสถานะขัดกัน ส่ง `hold_conflict`. Output ต่อ input row คือ `source_row,scenario_id,record_id,input_revision,rule_revision,expected_status,reason,next_check`; error ต้องเป็นสถานะ hold พร้อมเหตุและห้ามทำแถวหาย. Manifest แยก audience และ event `not_sent/sent/received/acknowledged` พร้อม package revision/เวลา/actor; retry เดิมต้องไม่สร้างการรับทราบปลอม.

**D-v0:** เขียน requirement/interface schema, precedence และ data-flow input → validation → disposition → audience filter → package → receipt. ทำ baseline ที่ตั้งใจผิดหนึ่งข้อ เช่นเลือก revision สูงโดยไม่ตรวจ duplicate. บันทึก issue, trigger rollback, ผู้แก้/ผู้ตรวจ และผลกระทบที่ต้องแจ้งผู้รับหากเคยส่งผิด. ห้ามใช้ baseline ที่ผิดส่งจริง.

**D-v1:** แก้กฎและสร้าง test matrix นี้. ทำในสำเนา fixture; แถวที่แก้เพื่อทดสอบต้องระบุ mutation แล้วคืนต้นฉบับ. `expected.csv` ต้นฉบับต้องคง 13 แถว.

| Test | Input / mutation | Expected |
|---|---|---|
| normal | `scenario.csv` 13 บรรทัดข้อมูล | 3 `candidate_check`, 10 history/hold; ทุก `source_row` มี output |
| duplicate | R11v0 สอง occurrence | ทั้งสอง `hold_duplicate`; ไม่เลือกบรรทัดแรกเงียบ ๆ |
| revision | R01v0/v1 | v0 `superseded`, v1 `candidate_check`; เก็บสองรุ่น |
| conflict | R05/R06 ที่ดูเหมือนขัดกันแต่ footprint ไม่พอเทียบ | ทั้งคู่ `hold_conflict`; ขอ footprint/การทวนก่อนตัดสิน |
| missing | R04 `observed_at=unknown` | `hold_missing_time`; ไม่เติม receive time |
| time late | R10 observed/received หลัง cutoff | `hold_late`; ไม่แทรกใน package ปัจจุบัน |
| time boundary | สำเนา R02 ตั้ง observed `09:35:00+07:00` และ received ≤ cutoff | อายุ 45 นาทีรวมขอบ; disposition ยัง `candidate_check` |
| time outside | สำเนา R02 ตั้ง observed `09:34:59+07:00` | `hold_stale`; ข้ามขอบหนึ่งวินาที |
| time invalid | สำเนา R02 ลบ offset หรือให้ observed > received | `hold_missing_time` หรือ `hold_invalid_time`; ไม่เดา offset |
| rights/recipient | R08/R09; ลองส่ง raw R07 ไป PUBLIC | R08 `hold_rights`, R09 `hold_recipient`, export R07/public ถูกปฏิเสธ |
| offline/retry | package `INCIDENT-v1` ส่งล้มเหลวแล้ว retry 2 ครั้งด้วย ID เดิม | log failed/retry; ไม่มี `received/acknowledged` จนมี event จากผู้รับ, ไม่มี package ซ้ำที่สับสนรุ่น |
| rollback | กลับ baseline หลัง defect และรันตารางซ้ำ | บันทึกผลจริงของแต่ละ test; ถ้า baseline ยังผิดให้ HOLD และแจ้งผู้รับที่ได้รับผลผิด |

สำหรับรันในเครื่อง ให้บันทึก `python --version`, คำสั่ง, UTC run time, SHA-256 ของ input และ rule script, exit status, output path, diff เทียบ expected และจำนวนผ่าน/ไม่ผ่าน **แยกใน local_author_run log**. ตัวอย่างตรวจรูปและความครบแบบอ่านอย่างเดียว (PowerShell ที่มี Python 3):

```powershell
@'
import csv, collections, pathlib
p = pathlib.Path("scenario.csv")
e = pathlib.Path("expected.csv")
rows = list(csv.DictReader(p.open(encoding="utf-8-sig", newline="")))
expected = list(csv.DictReader(e.open(encoding="utf-8-sig", newline="")))
assert len(rows) == len(expected) == 13
assert [int(x["source_row"]) for x in expected] == list(range(2, 15))
for line, (src, out) in enumerate(zip(rows, expected), 2):
    assert (src["scenario_id"], src["record_id"], src["revision"]) == (out["scenario_id"], out["record_id"], out["input_revision"]), line
counts = collections.Counter(x["expected_status"] for x in expected)
assert counts["candidate_check"] == 3 and counts["hold_duplicate"] == 2
print("fixture shape PASS; this does not execute the learner rule")
'@ | python -
```

ผู้เรียนต้องเขียนตัวคัดของตนเองและเทียบผล **ทุก row** รวม mutation; snippet ข้างต้นตรวจเพียงรูปไฟล์ ไม่พิสูจน์กฎ. ถ้าไม่ได้รันให้บันทึก `NOT_RUN`, ไม่แต่ง actual/exit status และ **ยังไม่ผ่าน APP-CSF-D-S01**. ส่ง D-v0/v1, source/rule hashes, matrix expected/actual, issue/change/rollback log, audience-filter test, และคำทักจากผู้ตรวจอย่างน้อยสองข้อพร้อมคำตอบและผลทวน. ผลในเครื่องไม่เป็นผลวิทยุ การส่งถึงคน การบิน หรือผลผู้เรียนคนอื่น.
