From 30b858fbf55b43916c3f81532fefffc867f66b95 Mon Sep 17 00:00:00 2001 From: monthop-gmail Date: Fri, 21 Aug 2026 15:03:07 +0700 Subject: [PATCH] =?UTF-8?q?=E0=B8=84=E0=B8=B3=E0=B8=A7=E0=B9=88=E0=B8=B2?= =?UTF-8?q?=20subject:=20lock=20=E0=B8=84=E0=B8=A7=E0=B8=B2=E0=B8=A1?= =?UTF-8?q?=E0=B8=AB=E0=B8=A1=E0=B8=B2=E0=B8=A2=20=E0=B9=81=E0=B8=A5?= =?UTF-8?q?=E0=B8=B0=E0=B9=83=E0=B8=AB=E0=B9=89=20policy/v1=20=E0=B9=80?= =?UTF-8?q?=E0=B8=A5=E0=B8=B4=E0=B8=81=E0=B9=80=E0=B8=A3=E0=B8=B5=E0=B8=A2?= =?UTF-8?q?=E0=B8=81=E0=B8=9C=E0=B8=B9=E0=B9=89=E0=B8=81=E0=B8=A3=E0=B8=B0?= =?UTF-8?q?=E0=B8=97=E0=B8=B3=E0=B8=A7=E0=B9=88=E0=B8=B2=20subject?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0017 เคาะ option B ไล่ทั้ง contracts/ แล้วคำนี้ถูกใช้ใน 5 contract 3 ความหมาย — policy (ผู้กระทำ) capability (ผู้ประกาศ) event (หัวเรื่องของบันทึก) approval (สิ่งที่ถูกอนุมัติ) consent (เจ้าของข้อมูล ซึ่งเขียนกำกับไว้เองว่าไม่ใช่ actor) และคำนี้ไม่อยู่ใน ตารางศัพท์ที่ lock ไว้ ทั้งที่ตารางนั้นมีไว้กันเรื่องแบบนี้โดยเฉพาะ คำวินิจฉัย: subject = สิ่งที่บันทึกนั้นเกี่ยวกับ ผู้กระทำคือ actor ความหมายเจ้าของข้อมูลส่วนบุคคลใช้ได้เฉพาะ consent/v1 เพราะเป็นศัพท์กฎหมาย กฎนี้ทำให้ 4 ใน 5 ถูกอยู่แล้ว เหลือ policy/v1 ตัวเดียวที่ต้องแก้ policy/v1 v1.2.0 เพิ่ม actor ติด deprecated ให้ subject ถอด subject ออกจาก required แล้วใช้ oneOf บังคับให้มีอย่างใดอย่างหนึ่ง ห้ามมีทั้งคู่ — payload เดิม ยัง valid ทุกใบ ไม่มีใครต้อง migrate ทันที event/v1 กับ approval/v1 ไม่แตะ เพราะใช้ในความหมายที่ถูกอยู่แล้วและเป็น semantics ของ devfactory-core ที่เปลี่ยนที่นี่ไม่ได้ ทั้งสามได้หมายเหตุ อ้างอิงไขว้แทน ซึ่งเป็นส่วนที่ปิดความเสี่ยงจริง เพราะความเสี่ยงอยู่ที่คนอ่าน ไม่ใช่บน wire — schema จับการสลับได้อยู่แล้วเพราะรูปต่างกัน Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LHv7HRmnnGAoKT5BvxDWHs --- contracts/README.md | 8 ++ contracts/approval/v1/approval.schema.yaml | 5 +- .../capability/v1/declaration.schema.yaml | 4 +- contracts/consent/v1/consent.schema.yaml | 5 + contracts/event/v1/event.schema.yaml | 3 + contracts/policy/v1/CHANGELOG.md | 22 +++ .../policy/v1/policy-decision.schema.yaml | 42 ++++-- decisions/0017-the-word-subject.md | 136 ++++++++++++++++++ decisions/README.md | 4 + 9 files changed, 218 insertions(+), 11 deletions(-) create mode 100644 decisions/0017-the-word-subject.md diff --git a/contracts/README.md b/contracts/README.md index 55abab8..4da7584 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -37,6 +37,14 @@ canonical schema ที่ทุก repo ใน ecosystem ต้องใช้ คำขอที่ไม่ครบ 4 ข้อไม่ได้ถูกปฏิเสธถาวร — แต่ต้องรอให้ครบก่อน ไม่ใช่ผ่านด้วยความน่าเชื่อของผู้ขอ +### ⏳ field ที่ deprecated รอลบใน major ถัดไป + +| contract | field | แทนด้วย | ลบเมื่อ | +| --- | --- | --- | --- | +| `policy/v1` | `Request.subject` | `Request.actor` | `policy/v2` ([ADR-0017](../decisions/0017-the-word-subject.md)) | + +field ที่ติด `deprecated: true` **ยังใช้ได้และยัง valid** — มีอยู่เพื่อไม่ให้ consumer ที่ pin อยู่ต้องขึ้น major เพราะชื่อ · แต่ **major ถัดไปที่เกิดด้วยเหตุอื่นต้องลบมันทิ้งพร้อมกัน** ไม่ใช่ปล่อยไว้เป็นชื่อที่สองถาวร — สองชื่อสำหรับสิ่งเดียวกันคือสิ่งที่ repo นี้ห้ามไว้ทุกที่ ([`expires_at: null`](consent/v1/) · `conditions: []`) + ### 🔗 Derived contracts `approval/v1` และ `event/v1` **derive semantics มาจาก `devfactory-core`** ตาม [ADR-0006 C2](../decisions/0006-contract-versioning.md) — เราเป็นเจ้าของ *รูปร่างบน wire* เขาเป็นเจ้าของ *ความหมาย* diff --git a/contracts/approval/v1/approval.schema.yaml b/contracts/approval/v1/approval.schema.yaml index 7c6e4c9..ad78f61 100644 --- a/contracts/approval/v1/approval.schema.yaml +++ b/contracts/approval/v1/approval.schema.yaml @@ -53,7 +53,10 @@ properties: subject: type: object - description: อนุมัติให้อะไร + description: >- + อนุมัติให้อะไร — `subject` ที่นี่คือ **สิ่งที่ถูกอนุมัติ** ไม่ใช่ผู้กระทำ + และไม่ใช่เจ้าของข้อมูล ([ADR-0017](../../../decisions/0017-the-word-subject.md)) + 🔒 เป็น semantics ของ devfactory-core — เปลี่ยนชื่อที่นี่ไม่ได้ required: [type, id] properties: type: diff --git a/contracts/capability/v1/declaration.schema.yaml b/contracts/capability/v1/declaration.schema.yaml index 985190d..951b2f0 100644 --- a/contracts/capability/v1/declaration.schema.yaml +++ b/contracts/capability/v1/declaration.schema.yaml @@ -11,7 +11,9 @@ required: [subject, capabilities] properties: subject: type: object - description: ใครเป็นคนประกาศ + description: >- + ใครเป็นคนประกาศ — `subject` ที่นี่คือ **สิ่งที่ declaration นี้เกี่ยวกับ** + ซึ่งตรงกับคำวินิจฉัยใน [ADR-0017](../../../decisions/0017-the-word-subject.md) · ผู้กระทำใน `policy/v1` ใช้ `actor` required: [kind, id] properties: kind: diff --git a/contracts/consent/v1/consent.schema.yaml b/contracts/consent/v1/consent.schema.yaml index dd5ebcf..b3ddb9e 100644 --- a/contracts/consent/v1/consent.schema.yaml +++ b/contracts/consent/v1/consent.schema.yaml @@ -135,6 +135,11 @@ properties: $ref: https://schemas.agent-platform.internal/identity/v1/identity.schema.yaml#/$defs/Id description: >- **เจ้าของข้อมูล** — 🔒 ไม่ใช่ actor และไม่ใช่ resource + + ⚠️ คำว่า `subject` ในไฟล์นี้คือ **data subject** ตามศัพท์กฎหมาย (PDPA/GDPR) + ซึ่งเป็นข้อยกเว้นที่ [ADR-0017](../../../decisions/0017-the-word-subject.md) อนุญาตไว้ · `policy/v1` ใช้ `actor` เรียกผู้กระทำ + **อย่าจับคู่ `consent/v1.subject_id` กับ `policy/v1.actor` ว่าเป็นคนเดียวกัน** + — ในเคสที่สำคัญที่สุด (หมออ่านข้อมูลผู้ป่วย) มันคนละคนเสมอ ชื่อ field เป็น `subject_id` ไม่ใช่ชื่อของโดเมน (`patient_id`, `customer_id`) โดยเจตนา เพราะ platform ไม่รู้จักโดเมนและไม่ควรรู้ diff --git a/contracts/event/v1/event.schema.yaml b/contracts/event/v1/event.schema.yaml index a386df1..75140dd 100644 --- a/contracts/event/v1/event.schema.yaml +++ b/contracts/event/v1/event.schema.yaml @@ -88,6 +88,9 @@ properties: $ref: https://schemas.agent-platform.internal/identity/v1/identity.schema.yaml#/$defs/WorkspaceId description: optional เฉพาะ event ระดับ tenant ตาม ADR-0007 + # ⚠️ `subject` ที่นี่ = **หัวเรื่องของบันทึก** ไม่ใช่ผู้กระทำและไม่ใช่เจ้าของข้อมูล + # ผู้กระทำอยู่ที่ `actor` · ดูตารางศัพท์ที่ lock ไว้ ([ADR-0017](../../../decisions/0017-the-word-subject.md)) + # 🔒 ชื่อคู่นี้เป็น semantics ของ devfactory-core (RFC-0008) — เปลี่ยนที่นี่ไม่ได้ subject_type: $ref: '#/$defs/SubjectType' subject_id: diff --git a/contracts/policy/v1/CHANGELOG.md b/contracts/policy/v1/CHANGELOG.md index 6ebdae8..8b687ec 100644 --- a/contracts/policy/v1/CHANGELOG.md +++ b/contracts/policy/v1/CHANGELOG.md @@ -1,5 +1,27 @@ # policy/v1 +## v1.2.0 — 2026-08-21 + +`Request` เรียกผู้กระทำว่า `subject` ขณะที่ `consent/v1` เขียนกำกับ field ชื่อเดียวกันไว้เองว่า 🔒 *"ไม่ใช่ actor"* — [ADR-0017](../../../decisions/0017-the-word-subject.md) ไล่ทั้ง `contracts/` แล้วพบว่าคำนี้ถูกใช้ใน **5 contract 3 ความหมาย** และวินิจฉัยว่า **`subject` = สิ่งที่บันทึกนั้นเกี่ยวกับ · ผู้กระทำคือ `actor`** ซึ่งทำให้ 4 ใน 5 ถูกอยู่แล้ว และเหลือไฟล์นี้ไฟล์เดียวที่ต้องแก้ + +* `$defs.Actor` — นิยามรูปครั้งเดียว +* `Request.actor` — ชื่อใหม่ · `Request.subject` ติด `deprecated: true` รูปเหมือนเดิมทุกอย่าง +* `Request.required` ถอด `subject` ออก เหลือ `[context, action]` แล้วใช้ **`oneOf` บังคับให้มีอย่างใดอย่างหนึ่ง ห้ามมีทั้งคู่** + +### ไม่ breaking + +payload เดิมที่ส่ง `subject` **ยัง valid ทุกใบ** · การถอดออกจาก `required` เป็นการผ่อน ไม่ใช่บังคับ · `oneOf` ห้ามส่งสองชื่อพร้อมกัน แต่ `actor` เพิ่งเกิด **จึงไม่มี payload เดิมใบไหนส่งทั้งคู่ได้** ไม่มีใบไหนกลายเป็น invalid + +`care-agent-platform` และ `devfactory-core` ที่ pin อยู่ **ไม่ต้องทำอะไรทันที** — ย้ายไป `actor` เมื่อสะดวก + +### ⏳ ต้องลบใน `policy/v2` + +`subject` มีอยู่เพื่อไม่ให้ใครต้องขึ้น major เพราะชื่อเท่านั้น · **v2 ที่เกิดด้วยเหตุอื่นต้องลบมันทิ้งพร้อมกัน** ไม่ใช่ปล่อยไว้เป็นชื่อที่สองถาวร + +### ที่ไม่แตะ + +`event/v1` และ `approval/v1` ใช้ `subject` ในความหมายที่ถูกตามคำวินิจฉัยอยู่แล้ว และเป็น 🔒 semantics ของ `devfactory-core` ที่เปลี่ยนที่นี่ไม่ได้ · `consent/v1.subject_id` เป็น **data subject** ตามศัพท์กฎหมาย ซึ่ง ADR-0017 อนุญาตไว้เป็นข้อยกเว้น · ทั้งสามได้หมายเหตุอ้างอิงไขว้แทนการเปลี่ยนชื่อ + ## v1.1.0 — 2026-08-21 เพิ่ม **ผลการประเมินความยินยอม** ตาม [ADR-0016](../../../decisions/0016-recording-which-consent-allowed-access.md) (option C) diff --git a/contracts/policy/v1/policy-decision.schema.yaml b/contracts/policy/v1/policy-decision.schema.yaml index ec86b72..a69eeb3 100644 --- a/contracts/policy/v1/policy-decision.schema.yaml +++ b/contracts/policy/v1/policy-decision.schema.yaml @@ -24,21 +24,45 @@ $defs: description: เหตุผลเชิงปริมาณที่ทำให้ถูกปฏิเสธหรือถูกจำกัด enum: [none, rate_limited, budget_exceeded, quota_exhausted, out_of_window] + Actor: + description: >- + **ผู้กระทำ** — *"ใครจะทำ"* ([ADR-0017](../../../decisions/0017-the-word-subject.md)) + + 🔒 คนละอย่างกับ `subject` ในความหมายที่ contract อื่นใช้ · ดูตารางศัพท์ที่ lock ไว้ + ใน [`decisions/README.md`](../../../decisions/README.md) — โดยเฉพาะ `consent/v1.subject_id` + ที่หมายถึง **เจ้าของข้อมูล** ซึ่งเป็นคนละคนกับผู้กระทำเสมอในกรณีที่น่ากังวลที่สุด + type: object + required: [principal] + properties: + principal: + $ref: https://schemas.agent-platform.internal/identity/v1/identity.schema.yaml#/$defs/Principal + agent_id: + $ref: https://schemas.agent-platform.internal/identity/v1/identity.schema.yaml#/$defs/AgentId + Request: type: object - required: [context, subject, action] + required: [context, action] + # ต้องมี actor หรือ subject อย่างใดอย่างหนึ่ง — ห้ามมีทั้งคู่ + # ([ADR-0017](../../../decisions/0017-the-word-subject.md)) · payload เดิมที่ส่ง `subject` + # ยัง valid ทุกใบ และไม่มีใบเดิมใบไหนส่งทั้งคู่ได้เพราะ `actor` เพิ่งเกิด + oneOf: + - required: [actor] + - required: [subject] properties: context: $ref: https://schemas.agent-platform.internal/identity/v1/identity.schema.yaml#/$defs/RequestContext + actor: + $ref: '#/$defs/Actor' subject: - type: object - description: ใครจะทำ - required: [principal] - properties: - principal: - $ref: https://schemas.agent-platform.internal/identity/v1/identity.schema.yaml#/$defs/Principal - agent_id: - $ref: https://schemas.agent-platform.internal/identity/v1/identity.schema.yaml#/$defs/AgentId + $ref: '#/$defs/Actor' + deprecated: true + description: >- + ⚠️ **เลิกใช้ — ใช้ `actor` แทน** · รูปเหมือนกันทุกอย่าง เปลี่ยนแค่ชื่อ + + ชื่อนี้ผิดตามคำวินิจฉัยของ [ADR-0017](../../../decisions/0017-the-word-subject.md): + `subject` แปลว่า *สิ่งที่บันทึกนั้นเกี่ยวกับ* ไม่ใช่ *ผู้กระทำ* — และในไฟล์นี้มันคือผู้กระทำ + + ยังอยู่เพื่อไม่ให้ consumer ที่ pin อยู่ต้องขึ้น major เพราะชื่อ · **จะถูกลบใน `policy/v2`** action: type: object description: จะทำอะไร diff --git a/decisions/0017-the-word-subject.md b/decisions/0017-the-word-subject.md new file mode 100644 index 0000000..290ceab --- /dev/null +++ b/decisions/0017-the-word-subject.md @@ -0,0 +1,136 @@ +# ADR-0017: คำว่า `subject` — หนึ่งคำ สามความหมาย ข้าม 5 contract + +**Status:** Accepted (2026-08-21) +**Date:** 2026-08-21 +**Depends on:** [ADR-0006](0006-contract-versioning.md) · [ADR-0012](0012-consent-contract.md) · [ADR-0016](0016-recording-which-consent-allowed-access.md) +**Blocking:** ตารางศัพท์ที่ lock ใน [`README.md`](README.md) · `contracts/policy/v1` + +## Context + +เจอตอนเขียน [ADR-0016](0016-recording-which-consent-allowed-access.md) ว่า `policy/v1` เรียกผู้กระทำว่า `subject` ขณะที่ `consent/v1` เขียนกำกับ field ชื่อเดียวกันไว้เองว่า 🔒 *"ไม่ใช่ actor"* + +ADR-0016 บันทึกไว้ว่าเป็นปัญหาข้าม **3 contract** — **นับขาด** · ไล่ทั้ง `contracts/` แล้วมี **5 ตัว** และแยกได้เป็น **3 ความหมาย**: + +| contract | field | ความหมาย | รูป | +| --- | --- | --- | --- | +| `policy/v1` | `Request.subject` | **ผู้กระทำ** — *"ใครจะทำ"* | object `{principal, agent_id}` | +| `capability/v1` | `declaration.subject` | **ผู้ประกาศ** — *"ใครเป็นคนประกาศ"* | object `{kind, id}` | +| `event/v1` 🔗 | `subject_type` / `subject_id` | **หัวเรื่องของบันทึก** | enum 9 ค่า + `Id` | +| `approval/v1` 🔗 | `subject` | **สิ่งที่ถูกอนุมัติ** — *"อนุมัติให้อะไร"* | object `{type, id}` | +| `consent/v1` | `subject_id` | **เจ้าของข้อมูล** — 🔒 *"ไม่ใช่ actor และไม่ใช่ resource"* | `Id` เดี่ยว | + +สองกลุ่มแรกคือ *ผู้กระทำ/ผู้ถือ* · สองกลุ่มกลางคือ *สิ่งที่บันทึกเกี่ยวกับ* · ตัวสุดท้ายคือ *เจ้าของข้อมูลส่วนบุคคล* ซึ่งเป็นคนละเรื่องกับทั้งสองกลุ่ม + +และคำนี้ **ไม่อยู่ในตารางศัพท์ที่ lock ไว้** ทั้งที่ [`decisions/README.md`](README.md) มีตารางนั้นอยู่เพื่อกันเรื่องแบบนี้โดยเฉพาะ + +## ประเมินความเสี่ยงตามจริง — อย่าตีขลุม + +คู่ที่อันตรายจริงมีคู่เดียว: **`policy/v1.Request.subject` (ผู้กระทำ) ↔ `consent/v1.subject_id` (เจ้าของข้อมูล)** เพราะทั้งคู่คือ "id ของคน" ที่ความหมายตรงข้ามกัน และเป็นสองด่านที่ต้องเรียกคู่กันตาม `consent_rules` ข้อ 6 + +ส่วน `event/v1` กับ `approval/v1` มี `type`/`subject_type` ติดมาด้วยเสมอ (job · execution · artifact …) จึงไม่ถูกสับสนกับ principal ได้ง่าย · `capability/v1` ก็มี `kind` กำกับ + +**แต่ต้องพูดให้ตรง:** บน wire วันนี้ **schema จับความผิดพลาดนี้ได้อยู่แล้ว** — `policy` เป็น object `{principal, …}` ส่วน `consent` เป็น scalar `Id` · ส่งสลับกันจะ validate ไม่ผ่าน + +```text +ความเสี่ยงจริงจึงไม่ได้อยู่บน wire แต่อยู่ใน: + · หัวคนที่อ่าน contract สองตัวเรียงกัน + · โมเดลภายในของ consumer ที่เขียนเอง ซึ่งไม่มี schema มาจับให้ +``` + +`enterprise-knowledge` กำลัง map `Principal` / `TenantScope` / `PolicyContext` ของตัวเองเข้ากับ contract ชุดนี้ ([#17 ของเขา](https://github.com/monthop-gmail/enterprise-knowledge/issues/17)) และ `contracts.py` ของเขาเป็น Python ล้วน — **ไม่มี JSON Schema มาจับตรงนั้น** · ในโดเมนที่เขาทำ (ACL-aware retrieval) การสลับสองคำนี้แปลว่า *เอาสิทธิ์ของคนหนึ่งไปเปิดข้อมูลของอีกคน* + +นี่คือเหตุผลที่เรื่องนี้ควรทำตอนนี้ ไม่ใช่ตอนมี consumer รายที่สี่ + +## ข้อจำกัดที่กำหนดทางเลือก + +* [ADR-0006](0006-contract-versioning.md) ระบุ **"เปลี่ยนชื่อ field"** เป็น breaking ตรง ๆ → rename = major ใหม่ +* `event/v1` และ `approval/v1` เป็น **derived contract** — `subject_type`/`subject_id` มาจาก [RFC-0008](https://github.com/monthop-gmail/devfactory-core/blob/main/rfcs/0008-external-event-intake.md) ซึ่งเป็น 🔒 semantics ของ `devfactory-core` · **เราเปลี่ยนเองไม่ได้** และไม่ควรอยากเปลี่ยน +* `consent/v1` `subject_id` ใช้อยู่ใน production ของ `care-agent-platform` และคำว่า **data subject เป็นศัพท์กฎหมาย** (PDPA/GDPR) ที่ถูกต้องอยู่แล้วในบริบทนั้น +* `policy/v1` มีคน pin สองราย (`care-agent-platform` · `devfactory-core`) และกำลังจะมีรายที่สาม + +## คำวินิจฉัยที่เสนอให้ lock + +> **`subject` = สิ่งที่บันทึกหรือข้อความนั้นเกี่ยวกับ** +> ห้ามใช้เรียก **ผู้กระทำ** — ผู้กระทำคือ `actor` +> ความหมาย *"เจ้าของข้อมูลส่วนบุคคล"* ใช้ได้เฉพาะใน `consent/v1` เพราะเป็นศัพท์กฎหมาย (*data subject*) และต้องมีหมายเหตุกำกับเสมอ + +กฎนี้ทำให้ 4 ใน 5 contract ถูกอยู่แล้ว และชี้ตัวที่ต้องแก้ได้ตัวเดียวคือ `policy/v1` + +## Options + +### A. เอกสารอย่างเดียว — lock คำ + ใส่หมายเหตุอ้างอิงไขว้ในทั้ง 5 contract + +* ✅ ไม่แตะ wire เลย · ไม่มี consumer ต้อง migrate +* ✅ ปิดความเสี่ยงที่เป็นความเสี่ยงจริง (การอ่านผิด) ตรงจุด +* ❌ `policy/v1` ยังเรียกผู้กระทำว่า `subject` ต่อไป — กฎที่เพิ่ง lock ถูกละเมิดโดย contract ของตัวเองตั้งแต่วันแรก +* ❌ consumer รายที่สี่ยังอ่านชื่อผิดได้เหมือนเดิม เพราะชื่อยังผิดอยู่ + +### B. A + `policy/v1` เพิ่ม `actor` · เลิกใช้ `subject` แบบมีช่วงเปลี่ยนผ่าน ⭐ + +```yaml +Request: + required: [context, action] # ถอด subject ออกจาก required (ผ่อน ไม่ใช่บังคับ) + oneOf: + - required: [actor] # ทางใหม่ + - required: [subject] # ทางเดิม — deprecated + properties: + actor: { ... } # รูปเดียวกับ subject เดิมทุกอย่าง + subject: { deprecated: true } +``` + +* ✅ **ไม่ breaking** — payload เดิมที่ส่ง `subject` ยัง valid ทุกใบ · ถอดออกจาก `required` เป็นการผ่อน +* ✅ `oneOf` บังคับให้มี **อย่างใดอย่างหนึ่ง ไม่ใช่ทั้งคู่** — ไม่มีช่วงที่สองชื่อพูดคนละเรื่องพร้อมกัน ตรงกับกฎ *"สิ่งเดียวกันต้องเขียนได้แบบเดียว"* ที่ใช้กับ `expires_at: null` และ `conditions: []` +* ✅ ลบ `subject` ทิ้งเมื่อ `policy/v2` เกิดขึ้นด้วยเหตุอื่น — ไม่ต้องบังคับให้ใครขึ้น major เพราะชื่อ +* ❌ มีสองชื่ออยู่ร่วมกันชั่วคราว ซึ่ง repo นี้ไม่ชอบ — แลกกับการไม่บังคับ consumer สองรายให้ migrate ทันที +* ❌ ต้องมีคนจำว่าให้ลบตอน v2 · แก้ด้วยการเขียนไว้ใน `CHANGELOG` และ `platform_rules` + +### C. A + rename `consent/v1.subject_id` → `data_subject_id` (`consent/v2`) + +* ✅ ตรงศัพท์กฎหมายที่สุด และทำให้ `subject` เหลือความหมายเดียว +* ❌ **breaking กับ contract ที่ใช้ใน production อยู่** และ `care-agent-platform` เพิ่ง migrate `conditions` ไปหมาด ๆ +* ❌ แก้ตัวที่ *ถูกอยู่แล้ว* แทนที่จะแก้ตัวที่ผิด — `consent/v1` เขียนกำกับไว้ชัดตั้งแต่แรกว่าไม่ใช่ actor + +### D. rename ให้เป็นคำเฉพาะทุกที่ (`actor` · `data_subject_id` · `record_subject`) + +* ✅ ไม่เหลือความกำกวมเลย +* ❌ major bump 4–5 contract พร้อมกัน · และ **แตะ `event/v1` กับ `approval/v1` ซึ่งเป็น semantics ของ `devfactory-core` ที่เราเปลี่ยนเองไม่ได้** +* ❌ ราคาสูงกว่าความเสี่ยงจริงมาก ในเมื่อ schema จับกรณีสลับบน wire ได้อยู่แล้ว + +### E. ไม่ทำอะไร + +* ✅ ศูนย์บาท +* ❌ ตารางศัพท์ที่ lock มีไว้กันเรื่องนี้โดยเฉพาะ แล้วปล่อยเคสที่ชัดที่สุดไว้นอกตาราง +* ❌ consumer รายที่สามกำลังอ่านอยู่ตอนนี้ + +## Decision + +**B** — lock คำ + `policy/v1` เพิ่ม `actor` และ deprecate `subject` ด้วย `oneOf` ที่บังคับให้เลือกอย่างใดอย่างหนึ่ง + +**Reason:** ความเสี่ยงจริงอยู่ที่คนอ่านและที่โมเดลภายในของ consumer ไม่ใช่บน wire (schema จับการสลับได้อยู่แล้วเพราะรูปต่างกัน) — จึงไม่คุ้มที่จะบังคับ major bump ให้ใคร (ปฏิเสธ C และ D) · แต่การ lock กฎแล้วปล่อยให้ contract ของตัวเองละเมิดตั้งแต่วันแรกก็ไม่ใช่การ lock (ปฏิเสธ A เดี่ยว ๆ) · `oneOf` + ถอดออกจาก `required` ปิดช่องได้โดยไม่ทำให้ payload เดิมใบไหน invalid และไม่เปิดช่วงที่สองชื่อพูดคนละเรื่องพร้อมกัน · `event/v1` และ `approval/v1` ไม่ต้องแตะเพราะกฎที่ lock ทำให้มันถูกอยู่แล้ว และมันเป็น semantics ของ repo อื่นที่เราเปลี่ยนเองไม่ได้อยู่ดี + +**Authority:** Monthop Champaruang — Platform Owner / Architecture Authority of `agent-platform` + +### ไม่ bump major — `policy/v1` `v1.1.0` → `v1.2.0` + +| เกณฑ์ breaking ของ [ADR-0006](0006-contract-versioning.md) | การเปลี่ยนนี้ | +| --- | --- | +| ลบ field · เปลี่ยนชื่อ field · เปลี่ยน type | ❌ `subject` ยังอยู่ ยังรูปเดิม แค่ติด `deprecated` | +| เพิ่ม required field ใหม่ · optional → required | ❌ ตรงข้าม — **ถอด `subject` ออกจาก `required`** คือการผ่อน | +| ลบค่าออกจาก enum · เปลี่ยนความหมายของค่าเดิม | ❌ ไม่มี enum ถูกแตะ | +| เปลี่ยน default | ❌ ไม่มี default | +| เข้มขึ้นใน validation | ⚠️ `oneOf` ห้ามส่งทั้งสองชื่อพร้อมกัน — แต่ `actor` เพิ่งเกิด **ไม่มี payload เดิมใบไหนส่งทั้งคู่ได้** จึงไม่มีใบไหนกลายเป็น invalid | + +## Consequences + +* ตารางศัพท์ที่ lock ใน [`decisions/README.md`](README.md) เพิ่มแถว `subject` — ที่มา ADR นี้ +* `policy/v1` `v1.2.0` · `contracts/README.md` บันทึกว่ามี field ที่ deprecated รอลบใน `v2` +* ใส่หมายเหตุอ้างอิงไขว้ใน `consent/v1` · `event/v1` · `approval/v1` · `capability/v1` ว่า `subject` ที่นั่นหมายถึงอะไร และชี้มาที่กฎเดียวกัน — **นี่คือส่วนที่ปิดความเสี่ยงจริง** ไม่ใช่การ rename +* `care-agent-platform` และ `devfactory-core` **ไม่ต้องทำอะไรทันที** — payload เดิมยัง valid · ย้ายไป `actor` เมื่อสะดวก +* ตอบ [enterprise-knowledge#17](https://github.com/monthop-gmail/enterprise-knowledge/issues/17) ได้ด้วยชื่อที่ไม่กำกวมตั้งแต่วันแรกที่เขา map +* **`event/v1` และ `approval/v1` ไม่ถูกแตะ** — ถ้าวันหนึ่งอยากให้ตรงกันหมดจริง ๆ ต้องเปิด RFC ที่ `devfactory-core` ไม่ใช่ ADR ที่นี่ +* **drift check ตรวจข้อนี้ไม่ได้** — เป็นเรื่องความหมายของชื่อ ไม่ใช่โครงสร้าง · สิ่งเดียวที่ตรวจได้คือ `oneOf` ทำงานจริงไหม ซึ่งพิสูจน์ด้วย negative test +* ค้างต่อจาก ADR-0016: `event/v1.policy_result` ที่เป็นสำเนามือของ `Decision` — ยังไม่แตะ + +## Sources + +[ADR-0016](0016-recording-which-consent-allowed-access.md) ข้อค้นพบ 4 (ซึ่งนับ contract ขาดไป 2 ตัว) · [ADR-0012](0012-consent-contract.md) · [RFC-0008](https://github.com/monthop-gmail/devfactory-core/blob/main/rfcs/0008-external-event-intake.md) `subject_type`/`subject_id` · [enterprise-knowledge#17](https://github.com/monthop-gmail/enterprise-knowledge/issues/17) · ตารางศัพท์ที่ lock ใน [`decisions/README.md`](README.md) diff --git a/decisions/README.md b/decisions/README.md index 0e4d86d..e84b751 100644 --- a/decisions/README.md +++ b/decisions/README.md @@ -34,6 +34,7 @@ ADR ในโฟลเดอร์นี้เป็น **authority** ของ | [0014](0014-consent-access-time-conditions.md) | `consent/v1` เงื่อนไขตอนเข้าถึง | **B** — `conditions` = `kind` + `params` · ตรวจทุกครั้งที่ใช้ · ไม่รู้จัก = ไม่อนุญาต | ✅ Accepted | | [0015](0015-event-sequence-and-trail-closure.md) | `event/v1` ลำดับ + trail ที่ถูกตัดท้าย | **C** — `sequence` เรียงอย่างเดียว · ช่องว่างไม่มีความหมาย · ความครบถ้วนปิดด้วยใบปิดท้าย (RFC ที่ต้นทาง) | ✅ Accepted | | [0016](0016-recording-which-consent-allowed-access.md) | บันทึกว่าอนุญาตด้วยความยินยอมใบไหน | **C** — `consent/v1` `$defs.Evaluation` นิยามครั้งเดียว · `policy/v1` + `event/v1` `$ref` · แช่แข็งผล ไม่ใช่เก็บ id | ✅ Accepted | +| [0017](0017-the-word-subject.md) | คำว่า `subject` | **B** — `subject` = สิ่งที่บันทึกเกี่ยวกับ · ผู้กระทำคือ `actor` · `policy/v1` เพิ่ม `actor` deprecate `subject` | ✅ Accepted | การเคาะบันทึกไว้ที่ [issue #1–#10](https://github.com/monthop-gmail/agent-platform/issues?q=is%3Aissue+label%3Aadr) — **ไฟล์บันทึกว่าตัดสินอะไร issue บันทึกว่าใครตัดสินและเมื่อไหร่** @@ -56,6 +57,7 @@ Architecture Owner ของ [`devfactory-core`](https://github.com/monthop-gmai | `harness` ในความหมาย test rig | `evals` | 0005 | | `Project` / `Department` เป็นชั้น id | label ของ workspace | 0007 | | `risk_level` เดี่ยว ๆ | `action_risk` / `authority` / `severity` | 0010 | +| `subject` เรียก**ผู้กระทำ** | `actor` — `subject` สงวนไว้แปลว่า *สิ่งที่บันทึกนั้นเกี่ยวกับ* · ความหมาย *เจ้าของข้อมูลส่วนบุคคล* ใช้ได้เฉพาะ `consent/v1` (ศัพท์กฎหมาย *data subject*) | 0017 | ## ลำดับที่เคาะไปแล้ว @@ -75,6 +77,8 @@ contracts/ P0 ✅ ── profiles/ ✅ ── planes/ ✅ 0011 (conformance automation) ✅ ── 0012 (consent) ✅ ── 0013 (approval supersedes) ✅ ↓ 0014 (consent conditions) ✅ ── 0015 (event sequence) ✅ ── 0016 (consent evaluation) ✅ + ↓ +0017 (คำว่า subject) ✅ ``` ## ที่มา