{
  "schema_version": "1.1",
  "journey_version": "2.3",
  "system": "DNA OS",
  "source": "registry/developer-portal-spec.ts",
  "journey_source": "registry/integration-portal-spec.ts",
  "human_portal": "/",
  "related": {
    "tool_spec": "https://dnaos.dev/api/v1/spec",
    "modules": "https://dnaos.dev/api/v1/modules",
    "capabilities": "https://dnaos.dev/api/v1/capabilities",
    "constitution": "https://dnaos.dev/api/v1/constitution"
  },
  "categories": [
    {
      "id": "system",
      "icon": "🏛️",
      "title": "สถาปัตยกรรมและเลเยอร์หลัก",
      "titleEn": "System Architecture & Core Layers",
      "summary": "โครงสร้างเลเยอร์ ทิศทางการพึ่งพา ลำดับการทำงานของคำขอ และ Registry ที่เป็นแหล่งความจริงเดียวของระบบ"
    },
    {
      "id": "engines",
      "icon": "⚙️",
      "title": "เครื่องยนต์ธุรกิจและการคำนวณ",
      "titleEn": "Business Engines & Pricing",
      "summary": "สัญญาของเครื่องยนต์ราคา ภาษี ต้นทุน และเส้นทาง dispatch ที่แปลง command เป็นผลลัพธ์ธุรกิจจริง"
    },
    {
      "id": "api",
      "icon": "🔌",
      "title": "สเปกการเชื่อมต่อและโครงสร้างข้อมูล",
      "titleEn": "API Endpoints & Request Schemas",
      "summary": "Endpoint จริงทั้งหมด พร้อม header, body schema, ตัวอย่าง JSON, รหัสข้อผิดพลาด และขั้นตอนทดสอบ"
    },
    {
      "id": "integrations",
      "icon": "💬",
      "title": "การเชื่อมต่อ LINE, Bot และ Mini App",
      "titleEn": "Integrations & Mini Apps",
      "summary": "Webhook ของ LINE, MCP Server สำหรับ AI agents, และวงจรชีวิตของ Connector ที่ผูกกับ Provider Registry"
    },
    {
      "id": "settings",
      "icon": "🔐",
      "title": "การจัดการคีย์ ระบบสิทธิ์ และเกณฑ์ความปลอดภัย",
      "titleEn": "Settings & Security",
      "summary": "ตัวแปรสภาพแวดล้อม ระดับสิทธิ์ของบัญชีและ token ขอบเขตข้อมูล และเกณฑ์ deploy-gate ที่บังคับใช้ก่อนทุก release"
    }
  ],
  "endpoints": [
    {
      "method": "POST",
      "path": "/api/v1/query",
      "auth": "X-DNA-Token header หรือ ?token= (token ออกที่ /admin/api-tokens)",
      "summary": "Execute read command ผ่าน dispatcher พร้อมตรวจ role ระดับ admin tier และจำกัดขอบเขตข้อมูลตามบัญชีผู้ถือ token",
      "doc": "query-endpoint",
      "section": "endpoint"
    },
    {
      "method": "GET",
      "path": "/api/v1/spec",
      "auth": "ไม่ต้องใช้ token (public)",
      "summary": "คืนรายการ tool ทั้งหมดที่ external AI เรียกได้ พร้อม JSON Schema ของพารามิเตอร์และชื่อคำสั่งดั้งเดิมในฟิลด์ _dna_command",
      "doc": "discovery-endpoints",
      "section": "spec"
    },
    {
      "method": "GET",
      "path": "/api/v1/capabilities",
      "auth": "ไม่ต้องใช้ token (public)",
      "summary": "คืน existence.json และข้อมูล capability registry พร้อม composition plan เมื่อระบุ capability ที่ต้องการ",
      "doc": "discovery-endpoints",
      "section": "spec"
    },
    {
      "method": "GET",
      "path": "/api/v1/modules",
      "auth": "ไม่ต้องใช้ token (public)",
      "summary": "คืนรายการโมดูลทั้งหมดพร้อม layer, owner, exports และสถานะ",
      "doc": "discovery-endpoints",
      "section": "spec"
    },
    {
      "method": "GET",
      "path": "/api/admin/api-tokens",
      "auth": "Session ระดับ ADMIN_TIER",
      "summary": "คืนรายการ token ของบัญชีที่ล็อกอินอยู่",
      "doc": "api-tokens",
      "section": "endpoints"
    },
    {
      "method": "POST",
      "path": "/api/admin/api-tokens",
      "auth": "Session ระดับ ADMIN_TIER",
      "summary": "ออก token ใหม่โดยเลือก scope จาก allow-list ของระบบ",
      "doc": "api-tokens",
      "section": "endpoints"
    },
    {
      "method": "POST",
      "path": "$SUPABASE_URL/functions/v1/line-webhook-v2",
      "runtime": "supabase-edge-function",
      "auth": "ลายเซ็น x-line-signature (HMAC-SHA256 ของ channel secret) — ฟังก์ชันตั้ง verify_jwt = false",
      "summary": "รับ LINE webhook event ทุกชนิด (message, follow, unfollow, postback, location, image, file, audio, group) และส่งต่อไปยัง handler ที่รับผิดชอบ",
      "doc": "line-webhook",
      "section": "endpoint"
    }
  ],
  "docs": [
    {
      "slug": "architecture-overview",
      "category": "system",
      "title": "ภาพรวมสถาปัตยกรรมระบบ",
      "titleEn": "Architecture Overview",
      "summary": "DNA OS แบ่งเป็น 8 เลเยอร์ที่พึ่งพาได้ทางเดียว (UI → API → Engine → Core → Services) โดยมี Registry เป็นแหล่งความจริงกลางที่ทุกเลเยอร์อ่าน",
      "sections": [
        {
          "id": "layers",
          "heading": "เลเยอร์หลักและความรับผิดชอบ",
          "lead": "ทุกโมดูลต้องประกาศเลเยอร์ของตนใน registry/module-registry.json และถูกตรวจซ้ำโดย architecture book ในทุกการคอมไพล์",
          "table": {
            "columns": [
              "เลเยอร์",
              "พาธ",
              "ความรับผิดชอบ",
              "Registry ที่อ้างอิง"
            ],
            "rows": [
              [
                "UI / Presentation",
                "app/, components/, presenter/",
                "หน้าเว็บ, shell, การแสดงผล และการเรียก API ผ่าน route handler",
                "portals.json, architecture.json"
              ],
              [
                "API / Gateway",
                "app/api/, gateway/",
                "รับคำขอ, ตรวจสิทธิ์, แปลง payload, เรียก engine ผ่าน dispatcher",
                "command-registry.json, permissions.json"
              ],
              [
                "Business Engines",
                "engines/",
                "คำนวณราคา ภาษี ต้นทุน ระยะทาง เอกสาร และตรวจ business rule",
                "command-registry.json, business-coverage.json"
              ],
              [
                "Cognitive",
                "cognitive/",
                "Intent → Plan → Execute → Reflect สำหรับงานที่ต้องใช้เหตุผล",
                "intent-dictionary.json, decisions.json"
              ],
              [
                "Core",
                "core/",
                "Config loader, execution engine, workspace composer, authority context",
                "CONSTITUTION.json, constraints.json"
              ],
              [
                "Services",
                "services/",
                "ขอบเขต I/O: Supabase, Realtime, AI bridge, token store",
                "services.json, infrastructure.json"
              ],
              [
                "Plugins & Drivers",
                "plugins/",
                "ไดรเวอร์ LINE, Chrome, GitHub, macOS และ platform runtime",
                "drivers.json, PROVIDER_DIRECTORY.json"
              ],
              [
                "Registry",
                "registry/",
                "แหล่งความจริงเดียวของ command, module, capability, schema และสิทธิ์",
                "architecture-book.json, dependency-graph.json"
              ]
            ]
          }
        },
        {
          "id": "direction",
          "heading": "ทิศทางการพึ่งพา (Dependency Direction)",
          "lead": "กราฟการพึ่งพาถูกบังคับด้วย registry/dependency-graph.json และตรวจใน pre-commit ทุกครั้ง",
          "bullets": [
            "UI เรียกได้เฉพาะ route handler ของตัวเอง ไม่ import engine โดยตรง",
            "Route handler เรียก engine ผ่าน engines/validation/dispatcher.ts เท่านั้น เพื่อให้ registry gate และ Rule 72 ทำงานทุกเส้นทาง",
            "Engine อ่านค่าตั้งต้นจาก core/config-loader (ตาราง app_configs) ไม่มีค่าคงที่ซ่อนอยู่ในโค้ด",
            "Presenter สรุปผลที่คำนวณเสร็จแล้วเท่านั้น และไม่ import engines",
            "Services เป็นชั้นเดียวที่แตะ Supabase โดยตรง",
            "Registry เป็นอ่านอย่างเดียว (read-only) สำหรับ runtime ทุกชั้น"
          ],
          "code": {
            "language": "text",
            "content": "app/ (UI)\n  └─ app/api/ (route handler)\n       └─ engines/validation/dispatcher.ts  ── registry/command-registry.json\n            └─ engines/*                    ── core/config-loader ── app_configs\n                 └─ services/supabase/*      ── PostgreSQL + RLS\n                      └─ presenter/* (สรุปผล)\nregistry/*  ← อ่านโดยทุกชั้น ห้ามเขียนจาก runtime"
          }
        },
        {
          "id": "contracts",
          "heading": "สัญญาข้อมูลกลาง (Core Contracts)",
          "lead": "โครงสร้างข้อมูลที่ทุกเลเยอร์ใช้ร่วมกัน ประกาศไว้ที่ registry/schemas.json",
          "table": {
            "columns": [
              "Contract",
              "ฟิลด์บังคับ",
              "ฟิลด์เสริม",
              "ใช้ที่ไหน"
            ],
            "rows": [
              [
                "Command",
                "version, command",
                "args",
                "POST /api/v1/query, dispatcher"
              ],
              [
                "Event",
                "id, type, source, timestamp",
                "data, traceId",
                "event bus, business_events"
              ],
              [
                "DriverResponse",
                "success, command, timestamp",
                "data, error",
                "plugins/drivers/*"
              ],
              [
                "AgentCall",
                "driver, command",
                "args",
                "cognitive/executor"
              ],
              [
                "DocumentData",
                "type, items",
                "company, customer, events, payment, debt",
                "engines/documents/*"
              ]
            ]
          },
          "code": {
            "language": "json",
            "content": "{\n  \"version\": 1,\n  \"command\": \"debt.list\",\n  \"args\": {}\n}"
          }
        }
      ]
    },
    {
      "slug": "runtime-flow",
      "category": "system",
      "title": "ลำดับการทำงานของคำขอ",
      "titleEn": "Request Lifecycle",
      "summary": "ทุกคำขอเดินผ่าน 7 ขั้นตอนเดียวกัน ตั้งแต่ auth guard จนถึง presenter ทำให้ตรวจสอบย้อนกลับได้ด้วย request_id เดียว",
      "sections": [
        {
          "id": "steps",
          "heading": "เจ็ดขั้นตอนมาตรฐาน",
          "bullets": [
            "1. Auth guard — requireAuth / requireAuthPage ตรวจ session และระดับสิทธิ์ (ADMIN_TIER)",
            "2. Parse & validate — route handler ตรวจ JSON, header และ query parameter",
            "3. Registry gate — dispatcher ตรวจว่าคำสั่งมีจริงใน registry/command-registry.json",
            "4. Policy gate — Rule 72 บังคับให้ write command ต้องยืนยันจากมนุษย์ผ่าน /admin UI",
            "5. Data scope — resolveTokenDataScope จำกัดขอบเขตข้อมูลตามบัญชีผู้ถือ token",
            "6. Execute — engine คำนวณโดยอ่านค่าตั้งต้นจาก app_configs ผ่าน core/config-loader",
            "7. Respond — presenter จัดรูปแบบผลลัพธ์พร้อม request_id และ capability context"
          ]
        },
        {
          "id": "trace",
          "heading": "ตัวอย่างการเดินทางของคำขอ",
          "code": {
            "language": "text",
            "content": "POST /api/v1/query\n  header  X-DNA-Token: <token>\n  body    { \"command\": \"debt.list\", \"args\": {} }\n    ↓\nservices/supabase/api-tokens.ts   validateApiToken()        → token record\ncore/auth/session.ts              profiles.role ∈ admin_tier → ผ่าน\nengines/authority/session-scope   resolveTokenDataScope()   → company scope\nengines/validation/dispatcher.ts  registry gate + Rule 72   → allow (read)\ncognitive/debts                   query invoice_debts       → rows\napp/api/v1/query/route.ts         JSON + request_id         → 200"
          }
        },
        {
          "id": "statuses",
          "heading": "รหัสสถานะที่ระบบใช้",
          "table": {
            "columns": [
              "Status",
              "ความหมาย",
              "สิ่งที่ client ควรทำ"
            ],
            "rows": [
              [
                "200",
                "คำสั่งสำเร็จและคืนข้อมูล",
                "อ่านฟิลด์ data"
              ],
              [
                "400",
                "body ไม่ใช่ JSON หรือไม่ได้ระบุ command",
                "แก้ payload ตาม schema"
              ],
              [
                "401",
                "ไม่มี token หรือ token ไม่ถูกต้อง/ถูก revoke",
                "สร้าง token ใหม่ที่ /admin/api-tokens"
              ],
              [
                "403",
                "token ไม่มีสิทธิ์ หรือคำสั่งต้องยืนยันจากมนุษย์",
                "ใช้ /admin UI สำหรับ write command"
              ],
              [
                "503",
                "เซิร์ฟเวอร์ยังไม่พร้อมใช้งาน token store",
                "ตรวจการตั้งค่า SUPABASE_SERVICE_ROLE_KEY"
              ]
            ]
          }
        },
        {
          "id": "invariants",
          "heading": "หลักประกันที่ไม่เปลี่ยน",
          "bullets": [
            "ทุกคำสั่งภายนอกต้องผ่าน registry gate — คำสั่งที่ไม่มีใน registry จะไม่ถูก execute",
            "write command ไม่ถูกเปิดให้ external AI เรียกตรง (Rule 72)",
            "สิทธิ์ถูกประเมินใหม่ทุกคำขอจาก profiles.role ไม่เชื่อสถานะฝั่ง client",
            "ทุก response ประกอบด้วย request_id เพื่อใช้ trace ข้ามชั้น",
            "การอ่านค่าตั้งต้นที่หายไปจะหยุดด้วย [CONFIG MISSING] ไม่คืนค่าเดา"
          ]
        }
      ]
    },
    {
      "slug": "registry-map",
      "category": "system",
      "title": "แผนที่ Registry กลาง",
      "titleEn": "Central Registry Map",
      "summary": "Registry แต่ละไฟล์มีเจ้าของชัดเจนและมี endpoint หรือ runtime เดียวที่อ่านมัน AI agent ใช้ตารางนี้เพื่อเลือกแหล่งข้อมูลก่อนเขียนโค้ดเชื่อมต่อ",
      "sections": [
        {
          "id": "map",
          "heading": "ไฟล์ registry และผู้ใช้งานจริง",
          "table": {
            "columns": [
              "Registry",
              "ความจริงที่เก็บ",
              "อ่านโดย",
              "เข้าถึงผ่าน"
            ],
            "rows": [
              [
                "command-registry.json",
                "คำสั่งทั้งหมด: owner, layer, input, output, requires_confirmation",
                "dispatcher, /api/v1/spec",
                "GET /api/v1/spec, GET /api/v1/mcp/registry/commands"
              ],
              [
                "module-registry.json",
                "ทุกโมดูล: layer, purpose, exports, access, status",
                "architecture book, IDE, /api/v1/modules",
                "GET /api/v1/modules"
              ],
              [
                "capability-registry.json",
                "CapabilityId และ OwnerModule ที่ผูกกับ command",
                "composer, /api/v1/capabilities",
                "GET /api/v1/capabilities"
              ],
              [
                "schemas.json",
                "สัญญาข้อมูล Command / Event / DriverResponse / AgentCall",
                "ทุกเลเยอร์",
                "import ตรงจาก registry"
              ],
              [
                "permissions.json",
                "role_tiers (admin_tier ฯลฯ) และสิทธิ์รายบทบาท",
                "core/auth/session.ts",
                "requireAuth(ADMIN_TIER)"
              ],
              [
                "drivers.json",
                "ไดรเวอร์ฮาร์ดแวร์และแพลตฟอร์ม",
                "plugins, /api/v1/drivers",
                "GET /api/v1/drivers"
              ],
              [
                "commands/workflows.json",
                "Workflow Registry ที่ executor อ่านจริง",
                "core/executor/engine.ts",
                "GET /api/v1/workflows"
              ],
              [
                "CONSTITUTION.json",
                "Iron Rules และหลักการออกแบบ",
                "/api/v1/constitution, governance",
                "GET /api/v1/constitution"
              ],
              [
                "mcp-planned-manifest.json",
                "สัญญา HTTP ของเครื่องมือ MCP และสถานะ integration",
                "dna-os-mcp-server",
                "GET /api/v1/mcp/manifest"
              ],
              [
                "PROVIDER_DIRECTORY.json",
                "Provider ที่รองรับและสถานะการเชื่อมต่อ",
                "connectors, admin console",
                "/admin/connectors"
              ],
              [
                "connector-lifecycle.json",
                "กฎวงจรชีวิตของ connector และ failure flow",
                "connector center",
                "/admin/connectors"
              ],
              [
                "architecture.json",
                "รายชื่อเลเยอร์และโมดูลของแต่ละเลเยอร์",
                "architecture book, portal",
                "registry/architecture-book.json"
              ]
            ]
          }
        },
        {
          "id": "consumption",
          "heading": "รูปแบบการอ่าน registry สำหรับ agent",
          "lead": "โหลดผ่าน HTTP เพื่อไม่ผูกกับโครงสร้างไฟล์ภายใน หรือ import ตรงเมื่อเขียนโค้ดใน repository นี้",
          "code": {
            "language": "ts",
            "content": "// ภายใน repository\nimport commandRegistry from \"@/registry/command-registry.json\";\n\nconst readCommands = Object.entries(commandRegistry.commands)\n  .filter(([, c]) => !c.requires_confirmation)\n  .map(([name, c]) => ({ name, owner: c.owner, output: c.output }));\n\n// ภายนอกรายการคำสั่งทั้งหมด (tool schema พร้อมใช้กับ LLM)\nconst spec = await fetch(\"/api/v1/spec\").then((r) => r.json());"
          }
        },
        {
          "id": "rules",
          "heading": "กฎการใช้งาน Registry",
          "bullets": [
            "หนึ่งความจริง หนึ่งไฟล์ — ห้ามคัดลอกข้อมูล registry ไปเก็บซ้ำในโค้ดหรือเอกสาร",
            "เพิ่ม endpoint ใหม่ต้องเพิ่มใน spec นี้ไฟล์เดียว เพื่อให้ทั้งหน้าเว็บและ JSON index ตรงกัน",
            "ค่า config เชิงธุรกิจอยู่ในตาราง app_configs ไม่ใช่ค่าคงที่ในซอร์สโค้ด",
            "ทุกโมดูลใหม่ต้องลงทะเบียนใน module-registry.json ก่อน commit (pre-commit ตรวจอัตโนมัติ)",
            "Registry integrity gate ตรวจความสอดคล้อง Source ↔ Registry ↔ Git ในทุกคอมมิต"
          ]
        }
      ]
    },
    {
      "slug": "pricing-engine",
      "category": "engines",
      "title": "เครื่องยนต์คำนวณราคา",
      "titleEn": "Pricing Engine",
      "summary": "engines/pricing.ts คำนวณต้นทุนสึกหรอ กำไร ค่าธรรมเนียมระบบ ค่าด่วน และ VAT โดยอ่านอัตราทั้งหมดจาก app_configs",
      "sections": [
        {
          "id": "formula",
          "heading": "สูตรการคำนวณ",
          "lead": "ต้นทุนสึกหรอคิดจากค่าน้ำมันจริงเท่านั้น (ไม่รวมค่าแรงคนขับ เพราะคนขับเป็นผู้รับเหมาอิสระ) จากนั้นจึงบวกกำไร ค่าด่วน และ VAT",
          "code": {
            "language": "ts",
            "content": "wearCost      = fuel_cost × (wear_percent / 100)\ntotalWithWear = total_cost + wearCost\nmarkup        = totalWithWear × (markup_percent / 100)\nsystemFee     = totalWithWear × (system_fee_percent / 100)\nurgentFee     = is_urgent ? totalWithWear × (urgent_fee_percent / 100) : 0\nprofit        = markup − systemFee\nsellingExVAT  = totalWithWear + markup + urgentFee\nvat           = sellingExVAT × vat_rate      // vat_rate เก็บเป็นเศษส่วน เช่น 0.07\nsellingPrice  = sellingExVAT + vat\npricePerTrip  = trips > 0 ? sellingPrice / trips : sellingPrice"
          }
        },
        {
          "id": "config",
          "heading": "คีย์ตั้งต้นใน app_configs",
          "lead": "ทุกอัตราต้องมีอยู่ในฐานข้อมูลจริง หากคีย์ใดหายไป core/config-loader จะหยุดด้วย [CONFIG MISSING] แทนการคืนค่าเดา",
          "table": {
            "columns": [
              "คีย์",
              "ชนิด",
              "ความหมาย",
              "ตัวอย่าง"
            ],
            "rows": [
              [
                "pricing-markup-percent",
                "number (%)",
                "กำไรที่บวกเพิ่มจากต้นทุนรวมหลังสึกหรอ",
                "15"
              ],
              [
                "system-fee-percent",
                "number (%)",
                "ค่าธรรมเนียมระบบ หักจากกำไร",
                "3"
              ],
              [
                "urgent-fee-percent",
                "number (%)",
                "ค่าบริการงานด่วน ใช้เมื่อ is_urgent = true",
                "10"
              ],
              [
                "wear-percent",
                "number (%)",
                "อัตราสึกหรอ คิดจากค่าน้ำมัน",
                "5"
              ],
              [
                "vat-rate",
                "number (fraction)",
                "อัตรา VAT เก็บเป็นเศษส่วน",
                "0.07"
              ]
            ]
          }
        },
        {
          "id": "contract",
          "heading": "สัญญาอินพุตและเอาต์พุต",
          "code": {
            "language": "json",
            "content": "// PricingInput\n{\n  \"total_cost\": 18500,\n  \"material_cost\": 12000,\n  \"fuel_cost\": 6500,\n  \"trips\": 3,\n  \"is_urgent\": true\n}\n\n// PricingResult\n{\n  \"wear_cost\": 325,\n  \"selling_price_ex_vat\": 22022.5,\n  \"vat\": 1541.58,\n  \"selling_price\": 23564.08,\n  \"profit\": 2536.13,\n  \"system_fee\": 564.75,\n  \"urgent_fee\": 1882.5,\n  \"price_per_trip\": 7854.69,\n  \"audit\": { \"engine\": \"pricing\", \"input\": {}, \"rules\": {}, \"computed\": {} }\n}"
          }
        },
        {
          "id": "verify",
          "heading": "ขั้นตอนตรวจสอบ",
          "bullets": [
            "ตรวจว่าคีย์ครบ: select key, value from app_configs where key in ('pricing-markup-percent','system-fee-percent','urgent-fee-percent','wear-percent','vat-rate');",
            "เรียก calcPricing ด้วยอินพุตตัวอย่าง แล้วเทียบผลกับสูตรด้านบนทีละบรรทัด",
            "ยืนยันว่า audit.rules.formula ในผลลัพธ์สะท้อนอัตราที่อ่านมาจริง",
            "รัน npx tsc --noEmit เพื่อยืนยันสัญญา PricingInput / PricingResult"
          ]
        }
      ]
    },
    {
      "slug": "dispatch-contract",
      "category": "engines",
      "title": "สัญญาของ Dispatcher",
      "titleEn": "Dispatcher Contract",
      "summary": "engines/validation/dispatcher.ts เป็นประตูเดียวที่แปลง command เป็นการเรียก engine โดยตรวจ registry และกฎความปลอดภัยก่อนทุกครั้ง",
      "sections": [
        {
          "id": "entry",
          "heading": "รายการ command ใน registry",
          "lead": "ทุกคำสั่งประกาศ owner, layer, input, output และ requires_confirmation — ตัวอย่างจริงของ debt.list",
          "code": {
            "language": "json",
            "content": "{\n  \"debt.list\": {\n    \"owner\": \"cognitive/debts\",\n    \"layer\": \"cognitive\",\n    \"input\": {},\n    \"output\": \"InvoiceDebt[]\",\n    \"description\": \"แสดงรายการหนี้ค้างชำระ (OPEN)\",\n    \"description_th\": \"แสดงรายการหนี้ที่ยังค้างชำระ (สถานะ OPEN)\",\n    \"requires_confirmation\": false\n  }\n}"
          },
          "table": {
            "columns": [
              "ฟิลด์",
              "ชนิด",
              "ความหมาย"
            ],
            "rows": [
              [
                "owner",
                "string",
                "โมดูลเจ้าของคำสั่ง ใช้ resolve capability context"
              ],
              [
                "layer",
                "string",
                "เลเยอร์ที่คำสั่งสังกัด"
              ],
              [
                "input",
                "object",
                "แผนที่ชื่อพารามิเตอร์ → ชนิด; เติม ? ท้ายชื่อเมื่อไม่บังคับ"
              ],
              [
                "output",
                "string",
                "ชนิดผลลัพธ์ที่ผู้เรียกได้รับ"
              ],
              [
                "requires_confirmation",
                "boolean",
                "true = write command ปิดสำหรับ external AI (Rule 72)"
              ]
            ]
          }
        },
        {
          "id": "modes",
          "heading": "โหมดการเรียกและผลลัพธ์",
          "table": {
            "columns": [
              "โหมด",
              "เส้นทาง",
              "ผลลัพธ์"
            ],
            "rows": [
              [
                "Read ผ่าน HTTP",
                "POST /api/v1/query (token)",
                "dispatch(command, args, false) → data"
              ],
              [
                "Read ผ่าน UI",
                "/admin UI (session)",
                "dispatch(command, args, false) → presenter"
              ],
              [
                "Write ผ่าน UI",
                "/admin UI + การยืนยันของมนุษย์",
                "dispatch(command, args, true) เมื่อได้รับอนุมัติ"
              ],
              [
                "Read ผ่าน MCP",
                "POST /api/v1/mcp/business/dispatch",
                "คำสั่ง read ที่อยู่ใน whitelist เท่านั้น"
              ]
            ]
          },
          "code": {
            "language": "json",
            "content": "// สำเร็จ\n{ \"ok\": true, \"command\": \"debt.list\", \"user\": \"owner@example.com\", \"data\": [], \"capability\": { \"owner_module\": \"cognitive/debts\", \"capability_ids\": [] } }\n\n// ถูกปฏิเสธเพราะต้องยืนยัน\n{ \"ok\": false, \"command\": \"debt.create\", \"error\": \"requires human confirmation\", \"requires_confirmation\": true }"
          }
        },
        {
          "id": "verify",
          "heading": "ขั้นตอนตรวจสอบ",
          "bullets": [
            "ยืนยันว่าคำสั่งที่ไม่มีใน registry ถูกปฏิเสธก่อนถึง engine",
            "ยืนยันว่า write command คืน requires_confirmation = true และ HTTP 403",
            "ตรวจว่า response มี capability.owner_module ตรงกับ owner ใน registry",
            "ทดสอบผ่าน /playground ด้วย token จริงเพื่อดูผลแบบครบวงจร"
          ]
        }
      ]
    },
    {
      "slug": "query-endpoint",
      "category": "api",
      "title": "เรียกคำสั่งธุรกิจ",
      "titleEn": "Runtime Query",
      "summary": "POST /api/v1/query คือท่อหลักสำหรับ AI ภายนอกและ connector รับ command พร้อม args แล้วคืนข้อมูลที่ผ่านการตรวจสิทธิ์และขอบเขตข้อมูลแล้ว",
      "sections": [
        {
          "id": "endpoint",
          "heading": "สเปก endpoint",
          "endpoints": [
            {
              "id": "query",
              "method": "POST",
              "path": "/api/v1/query",
              "auth": "X-DNA-Token header หรือ ?token= (token ออกที่ /admin/api-tokens)",
              "summary": "Execute read command ผ่าน dispatcher พร้อมตรวจ role ระดับ admin tier และจำกัดขอบเขตข้อมูลตามบัญชีผู้ถือ token",
              "params": [
                {
                  "name": "X-DNA-Token",
                  "location": "header",
                  "type": "string",
                  "required": false,
                  "description": "API token ของบัญชีที่อยู่ใน admin tier (ใช้แทน query param ได้)",
                  "example": "dna_xxxxxxxx"
                },
                {
                  "name": "token",
                  "location": "query",
                  "type": "string",
                  "required": false,
                  "description": "ทางเลือกสำหรับ client ที่ไม่สามารถตั้ง header ได้",
                  "example": "dna_xxxxxxxx"
                },
                {
                  "name": "command",
                  "location": "body",
                  "type": "string",
                  "required": true,
                  "description": "ชื่อคำสั่งใน registry รองรับรูปแบบ OpenAI double-underscore เช่น debt__list",
                  "example": "debt.list"
                },
                {
                  "name": "args",
                  "location": "body",
                  "type": "object",
                  "required": false,
                  "description": "พารามิเตอร์ของคำสั่ง ตาม input ใน registry",
                  "example": "{}"
                }
              ],
              "requestSample": "curl -X POST https://<host>/api/v1/query \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-DNA-Token: $DNA_OS_API_TOKEN\" \\\n  -d '{\"command\":\"debt.list\",\"args\":{}}'",
              "responseSample": "{\n  \"ok\": true,\n  \"command\": \"debt.list\",\n  \"user\": \"owner@example.com\",\n  \"data\": [],\n  \"capability\": { \"owner_module\": \"cognitive/debts\", \"capability_ids\": [] }\n}",
              "verification": [
                "เรียกโดยไม่ใส่ token → 401 และ error ระบุให้สร้าง token ที่ /admin/api-tokens",
                "เรียกด้วย token ที่ถูก revoke → 401",
                "เรียก write command เช่น debt.create → 403 พร้อม requires_confirmation = true",
                "เรียก read command → 200 พร้อมฟิลด์ data และ capability",
                "เรียกด้วย command ที่ไม่มีใน registry → 400 พร้อม violations",
                "ส่ง body ที่ไม่ใช่ JSON → 400"
              ],
              "registryRefs": [
                "registry/command-registry.json",
                "registry/permissions.json",
                "registry/capability-registry.json"
              ]
            }
          ]
        },
        {
          "id": "cors",
          "heading": "CORS และการเรียกจากเบราว์เซอร์",
          "bullets": [
            "OPTIONS /api/v1/query คืน 204 พร้อม Allow-Origin: * และอนุญาต header Content-Type, X-DNA-Token",
            "ฝั่ง client ต้องแนบ token ใน header ทุกคำขอ ไม่มี cookie-based flow สำหรับ endpoint นี้",
            "แนะนำให้เรียกจาก server-side หรือ proxy ของคุณเองเพื่อไม่ให้ token ปรากฏในเบราว์เซอร์"
          ]
        }
      ]
    },
    {
      "slug": "discovery-endpoints",
      "category": "api",
      "title": "Endpoint ค้นหาโครงสร้างระบบ",
      "titleEn": "Discovery Endpoints",
      "summary": "ชุด endpoint แบบอ่านอย่างเดียวที่เปิดให้ทุก client ใช้ค้นหา command, module, capability, driver, workflow และกฎรัฐธรรมนูญของระบบ",
      "sections": [
        {
          "id": "overview",
          "heading": "ภาพรวม",
          "paragraphs": [
            "ทุก endpoint ในหมวดนี้เป็น GET, ไม่ต้องใช้ token และมี Cache-Control 300 วินาที (constitution 3600 วินาที) เพื่อให้ agent โหลดโครงสร้างระบบซ้ำได้โดยไม่สร้างภาระกับฐานข้อมูล",
            "ใช้ /api/v1/spec เป็นจุดเริ่มต้นเมื่อต้องต่อ LLM tool-use เพราะคืน tool schema ที่กรอง deprecated และ write command ออกแล้ว"
          ],
          "table": {
            "columns": [
              "Endpoint",
              "คืนอะไร",
              "ใช้เมื่อ"
            ],
            "rows": [
              [
                "GET /api/v1/spec",
                "tool schema รูปแบบ OpenAI/Claude + _dna_command",
                "สร้าง tool ให้ LLM เรียก DNA OS"
              ],
              [
                "GET /api/v1/modules",
                "โมดูลทั้งหมดพร้อม layer, purpose, exports, access",
                "หาเจ้าของฟีเจอร์หรือโมดูลที่ต้องเรียก"
              ],
              [
                "GET /api/v1/capabilities",
                "existence.json + capability registry + composition plan",
                "วางแผนประกอบ capability หลายตัว"
              ],
              [
                "GET /api/v1/drivers",
                "ไดรเวอร์ฮาร์ดแวร์และแพลตฟอร์ม",
                "ตรวจว่ามีไดรเวอร์สำหรับอุปกรณ์ที่ต้องการ"
              ],
              [
                "GET /api/v1/workflows",
                "workflow registry ที่ executor อ่านจริง",
                "สร้าง automation ตาม workflow ที่มีอยู่"
              ],
              [
                "GET /api/v1/constitution",
                "Iron Rules และหลักการออกแบบ",
                "ตรวจข้อกำหนดก่อนออกแบบ connector"
              ],
              [
                "GET /api/v1/developer-portal",
                "สเปกพอร์ทัลนี้ทั้งหมดในรูป JSON",
                "ให้ agent อ่านสเปกแล้วเขียนโค้ดเชื่อมต่อ"
              ]
            ]
          }
        },
        {
          "id": "spec",
          "heading": "Tool specification",
          "endpoints": [
            {
              "id": "spec",
              "method": "GET",
              "path": "/api/v1/spec",
              "auth": "ไม่ต้องใช้ token (public)",
              "summary": "คืนรายการ tool ทั้งหมดที่ external AI เรียกได้ พร้อม JSON Schema ของพารามิเตอร์และชื่อคำสั่งดั้งเดิมในฟิลด์ _dna_command",
              "params": [],
              "responseSample": "{\n  \"schema_version\": \"1.0\",\n  \"system\": \"DNA OS\",\n  \"query_endpoint\": \"/api/v1/query\",\n  \"auth\": \"Header: X-DNA-Token: <your-token>\",\n  \"tools\": [\n    {\n      \"type\": \"function\",\n      \"function\": {\n        \"name\": \"debt__list\",\n        \"description\": \"แสดงรายการหนี้ค้างชำระ (OPEN) | layer: cognitive | output: InvoiceDebt[]\",\n        \"parameters\": { \"type\": \"object\", \"properties\": {}, \"required\": [] },\n        \"_dna_command\": \"debt.list\"\n      }\n    }\n  ]\n}",
              "verification": [
                "curl -s /api/v1/spec | jq '.tools | length' → ต้องมากกว่า 0",
                "ยืนยันว่าไม่มี tool ใดมี _dna_command ที่เป็น write command",
                "นำ tool แรกไปเรียก POST /api/v1/query ด้วยชื่อใน _dna_command แล้วต้องได้ 200"
              ],
              "registryRefs": [
                "registry/command-registry.json"
              ]
            },
            {
              "id": "capabilities",
              "method": "GET",
              "path": "/api/v1/capabilities",
              "auth": "ไม่ต้องใช้ token (public)",
              "summary": "คืน existence.json และข้อมูล capability registry พร้อม composition plan เมื่อระบุ capability ที่ต้องการ",
              "params": [
                {
                  "name": "resolve",
                  "location": "query",
                  "type": "string (comma-separated)",
                  "required": false,
                  "description": "รายการ CapabilityId ที่ต้องการให้ composer ประกอบเป็นแผน",
                  "example": "cap.x,cap.y"
                },
                {
                  "name": "application_id",
                  "location": "query",
                  "type": "string",
                  "required": false,
                  "description": "ขอบเขตแอปพลิเคชันของ composition plan",
                  "example": "api.v1.capabilities"
                },
                {
                  "name": "workspace_id",
                  "location": "query",
                  "type": "string",
                  "required": false,
                  "description": "ขอบเขต workspace ของ composition plan",
                  "example": "default"
                }
              ],
              "responseSample": "{\n  \"schema_version\": \"1.0\",\n  \"capability_registry\": {\n    \"source\": \"registry/capability-registry.json\",\n    \"version\": 1,\n    \"checksum\": \"...\"\n  },\n  \"composition_plan\": null\n}",
              "verification": [
                "เรียกโดยไม่ใส่ resolve → composition_plan เป็น null และได้ registry summary",
                "เรียกด้วย resolve=<id> → ได้ CompositionPlan ที่อ้างอิง capability จริง",
                "ยืนยันว่า checksum เปลี่ยนเมื่อ registry เปลี่ยน"
              ],
              "registryRefs": [
                "registry/existence.json",
                "registry/capability-registry.json"
              ]
            },
            {
              "id": "modules",
              "method": "GET",
              "path": "/api/v1/modules",
              "auth": "ไม่ต้องใช้ token (public)",
              "summary": "คืนรายการโมดูลทั้งหมดพร้อม layer, owner, exports และสถานะ",
              "params": [],
              "responseSample": "{\n  \"schema_version\": \"1.0\",\n  \"source\": \"registry/module-registry.json\",\n  \"total\": 1079,\n  \"modules\": [\n    { \"path\": \"engines/pricing.ts\", \"owner\": \"engines\", \"layer\": \"engines\", \"purpose_th\": \"คำนวณราคา\", \"exports\": [\"calcPricing\"], \"status\": \"active\" }\n  ]\n}",
              "verification": [
                "curl -s /api/v1/modules | jq '.total' → ตรงกับจำนวน entry ใน module-registry.json",
                "ตรวจว่า path ที่ต้องการค้นหาปรากฏในผลลัพธ์"
              ],
              "registryRefs": [
                "registry/module-registry.json"
              ]
            }
          ]
        }
      ]
    },
    {
      "slug": "api-tokens",
      "category": "api",
      "title": "การออกและจัดการ API Token",
      "titleEn": "API Token Management",
      "summary": "Token ถูกออกโดยผู้ดูแลระบบเท่านั้น ผ่าน endpoint ที่ต้องมี session ระดับ admin tier และผูกกับบัญชีผู้ใช้จริงในระบบ",
      "sections": [
        {
          "id": "endpoints",
          "heading": "สเปก endpoint",
          "endpoints": [
            {
              "id": "list-tokens",
              "method": "GET",
              "path": "/api/admin/api-tokens",
              "auth": "Session ระดับ ADMIN_TIER",
              "summary": "คืนรายการ token ของบัญชีที่ล็อกอินอยู่",
              "params": [],
              "responseSample": "{\n  \"tokens\": [\n    { \"id\": \"...\", \"label\": \"contractor-runtime\", \"scopes\": [\"read\"], \"created_at\": \"2026-11-11T00:00:00Z\", \"revoked_at\": null }\n  ]\n}",
              "verification": [
                "เรียกด้วย session ผู้ใช้ทั่วไป → ถูกปฏิเสธด้วย auth guard",
                "เรียกด้วย session ผู้ดูแล → 200 และเห็นเฉพาะ token ของตน"
              ],
              "registryRefs": [
                "registry/permissions.json",
                "services/supabase/api-tokens.ts"
              ]
            },
            {
              "id": "create-token",
              "method": "POST",
              "path": "/api/admin/api-tokens",
              "auth": "Session ระดับ ADMIN_TIER",
              "summary": "ออก token ใหม่โดยเลือก scope จาก allow-list ของระบบ",
              "params": [
                {
                  "name": "label",
                  "location": "body",
                  "type": "string",
                  "required": false,
                  "description": "ชื่อเรียกของ token เพื่อให้ผู้ดูแลแยกแยะ",
                  "example": "contractor-runtime"
                },
                {
                  "name": "scopes",
                  "location": "body",
                  "type": "string[]",
                  "required": false,
                  "description": "ขอบเขตสิทธิ์: read, governance:read, governance:propose",
                  "example": "[\"read\"]"
                }
              ],
              "requestSample": "curl -X POST https://<host>/api/admin/api-tokens \\\n  -H \"Content-Type: application/json\" \\\n  --cookie \"session=<admin-session>\" \\\n  -d '{\"label\":\"contractor-runtime\",\"scopes\":[\"read\"]}'",
              "responseSample": "{\n  \"token\": {\n    \"id\": \"...\",\n    \"label\": \"contractor-runtime\",\n    \"scopes\": [\"read\"],\n    \"token\": \"dna_xxxxxxxxxxxxxxxx\"\n  }\n}",
              "verification": [
                "ส่ง scope ที่ไม่อยู่ใน allow-list → 400 พร้อมรายการ allowed",
                "ส่ง label ว่าง → ใช้ค่า default",
                "นำ token ที่ได้ไปเรียก POST /api/v1/query → 200"
              ],
              "registryRefs": [
                "registry/permissions.json",
                "services/supabase/api-tokens.ts"
              ]
            }
          ]
        },
        {
          "id": "scope-rules",
          "heading": "กฎของ scope",
          "table": {
            "columns": [
              "Scope",
              "อนุญาต",
              "หมายเหตุ"
            ],
            "rows": [
              [
                "read",
                "เรียก read command ผ่าน /api/v1/query",
                "ขอบเขตข้อมูลยังถูกจำกัดตามบัญชีผู้ถือ token"
              ],
              [
                "governance:read",
                "อ่านสถานะ governance และ proposal",
                "ไม่สามารถอนุมัติ"
              ],
              [
                "governance:propose",
                "ส่งข้อเสนอเข้าคิวให้มนุษย์อนุมัติ",
                "การอนุมัติต้องทำผ่าน /admin UI"
              ]
            ]
          },
          "bullets": [
            "client ไม่สามารถกำหนด scope เองได้ — มีเพียง route นี้ที่อ่านค่าดังกล่าวและต้องผ่าน session ผู้ดูแล",
            "token ที่ถูก revoke จะถูกปฏิเสธทันทีในคำขอถัดไป",
            "role ของบัญชีถูกตรวจใหม่ทุกคำขอ หากถูกถอดออกจาก admin tier token จะใช้ไม่ได้ทันที"
          ]
        }
      ]
    },
    {
      "slug": "line-webhook",
      "category": "integrations",
      "title": "LINE Webhook และ Mini App",
      "titleEn": "LINE Messaging Integration",
      "summary": "supabase/functions/line-webhook-v2 รับ event จาก LINE ตรวจลายเซ็น x-line-signature แล้วกระจายงานตามชนิด event ก่อนตอบ ACK ภายในเวลาที่ LINE กำหนด",
      "sections": [
        {
          "id": "endpoint",
          "heading": "สเปก webhook",
          "endpoints": [
            {
              "id": "line-webhook",
              "method": "POST",
              "path": "$SUPABASE_URL/functions/v1/line-webhook-v2",
              "runtime": "supabase-edge-function",
              "auth": "ลายเซ็น x-line-signature (HMAC-SHA256 ของ channel secret) — ฟังก์ชันตั้ง verify_jwt = false",
              "summary": "รับ LINE webhook event ทุกชนิด (message, follow, unfollow, postback, location, image, file, audio, group) และส่งต่อไปยัง handler ที่รับผิดชอบ",
              "params": [
                {
                  "name": "x-line-signature",
                  "location": "header",
                  "type": "string (base64)",
                  "required": true,
                  "description": "HMAC-SHA256 ของ raw body ด้วย channel secret"
                },
                {
                  "name": "destination",
                  "location": "body",
                  "type": "string",
                  "required": true,
                  "description": "LINE user/group id ของบัญชีที่รับ event ใช้เลือก credential ของร้านนั้น"
                },
                {
                  "name": "events",
                  "location": "body",
                  "type": "array",
                  "required": true,
                  "description": "รายการ event ตามสเปก LINE Messaging API"
                },
                {
                  "name": "events[].type",
                  "location": "body",
                  "type": "string",
                  "required": true,
                  "description": "message | follow | unfollow | postback | join | leave",
                  "example": "message"
                },
                {
                  "name": "events[].message.type",
                  "location": "body",
                  "type": "string",
                  "required": false,
                  "description": "text | image | file | audio | location",
                  "example": "text"
                },
                {
                  "name": "events[].replyToken",
                  "location": "body",
                  "type": "string",
                  "required": false,
                  "description": "token สำหรับตอบกลับภายในเวลาที่กำหนด"
                }
              ],
              "requestSample": "curl -X POST \"$SUPABASE_URL/functions/v1/line-webhook-v2\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"x-line-signature: <base64-hmac>\" \\\n  -d '{\n    \"destination\": \"Uxxxxxxxxxxxxxxxx\",\n    \"events\": [{\n      \"type\": \"message\",\n      \"replyToken\": \"xxxxxxxx\",\n      \"source\": { \"userId\": \"Uxxxxxxxxxxxxxxxx\", \"type\": \"user\" },\n      \"message\": { \"type\": \"text\", \"id\": \"1\", \"text\": \"ยอดค้างชำระ\" }\n    }]\n  }'",
              "responseSample": "{ \"ok\": true }",
              "verification": [
                "ยืนยันว่า verify_jwt = false ใน supabase/config.toml สำหรับฟังก์ชันนี้ เพื่อให้ LINE เรียกได้โดยไม่มี Supabase JWT",
                "ตั้งค่า LINE_CHANNEL_SECRET และ LINE_CHANNEL_ACCESS_TOKEN ใน secrets ของโปรเจกต์",
                "ส่งคำขอที่ลายเซ็นไม่ถูกต้อง → 401 และไม่มีการประมวลผล event",
                "ส่งคำขอที่ลายเซ็นถูกต้อง → 200 {\"ok\":true} และ event ถูกบันทึกในตาราง line_events",
                "ทดสอบ event text ที่เป็นคำสั่งคนขับ เช่น eta 82-1234 → ต้องได้รับการตอบกลับผ่าน reply API"
              ],
              "registryRefs": [
                "registry/communication/line.registry.json",
                "supabase/functions/line-webhook-v2",
                "registry/PROVIDER_DIRECTORY.json"
              ]
            }
          ]
        },
        {
          "id": "pipeline",
          "heading": "ลำดับการประมวลผล event",
          "bullets": [
            "ตรวจลายเซ็นจาก raw body ก่อน parse เพื่อป้องกัน payload ปลอม",
            "เลือก channel secret และ access token ตาม destination ผ่าน connect_external_account_credentials",
            "แปลง event เป็น handler เฉพาะทาง: follow, unfollow, text, location, driver, postback, image, file, audio, group",
            "ประมวลผล event พร้อมกันและส่งงานเบื้องหลังผ่าน EdgeRuntime.waitUntil() เพื่อตอบ 200 ทันที",
            "บันทึก event ลง line_events เพื่อให้ admin console และ analytics ตรวจย้อนหลังได้"
          ]
        },
        {
          "id": "mini-app",
          "heading": "การเชื่อมต่อ Mini App",
          "bullets": [
            "Mini App ที่ฝังใน LINE ใช้ LIFF token เพื่อระบุผู้ใช้ แล้วเรียกข้อมูลผ่าน endpoint ของ DNA OS ด้วย token ที่ระบบออกให้",
            "ห้ามฝัง channel access token หรือ service role key ฝั่ง client เด็ดขาด — ให้ proxy ผ่าน route handler ของคุณ",
            "ผลลัพธ์ที่แสดงใน Mini App ต้องมาจาก read command เดียวกับที่ /api/v1/query ใช้ เพื่อให้ตัวเลขตรงกับระบบหลัก"
          ]
        }
      ]
    },
    {
      "slug": "mcp-server",
      "category": "integrations",
      "title": "MCP Server สำหรับ AI Agent",
      "titleEn": "MCP Server",
      "summary": "dna-os-mcp-server เป็น protocol adapter ที่แปลง MCP tool call เป็น HTTP request ไปยัง endpoint ของ DNA OS โดยไม่ถือ business logic ของตัวเอง",
      "sections": [
        {
          "id": "contract",
          "heading": "สัญญาเครื่องมือ MCP",
          "lead": "สัญญาทั้งหมดประกาศใน registry/mcp-planned-manifest.json ซึ่งเป็นไฟล์ที่ยึด HTTP contract ก่อนเครื่องมือถูกเลื่อนสถานะเป็น active",
          "table": {
            "columns": [
              "MCP tool",
              "HTTP endpoint",
              "ลักษณะงาน",
              "สถานะ"
            ],
            "rows": [
              [
                "contractor.task.claim",
                "POST /api/v1/mcp/contractor/task-claim",
                "รับงานจากคิวและสร้าง JobContract",
                "active"
              ],
              [
                "contractor.evidence.submit",
                "POST /api/v1/mcp/contractor/evidence-submit",
                "ส่งหลักฐานและตรวจ reference",
                "active"
              ],
              [
                "contractor.status",
                "GET /api/v1/mcp/contractor/status?ownerId=",
                "อ่านสถานะงาน (read-only)",
                "active"
              ],
              [
                "business.dispatch",
                "POST /api/v1/mcp/business/dispatch",
                "execute read command ที่อยู่ใน whitelist",
                "active"
              ],
              [
                "architecture.system_summary",
                "GET /api/v1/mcp/architecture/summary",
                "สรุปสถาปัตยกรรมและกฎรัฐธรรมนูญ",
                "active"
              ],
              [
                "architecture.get_dependency_graph",
                "GET /api/v1/mcp/architecture/dependency-graph",
                "กราฟการพึ่งพา",
                "active"
              ],
              [
                "registry.list_modules",
                "GET /api/v1/modules",
                "รายการโมดูล",
                "active"
              ],
              [
                "registry.list_commands",
                "GET /api/v1/mcp/registry/commands",
                "รายการคำสั่งดิบจาก registry",
                "active"
              ],
              [
                "runtime.query",
                "POST /api/v1/query",
                "passthrough ไปยัง dispatcher",
                "planned"
              ],
              [
                "governance.status",
                "GET /api/v1/mcp/governance/status",
                "สถานะ governance",
                "active"
              ],
              [
                "governance.registry_proposals",
                "GET /api/v1/mcp/governance/registry-proposals",
                "ข้อเสนอที่รออนุมัติ",
                "active"
              ],
              [
                "governance.deploy_approval_status",
                "GET /api/v1/mcp/governance/deploy-approval-status",
                "สถานะการอนุมัติ deploy",
                "active"
              ],
              [
                "governance.session_proposals",
                "GET /api/v1/mcp/governance/session-proposals",
                "ข้อเสนอของ session",
                "active"
              ],
              [
                "governance.impact",
                "GET /api/v1/mcp/governance/impact/[evidenceEventId]",
                "ผลกระทบของหลักฐานหนึ่งรายการ",
                "active"
              ]
            ]
          }
        },
        {
          "id": "config",
          "heading": "การตั้งค่าฝั่ง client",
          "code": {
            "language": "json",
            "content": "{\n  \"mcpServers\": {\n    \"dna-os\": {\n      \"command\": \"node\",\n      \"args\": [\"dna-os-mcp-server/dist/index.js\"],\n      \"env\": {\n        \"DNA_OS_BASE_URL\": \"https://<host>\",\n        \"DNA_OS_API_TOKEN\": \"<token จาก /admin/api-tokens>\"\n      }\n    }\n  }\n}"
          },
          "bullets": [
            "Server นี้ไม่มี allow-list ของโมดูลภายใน — เข้าถึงสถานะ DNA OS ผ่าน HTTP ตาม manifest เท่านั้น",
            "เครื่องมือที่ต้องใช้ token ต้องได้รับ DNA_OS_API_TOKEN จากผู้ดูแลระบบ",
            "การยืนยัน write command ยังคงเกิดขึ้นที่ dispatcher ฝั่ง DNA OS ไม่ใช่ที่ MCP client"
          ]
        }
      ]
    },
    {
      "slug": "connectors",
      "category": "integrations",
      "title": "Connector และ Provider Registry",
      "titleEn": "Connectors & Providers",
      "summary": "Connector ทุกตัวอ้างอิง Provider ที่ประกาศใน registry/PROVIDER_DIRECTORY.json และเดินตามวงจรชีวิตที่กำหนดใน registry/connector-lifecycle.json",
      "sections": [
        {
          "id": "lifecycle",
          "heading": "วงจรชีวิตของ connector",
          "bullets": [
            "ประกาศ provider และ capability ที่รองรับใน PROVIDER_DIRECTORY.json ก่อนสร้าง connector",
            "ลงทะเบียน connector พร้อม credential ที่เก็บในที่ปลอดภัย ไม่เขียนค่าลงซอร์สโค้ด",
            "ทดสอบการเชื่อมต่อผ่านปุ่ม Test ของ /admin/connectors ซึ่งเรียก tester จริง",
            "เปิดใช้งานเมื่อทดสอบผ่าน และบันทึกเหตุการณ์วงจรชีวิตลงตาราง connectors",
            "เมื่อล้มเหลว ระบบทำตาม failure_flow ใน connector-lifecycle.json: error, auto_disable, log, notify_status"
          ]
        },
        {
          "id": "surfaces",
          "heading": "จุดเชื่อมต่อที่ผู้ใช้เห็น",
          "table": {
            "columns": [
              "Surface",
              "หน้าที่",
              "แหล่งข้อมูล"
            ],
            "rows": [
              [
                "/admin/connectors",
                "ศูนย์ควบคุม connector: Test / Enable / Remove และประวัติเหตุการณ์",
                "ตาราง connectors + WORKER_ADAPTER_REGISTRY + Integration Gateway"
              ],
              [
                "/admin/god-view",
                "มุมมองรวมสถานะระบบและ override ที่ต้องมีสิทธิ์",
                "registry + runtime state"
              ],
              [
                "/admin/roles",
                "จัดการบทบาทและระดับสิทธิ์",
                "registry/permissions.json + profiles"
              ],
              [
                "/admin/rules",
                "ดูและรันกฎ governance",
                "registry/GOVERNANCE_REGISTRY.json"
              ],
              [
                "/admin/line-customers",
                "ผูกบัญชี LINE กับ partner และดูประวัติลูกค้า",
                "line_events + partners"
              ]
            ]
          }
        },
        {
          "id": "credentials",
          "heading": "การจัดเก็บ credential",
          "bullets": [
            "credential ของ provider เก็บในฐานข้อมูลหรือ secret store ของแพลตฟอร์ม ไม่เก็บใน registry ที่ commit ลง git",
            "เอกสารนี้แสดงเฉพาะชื่อตัวแปรและตำแหน่งที่ระบบอ่านค่า ไม่แสดงค่าใด ๆ",
            "ห้ามส่ง secret กลับใน response ของ API ทุกตัว"
          ]
        }
      ]
    },
    {
      "slug": "environment-keys",
      "category": "settings",
      "title": "ตัวแปรสภาพแวดล้อม",
      "titleEn": "Environment Variables",
      "summary": "รายการคีย์ที่ระบบอ่านจริง พร้อมขอบเขตการใช้งาน ค่าจริงเก็บใน .env.local ระหว่างพัฒนา และใน environment ของแพลตฟอร์มเมื่อขึ้น production",
      "sections": [
        {
          "id": "keys",
          "heading": "คีย์ที่ระบบใช้งาน",
          "table": {
            "columns": [
              "ตัวแปร",
              "ขอบเขต",
              "ใช้ที่ไหน",
              "หมายเหตุ"
            ],
            "rows": [
              [
                "NEXT_PUBLIC_SUPABASE_URL",
                "public",
                "client และ server",
                "URL ของโปรเจกต์ Supabase production"
              ],
              [
                "NEXT_PUBLIC_SUPABASE_ANON_KEY",
                "public",
                "client",
                "คีย์สาธารณะสำหรับอ่านตาม RLS"
              ],
              [
                "SUPABASE_SERVICE_ROLE_KEY",
                "server only",
                "token store, admin queries, deploy-gate",
                "ห้ามส่งไป client เด็ดขาด"
              ],
              [
                "NEXT_PUBLIC_APP_URL",
                "public",
                "canonical URL, sitemap, structured data",
                "ถ้าไม่ตั้ง ระบบจะไม่สร้าง hostname เอง"
              ],
              [
                "LINE_CHANNEL_SECRET",
                "server only",
                "line-webhook-v2",
                "ใช้ตรวจลายเซ็นของ event"
              ],
              [
                "LINE_CHANNEL_ACCESS_TOKEN",
                "server only",
                "line-webhook-v2",
                "ใช้ตอบกลับผ่าน Messaging API"
              ],
              [
                "EQUIPMENT_TELEMETRY_SECRET",
                "server only",
                "app/api/internal/equipment/telemetry",
                "bearer secret สำหรับ ingestion อุปกรณ์"
              ]
            ]
          }
        },
        {
          "id": "rules",
          "heading": "ข้อปฏิบัติ",
          "bullets": [
            "ค่าจริงอยู่ใน .env.local (ไม่ถูก commit) หรือ environment ของแพลตฟอร์มเท่านั้น",
            "ไฟล์เอกสารและ registry ระบุได้เฉพาะชื่อตัวแปร ไม่ระบุค่า",
            "เมื่อคีย์ service role หาย ระบบจะคืน 503 พร้อมข้อความบอกผู้ดูแล ไม่เดาค่าแทน",
            "ห้ามใช้ NEXT_PUBLIC_ นำหน้าคีย์ที่ให้สิทธิ์เขียนหรือข้าม RLS",
            "ตรวจความพร้อมทั้งชุดด้วย npm run verify-env ก่อน deploy"
          ]
        }
      ]
    },
    {
      "slug": "access-control",
      "category": "settings",
      "title": "ระบบสิทธิ์และขอบเขตข้อมูล",
      "titleEn": "Access Control & Data Scope",
      "summary": "สิทธิ์ถูกตัดสินจาก profiles.role ทุกคำขอ โดยเทียบกับ role_tiers ใน registry/permissions.json และขอบเขตข้อมูลถูกจำกัดตามบัญชีผู้ถือ token",
      "sections": [
        {
          "id": "tiers",
          "heading": "ระดับสิทธิ์",
          "table": {
            "columns": [
              "ระดับ",
              "แหล่งประกาศ",
              "ใช้กับ",
              "ผลเมื่อไม่ผ่าน"
            ],
            "rows": [
              [
                "ADMIN_TIER",
                "registry/permissions.json → role_tiers.admin_tier",
                "หน้า /admin, /api/admin/*, token ภายนอก",
                "401 หรือ 403"
              ],
              [
                "Session ผู้ใช้ทั่วไป",
                "core/auth/session.ts",
                "พอร์ทัล user/partner",
                "redirect ไปหน้า login"
              ],
              [
                "API token",
                "services/supabase/api-tokens.ts",
                "/api/v1/query",
                "401 เมื่อ revoke, 403 เมื่อ role หลุดจาก admin tier"
              ],
              [
                "Worker contract",
                "app/api/admin/ide/_lib/workerContract.ts",
                "/api/v1/mcp/contractor/*",
                "ปฏิเสธงานที่ไม่อยู่ในสัญญา"
              ]
            ]
          }
        },
        {
          "id": "data-scope",
          "heading": "ขอบเขตข้อมูล",
          "bullets": [
            "resolveTokenDataScope แปลงโปรไฟล์ของผู้ถือ token เป็น DataScope ก่อน execute ทุกคำสั่ง",
            "runWithDataScope ครอบการเรียก dispatcher เพื่อให้ engine เห็นเฉพาะข้อมูลในขอบเขต",
            "RLS ในฐานข้อมูลเป็นชั้นป้องกันที่สอง ไม่ใช่ชั้นเดียว",
            "ตารางสำคัญเปิด RLS และกำหนด policy สำหรับ service role และ admin workspace แยกกัน"
          ]
        },
        {
          "id": "secrets",
          "heading": "การจัดการคีย์",
          "bullets": [
            "คีย์ service role ใช้ได้เฉพาะฝั่ง server และไม่ถูก log",
            "ระบบ log เฉพาะขนาดและรูปแบบของ token ไม่บันทึกค่าหรือ prefix",
            "การหมุนคีย์ทำได้โดยอัปเดต environment แล้วรีสตาร์ท service โดยไม่ต้องแก้โค้ด",
            "Vault ของระบบรองรับการเขียน/อ่าน/ลบคีย์ผ่าน RPC ที่ต้องใช้ service client"
          ]
        }
      ]
    },
    {
      "slug": "deploy-gate",
      "category": "settings",
      "title": "เกณฑ์การปล่อยระบบ (Deploy Gate)",
      "titleEn": "Deploy Gate Criteria",
      "summary": "ทุก release ต้องผ่าน deploy-gate ซึ่งตรวจสภาพแวดล้อม หลักฐาน Dev Loop สถานะสุขภาพ และความสอดคล้องของเลขเวอร์ชันก่อนอนุญาตให้ build",
      "sections": [
        {
          "id": "criteria",
          "heading": "เกณฑ์ที่ต้องผ่าน",
          "table": {
            "columns": [
              "เกณฑ์",
              "สิ่งที่ตรวจ",
              "ผลเมื่อไม่ผ่าน"
            ],
            "rows": [
              [
                "Environment",
                "คีย์ที่จำเป็นครบและอ่านได้",
                "NOT READY พร้อมระบุคีย์ที่ขาด"
              ],
              [
                "Dev Loop evidence",
                "อ่าน dev_loop_check_results ได้จริงและไม่มีผล fail",
                "NOT READY พร้อมเหตุผลของ path ที่ fail"
              ],
              [
                "Registry proposals",
                "ไม่มีข้อเสนอสถานะ pending",
                "NOT READY พร้อมรายการที่รออนุมัติ"
              ],
              [
                "Release chain",
                "package.json เป็น semver และ CURRENT_STATE อยู่ใน CHANGELOG",
                "NOT READY เมื่อหลักฐานหายหรือเลขเวอร์ชันผิด"
              ],
              [
                "Health & governance",
                "DNA_OS_HEALTH.json และ governance summary",
                "NOT READY เมื่อต่ำกว่าเกณฑ์"
              ]
            ]
          }
        },
        {
          "id": "run",
          "heading": "วิธีรันและการอ่านผล",
          "code": {
            "language": "bash",
            "content": "# ตรวจก่อน build (prebuild เรียกอัตโนมัติ)\nnpm run deploy-gate\n\n# ผลที่ผ่าน\nDNA OS Health Score: 100 / 100\nPASSED: 8 / 8\nFAILED: 0\nREADY\n\n# ผลที่ไม่ผ่าน\nDeploy Blocked — Dev Loop Pipeline\n  - Dev Loop evidence query failed: <reason>"
          },
          "bullets": [
            "เกณฑ์นี้ออกแบบแบบ fail-closed: หากอ่านหลักฐานไม่ได้ ระบบถือว่าไม่ผ่านและหยุดทันที",
            "คิวรีที่คืน error, โยน exception, คืน dataset ว่าง หรือคืน null จะถูกนับเป็นหลักฐานไม่ครบทั้งหมด",
            "ทุกครั้งที่ gate ผ่าน จะมีการบันทึกหลักฐานการตรวจไว้ใน registry/VALIDATION_RESULTS.json",
            "การอนุมัติ deploy ของมนุษย์ยังต้องทำผ่าน governance flow ก่อนปล่อยจริง"
          ]
        },
        {
          "id": "verify",
          "heading": "ขั้นตอนยืนยันก่อนปล่อย",
          "bullets": [
            "รัน npm run verify-env และต้องได้ exit 0",
            "รัน npm run deploy-gate และต้องเห็น READY",
            "รัน npx tsc --noEmit และต้องไม่มีข้อผิดพลาด",
            "รัน npm run build และต้องคอมไพล์ทุกหน้าสำเร็จ",
            "ตรวจ smoke test เส้นทางหลัก: /, /products, /admin/docs และ /api/v1/developer-portal"
          ]
        }
      ]
    }
  ],
  "journey": {
    "sections": [
      {
        "id": "overview",
        "kicker": "00 · เริ่มตรงนี้",
        "title": "ท่อเดียวจากปัญหาธุรกิจ ถึง endpoint จริง",
        "kind": "journey"
      },
      {
        "id": "settings",
        "kicker": "01 · ตั้งค่า",
        "title": "ก่อนเดินเรื่อง: secret, lifecycle และประตูที่ปิดไว้",
        "kind": "settings"
      },
      {
        "id": "problem-line",
        "kicker": "02 · ปัญหา",
        "title": "แชทลูกค้าไม่เคยกลายเป็นงาน",
        "kind": "journey"
      },
      {
        "id": "problem-money",
        "kicker": "03 · ปัญหา",
        "title": "สลิปโอนเงินไม่เคยกระทบยอด",
        "kind": "journey"
      },
      {
        "id": "problem-logistics",
        "kicker": "04 · ปัญหา",
        "title": "ชั่วโมงเครื่องกับเที่ยววิ่ง เป็นตัวเลขที่ไม่มีใครรู้",
        "kind": "journey"
      },
      {
        "id": "problem-maintenance",
        "kicker": "05 · ปัญหา",
        "title": "ซ่อมบำรุงตามปฏิทิน ทั้งที่ของจริงนับชั่วโมง",
        "kind": "journey"
      },
      {
        "id": "agents",
        "kicker": "06 · สถาปัตยกรรม",
        "title": "Multi-Agent Routing & Handoff — หลายตัว แต่ประตูเดียว",
        "kind": "journey"
      },
      {
        "id": "connect",
        "kicker": "07 · ลากสาย",
        "title": "Token, secret และรหัสที่ต้องเจอ",
        "kind": "reference"
      },
      {
        "id": "index",
        "kicker": "08 · ดัชนี",
        "title": "Endpoint ทั้งหมดที่พอร์ทัลนี้อ้างถึง",
        "kind": "reference"
      }
    ],
    "section_kinds": {
      "overview": "journey",
      "settings": "settings",
      "problem-line": "journey",
      "problem-money": "journey",
      "problem-logistics": "journey",
      "problem-maintenance": "journey",
      "agents": "journey",
      "connect": "reference",
      "index": "reference"
    },
    "pipelines": [
      {
        "id": "line-ingress",
        "sectionId": "problem-line",
        "title": "LINE → GatewayEvent → canonical evidence log",
        "titleEn": "Inbound LINE Observe",
        "summary": "จุดเดียวที่อ่าน payload ดิบของ LINE คือ adapter; ปลายทางเป็น event กลางที่ทุก engine ใช้ร่วมกัน ไม่มีตารางธุรกิจถูกแตะในเส้นนี้",
        "stages": [
          {
            "label": "Trigger — LINE ยิง event เข้ามา",
            "detail": "LINE ส่ง POST /api/gateway/observe/line พร้อม destination + events[] โดยไม่มี session; route อ่าน body เท่านั้นเพื่อรู้ว่ามาจากบัญชีไหน ยังไม่แตะฐานข้อมูล",
            "evidence": "app/api/gateway/observe/line/route.ts · POST"
          },
          {
            "label": "Gate — เลือก secret ของบัญชีปลายทาง",
            "detail": "destination ถูกใช้ resolve channel secret จาก public.connect_external_account_credentials; ถ้าไม่พบจึงถอยไปใช้ LINE_CHANNEL_SECRET ของบัญชีหลัก",
            "evidence": "engines/connect/accounts.ts · resolveExternalAccountCredential",
            "refuses": "ไม่มี secret เลย → 503"
          },
          {
            "label": "Gate — ตรวจลายเซ็นก่อนอ่าน/เขียนใด ๆ",
            "detail": "HMAC-SHA256 ของ raw body เข้ารหัส base64 แล้วเทียบ x-line-signature ด้วย timingSafeEqual — body ที่ยังไม่ผ่านไม่ถูกตีความต่อ",
            "evidence": "engines/integration-gateway/adapters/line.ts · verifyLineSignature",
            "refuses": "ลายเซ็นผิด/หาย → 401 · JSON เสีย → 400"
          },
          {
            "label": "Normalize — LINE event → GatewayEvent",
            "detail": "แต่ละ event แปลเป็น capability กลาง (message / document / location / identity signal / interaction) และ evidence ถูกจำกัดด้วย key whitelist 9 คีย์ — ข้อความลูกค้าและ reply token ไม่มีช่องเดินทางเข้า core",
            "evidence": "engines/integration-gateway/adapters/line.ts · normalizeLineWebhook / EVIDENCE_KEYS",
            "refuses": "ชนิด event ที่ยังไม่รองรับ → นับ skipped ไม่แต่ง capability ขึ้นเอง"
          },
          {
            "label": "Gate + Bus — สัญญาและช่องทาง",
            "detail": "publish() ตรวจ isGatewayEvent ก่อน แล้วตรวจ channelIsEnabled (observe/propose เท่านั้น) จากนั้น dispatchEvent('gateway.<capability>') และเขียน integration_gateway_events; ช่อง propose มีแถว proposal รอเจ้าของตัดสิน",
            "evidence": "engines/integration-gateway/event-bus/bus.ts · publish · policy/gate.ts",
            "refuses": "สัญญาไม่ครบ/ช่องปิด → ok:false แล้ว route ตอบ 500"
          },
          {
            "label": "Ack — เฉพาะเมื่อเขียนสำเร็จ",
            "detail": "ตอบ { ok, accepted, skipped } หลังทุก event ผ่าน bus เท่านั้น ไม่ ack ล่วงหน้า",
            "evidence": "app/api/gateway/observe/line/route.ts · NextResponse.json"
          }
        ]
      },
      {
        "id": "kbank-ingest",
        "sectionId": "problem-money",
        "title": "KBank webhook → staging → Observe",
        "titleEn": "Bank Inbound Ingest",
        "summary": "เส้นนี้ยังเป็น mock-mode เพราะยังไม่ได้ secret จาก KBank (ยื่น 26/10/2026 เลขคำขอ 1792988917) แต่ประตูความปลอดภัยถูกใช้ตัวเดียวกับ connector webhook จริงแล้ว",
        "stages": [
          {
            "label": "Trigger — ธนาคารยิง webhook",
            "detail": "POST /api/connectors/webhook/kbank รับเฉพาะ body ที่ผ่าน gate; ระหว่างรอ sandbox ยังไม่มี secret จริง จึงยังไม่มีการรับ event จากธนาคารได้",
            "evidence": "app/api/connectors/webhook/kbank/route.ts"
          },
          {
            "label": "Gate — สถานะ connector",
            "detail": "getConnectedConnector ต้องได้ connector ที่ status = 'connected' และ enabled = true เท่านั้น; connector ที่ปิดหรือ error หยุดรับทันทีโดยไม่ต้อง deploy",
            "evidence": "core/connector-loader.ts · getConnectedConnector",
            "refuses": "404 connector_not_found / connector_not_enabled"
          },
          {
            "label": "Gate — ขนาด body",
            "detail": "ปฏิเสธก่อน buffer เมื่อ content-length หรือ byte จริงเกิน 64KB",
            "evidence": "app/api/connectors/webhook/[slug]/_lib/webhookGate.ts · MAX_WEBHOOK_BODY_BYTES",
            "refuses": "413 body_too_large"
          },
          {
            "label": "Gate — ลายเซ็น (fail-closed สองชั้น)",
            "detail": "ถ้า connector ไม่ได้ตั้ง signature_header หรือ signing_secret ระบบปฏิเสธ (ไม่ใช่ข้ามการตรวจ); เมื่อตั้งแล้วต้องมี header และ HMAC ต้องตรง timingSafeEqual ก่อน parse body",
            "evidence": "app/api/connectors/webhook/[slug]/_lib/webhookGate.ts · evaluateWebhookRequest",
            "refuses": "503 signature_not_configured · 401 signature_missing / signature_invalid"
          },
          {
            "label": "Staging — เก็บ raw ก่อนแปล",
            "detail": "เขียน kbank_webhook_events (raw_body + allow-listed headers, status 'received') ก่อน normalize เพื่อให้ reconcile/payout มีหลักฐานระดับไบต์ ไม่ใช่แค่ evidence ที่แปลงแล้ว",
            "evidence": "app/api/connectors/webhook/kbank/route.ts · PHASE 1 (kbank_webhook_events insert)",
            "refuses": "เขียน staging ไม่ได้ → 500 ไม่ ack body ที่ผ่านลายเซ็นแล้ว"
          },
          {
            "label": "Normalize + Publish",
            "detail": "normalizeKBankWebhook แปลงเป็น capability กลาง payment.* / payout.* / transfer.* แล้ว publish เข้า Observe Channel",
            "evidence": "engines/integration-gateway/adapters/kbank.ts · normalizeKBankWebhook",
            "refuses": "ไม่มี event ที่แปลงได้ → 400 · publish ล้ม → 500"
          },
          {
            "label": "Ack — { received: true, published }",
            "detail": "ตอบหลัง publish สำเร็จทุก event เท่านั้น; ไม่มี published ก็ไม่ตอบว่ารับแล้ว",
            "evidence": "app/api/connectors/webhook/kbank/route.ts · NextResponse.json"
          }
        ]
      },
      {
        "id": "connect-event",
        "sectionId": "problem-line",
        "title": "GatewayEvent → Connect → canonical business event",
        "titleEn": "Business Event Bridge",
        "summary": "Transport ฝั่ง provider ส่ง 'คำสั่ง' ไม่ใช่ 'แถวข้อมูล' — writer เป็นเจ้าของทุกคอลัมน์ที่เปลี่ยนได้",
        "stages": [
          {
            "label": "Trigger — transport ส่ง canonical command",
            "detail": "POST /api/internal/connect/event รับเฉพาะคำสั่งที่ normalize แล้ว (ไม่มี payload ของ LINE ติดมา, ไม่มีการ branch ตาม provider)",
            "evidence": "app/api/internal/connect/event/route.ts"
          },
          {
            "label": "Gate — secret ของ bridge",
            "detail": "header x-connect-internal-secret ต้องตรงกับ CONNECT_INTERNAL_SECRET",
            "evidence": "app/api/internal/connect/event/route.ts · process.env.CONNECT_INTERNAL_SECRET",
            "refuses": "ไม่ตั้ง secret → 503 · ไม่ตรง → 401"
          },
          {
            "label": "Gate — คำสั่งปิด + actor",
            "detail": "รับ 4 คำสั่งเท่านั้น: business_event.create / .transition / .enrich / .link_transaction และต้องมี actor ทุกครั้ง",
            "evidence": "app/api/internal/connect/event/route.ts · COMMANDS",
            "refuses": "คำสั่งนอกชุด/ไม่มี actor → 400"
          },
          {
            "label": "Write — canonical event writer เท่านั้น",
            "detail": "createBusinessEvent / transitionBusinessEvent / enrichBusinessEvent / linkBusinessEventTransaction เป็นเจ้าของ allow-list ของ field และชุดสถานะ; ผู้เรียกส่ง row shape เข้ามาไม่ได้",
            "evidence": "engines/canonical/event-write.ts",
            "refuses": "422 EVENT_UNKNOWN_STATE / EVENT_TRANSITION_REFUSED / EVENT_FIELD_NOT_ALLOWED"
          },
          {
            "label": "Audit — metadata ที่บอกที่มา",
            "detail": "ทุกแถวที่สร้างพก written_by = canonical:event-write@<actor> และ source_provider ไว้ใน metadata เพื่อตรวจย้อนได้โดยไม่ต้องเดา",
            "evidence": "engines/canonical/event-write.ts · createBusinessEvent"
          }
        ]
      },
      {
        "id": "payment-confirm",
        "sectionId": "problem-money",
        "title": "สลิป → canonical payment (guards → RPC เดียว)",
        "titleEn": "Money In",
        "summary": "ผู้เรียกบอกได้แค่ว่า 'สลิปใบไหน' — ตัวเลขเงินทุกตัวอ่านจาก canonical ข้างในและ commit ใน transaction เดียว",
        "stages": [
          {
            "label": "Trigger — transport แจ้งว่าพบหลักฐานการจ่าย",
            "detail": "POST /api/internal/connect/payment-confirm รับเฉพาะ receipt_document_id + actor; ไม่รับ amount/outstanding เพราะผู้เรียกใส่ตัวเลขเงินเองไม่ได้",
            "evidence": "app/api/internal/connect/payment-confirm/route.ts"
          },
          {
            "label": "Gate — secret ของ bridge",
            "detail": "header x-payment-confirm-internal-secret เทียบ PAYMENT_CONFIRM_INTERNAL_SECRET",
            "evidence": "app/api/internal/connect/payment-confirm/route.ts · process.env.PAYMENT_CONFIRM_INTERNAL_SECRET",
            "refuses": "ไม่ตั้ง → 503 · ไม่ตรง → 401 · body ผิดรูป → 400"
          },
          {
            "label": "Flow + guards — ตรวจก่อนแตะเงิน",
            "detail": "confirmReceiptPayment → confirmPayment: receipt ต้องเป็น transfer_slip/payment_advice, ต้องยังไม่ถูกจัดสรร, Σ allocation ≤ ยอด receipt, invoice ต้องไม่ VOID บริษัทตรง และไม่เกินยอดคงค้าง",
            "evidence": "engines/canonical/receipt-confirm-flow.ts · engines/canonical/payment-write.ts",
            "refuses": "throw เป็นรหัส PAYMENT_* (เช่น PAYMENT_OVERPAYMENT_NOT_ALLOWED, PAYMENT_ALREADY_ALLOCATED)"
          },
          {
            "label": "Commit — RPC เดียว ไม่มี fallback",
            "detail": "confirm_payment_allocation เขียน allocations + ledger + invoice projection + receipt state + snapshot + audit ใน transaction เดียว; writer ไม่เขียนแถวเงินเองเลย และไม่มีเส้นสำรอง",
            "evidence": "engines/canonical/payment-write.ts · confirmPayment → db.rpc('confirm_payment_allocation')",
            "refuses": "RPC หาย/ปฏิเสธ → fail closed (mapPaymentRpcError)"
          },
          {
            "label": "Ack — ผลจริง ไม่ใช่ผลที่หวัง",
            "detail": "ตอบ 200 { already_reconciled, new_outstanding, debt_event_id, acknowledgement }; acknowledgement เป็นส่วนเสริมและล้มได้โดยไม่ทำให้การยืนยันเงินล้ม",
            "evidence": "app/api/internal/connect/payment-confirm/route.ts · acknowledgePayment"
          }
        ]
      },
      {
        "id": "expense-payable",
        "sectionId": "problem-money",
        "title": "เงินออก → canonical expense / payable",
        "titleEn": "Money Out",
        "summary": "ไม่มี approval ใหม่ถูกสร้างที่ bridge: คำขอเริ่ม pending เสมอ และการขยับสถานะถูกกรองด้วยสถานะต้นทางจริง",
        "stages": [
          {
            "label": "Trigger — transport แจ้งค่าใช้จ่าย/คำขอจ่าย",
            "detail": "POST /api/internal/connect/expense รับ 3 คำสั่ง: expense.record, payable.request, payable.transition",
            "evidence": "app/api/internal/connect/expense/route.ts · COMMANDS"
          },
          {
            "label": "Gate — secret ของ bridge",
            "detail": "header x-expense-internal-secret เทียบ EXPENSE_INTERNAL_SECRET",
            "evidence": "app/api/internal/connect/expense/route.ts",
            "refuses": "ไม่ตั้ง → 503 · ไม่ตรง → 401"
          },
          {
            "label": "Write — writer ตัดสิน ไม่ใช่ผู้เรียก",
            "detail": "recordCompanyExpense ตรวจ source (line_chat / subscription_email_sync / admin_manual) และ amount > 0; requestPartnerPayable ตั้ง status 'pending' เอง; transitionPartnerPayable อัปเดตแบบกรองสถานะต้นทาง",
            "evidence": "engines/canonical/expense-write.ts",
            "refuses": "0 แถวถูกอัปเดต → PAYABLE_TRANSITION_INVALID · 422 EXPENSE_* / PAYABLE_*"
          },
          {
            "label": "Policy — เวลาเสร็จมาจากหลักฐาน",
            "detail": "completed_at ถูก derive จาก bank_email_verified เท่านั้น — ผู้เรียกตั้งเวลาเสร็จเองไม่ได้",
            "evidence": "engines/canonical/expense-write.ts · transitionPartnerPayable"
          },
          {
            "label": "Audit — ทุกแถวเงินออกมีร่องรอย",
            "detail": "recordCanonicalAudit เขียน expense.recorded / payable.requested / payable.transitioned ทุกครั้ง",
            "evidence": "engines/canonical/audit.ts · recordCanonicalAudit"
          }
        ]
      },
      {
        "id": "telemetry-hours",
        "sectionId": "problem-logistics",
        "title": "VCC → engine hours → total_engine_hours",
        "titleEn": "Telemetry to Engine Hours",
        "summary": "ชั่วโมงเครื่องไม่ได้มาจากการเดา แต่มาจาก ping สองตัวที่บอกว่าเครื่องติดทั้งคู่ — ค่าที่ cap ได้ถูกคืนกลับมาเป็นหลักฐาน ไม่ใช่ซ่อน",
        "stages": [
          {
            "label": "Trigger — กล่อง GPS/รีเลย์ยิง ping",
            "detail": "POST /api/internal/equipment/telemetry รับ { imei, vccVoltage, recordedAt? } จากฮาร์ดแวร์โดยตรง — ไม่มี session เพราะกล่องไม่ได้ล็อกอิน และยังไม่มีผู้ให้บริการกล่อง/SMS gateway รายใดถูกเลือก จึงใช้ shape กลางนี้แทนการเดา wire format ของ vendor",
            "evidence": "app/api/internal/equipment/telemetry/route.ts"
          },
          {
            "label": "Gate — bearer secret ของฮาร์ดแวร์",
            "detail": "header x-equipment-telemetry-secret ต้องตรงกับ EQUIPMENT_TELEMETRY_SECRET; ถ้ายังไม่ตั้งค่า ระบบตอบ 500 (ไม่ใช่ 503 แบบ bridge ธุรกิจ) ตามโค้ดจริง",
            "evidence": "app/api/internal/equipment/telemetry/route.ts · process.env.EQUIPMENT_TELEMETRY_SECRET",
            "refuses": "ไม่ตั้งค่า → 500 · ไม่ตรง → 401"
          },
          {
            "label": "Validate — IMEI ต้องเป็นเครื่องที่รู้จัก",
            "detail": "imei และ vccVoltage ต้องมาและมีชนิดถูกต้อง; ค้น equipment_assets ด้วย imei ถ้าไม่พบคือปฏิเสธ ไม่สร้างเครื่องใหม่ให้เอง",
            "evidence": "app/api/internal/equipment/telemetry/route.ts · equipment_assets lookup by imei",
            "refuses": "ข้อมูลไม่ครบ → 400 · ไม่พบ IMEI → 404"
          },
          {
            "label": "Decision — VCC ≥ 13.5V คือเครื่องติด",
            "detail": "engine_running = vccVoltage >= 13.5 โดยค่าคงที่ ENGINE_RUNNING_THRESHOLD_VCC อยู่ที่เดียว ไม่กระจายไปตามผู้ให้บริการกล่อง",
            "evidence": "app/api/internal/equipment/telemetry/route.ts · ENGINE_RUNNING_THRESHOLD_VCC"
          },
          {
            "label": "Evidence — เก็บ ping ดิบก่อนสะสม",
            "detail": "insert equipment_telemetry_pings (recorded_at, engine_running, vcc_voltage, raw_payload) — เก็บ payload ดิบไว้ตรวจย้อน",
            "evidence": "app/api/internal/equipment/telemetry/route.ts · equipment_telemetry_pings insert",
            "refuses": "เขียน ping ไม่ได้ → 500"
          },
          {
            "label": "Accumulate — ชั่วโมงเกิดจาก ping คู่",
            "detail": "accumulateEngineHours อ่าน ping ก่อนหน้า (strictly before) และบวกเฉพาะเมื่อทั้ง ping ก่อนและ ping ใหม่เป็น engine_running; delta ถูก cap ที่ MAX_GAP_HOURS = 0.5 ชม. เพราะ ping ปกติมาทุก 10–30 วินาที — ช่องว่างที่ใหญ่กว่านั้นคือสัญญาณขาด ไม่ใช่ชั่วโมงเดินเครื่องที่มองไม่เห็น และค่า gap จริงถูกคืนเป็น capped_from_hours",
            "evidence": "engines/equipment/engine-hours.ts · computeEngineHoursDelta / accumulateEngineHours",
            "refuses": "ping ย้อนหลัง/เวลาเสีย → delta 0 ไม่ลบชั่วโมง · accumulate ล้ม → 500 พร้อม telemetry_saved:true"
          },
          {
            "label": "Ack + สถานะเครื่อง",
            "detail": "ถ้า engine_running เป็นจริง อัปเดต current_status = RUNNING แล้วตอบ { ok, engine_running, delta_hours, capped_from_hours, new_total_hours } — ตัวเลขที่ตอบคือค่าที่เขียนจริง ไม่ใช่ค่าที่คำนวณลอย",
            "evidence": "app/api/internal/equipment/telemetry/route.ts · NextResponse.json"
          }
        ]
      },
      {
        "id": "resource-hours",
        "sectionId": "problem-logistics",
        "title": "ชั่วโมงเครื่อง → resource transaction (ราคาจาก config จริง)",
        "titleEn": "Machinery Hours",
        "summary": "ชั่วโมงที่สะสมได้แล้วถูกส่งเข้าเส้นเงินผ่าน bridge นี้เท่านั้น — ผู้ส่งให้มาแค่ชั่วโมงกับเวลา ส่วนเงินคำนวณจาก app_configs ที่เจ้าของตั้งไว้ ไม่มีเลข hardcode ในโค้ด",
        "stages": [
          {
            "label": "Trigger — telematics/อีเมลแจ้งชั่วโมงเดินเครื่อง",
            "detail": "POST /api/internal/connect/resource-transaction พร้อม source_event_id และเวลาจริงจากต้นทาง",
            "evidence": "app/api/internal/connect/resource-transaction/route.ts"
          },
          {
            "label": "Gate — secret ของ bridge",
            "detail": "header x-resource-transaction-internal-secret เทียบ RESOURCE_TRANSACTION_INTERNAL_SECRET",
            "evidence": "app/api/internal/connect/resource-transaction/route.ts",
            "refuses": "ไม่ตั้ง → 503 · ไม่ตรง → 401"
          },
          {
            "label": "Validate — ต้องอ้างของจริง",
            "detail": "job_id ต้องมีอยู่จริงใน jobs, duration_hours > 0, started_at ต้องไม่เกิน received_at และ evidence_ref ต้องมี",
            "evidence": "engines/canonical/resource-transaction-write.ts · recordResourceTransaction",
            "refuses": "RESOURCE_TRANSACTION_JOB_NOT_FOUND / INVALID_TIME / INVALID_DURATION"
          },
          {
            "label": "Authority — ราคาอ่านจาก config",
            "detail": "fuel-price และ machinery_rates (base_rate_per_hour, fuel_consumption_lph, markup_margin) อ่านจาก app_configs; ขาดหรือผิดรูปคือหยุด ไม่ถอยไปใช้เลขเดิมในโค้ด",
            "evidence": "engines/canonical/resource-transaction-write.ts · readConfigAuthorities",
            "refuses": "CONFIG_AUTHORITY_MISSING (zero rows written)"
          },
          {
            "label": "Idempotency — event เดียวสร้างแถวเดียว",
            "detail": "source_event_id เป็น unique; ซ้ำจะคืนแถวเดิมพร้อม duplicate: true และยังกัน race 23505 ได้",
            "evidence": "engines/canonical/resource-transaction-write.ts · idempotency check",
            "refuses": "ไม่ throw ซ้ำ — คืน duplicate: true แทนการเขียนทับ"
          },
          {
            "label": "Compute + Write + Audit",
            "detail": "unit_cost = base_rate_per_hour + fuel_consumption_lph × fuel_price; unit_price = unit_cost + markup; total = duration_hours × unit_cost แล้วเขียนพร้อม audit transaction.created",
            "evidence": "engines/canonical/resource-transaction-write.ts · recordResourceTransaction"
          }
        ]
      },
      {
        "id": "price-authority",
        "sectionId": "problem-logistics",
        "title": "ข้อเสนอผู้ขาย ≠ ราคากลาง",
        "titleEn": "Price Authority",
        "summary": "แยกสองคำสั่งที่โค้ดเดิมปนกัน: การบันทึกข้อเสนอ (หลักฐาน) กับการแก้ราคากลาง (อำนาจ)",
        "stages": [
          {
            "label": "Trigger — มีข้อความตอบราคาเข้ามา",
            "detail": "ช่องทางใดก็ได้ส่งข้อความตอบกลับ; ข้อความนั้นเป็นหลักฐาน ไม่ใช่สิทธิ์แก้ราคา",
            "evidence": "engines/canonical/material-price-write.ts · recordSellerOffer"
          },
          {
            "label": "Evidence — บันทึกข้อเสนอพร้อม audit ใน transaction เดียว",
            "detail": "record_seller_offer_atomic เขียนแถวคำขอ + audit พร้อมกัน และคืน price_changed: false เสมอ",
            "evidence": "engines/canonical/material-price-write.ts · recordSellerOfferAtomic",
            "refuses": "PRICE_REQUEST_NOT_FOUND / PRICE_REQUEST_NOT_PENDING / PRICE_INVALID"
          },
          {
            "label": "Authority — ใครมีสิทธิ์เปลี่ยนราคากลาง",
            "detail": "applyMaterialPrice ใช้ resolveAuthority(action 'price.write'): เจ้าของหรือ delegation ที่ระบุชัดเท่านั้น — ADMIN_TIER อย่างเดียวหรือชื่อในแชตไม่พอ",
            "evidence": "core/authority/resolve.ts · resolveAuthority",
            "refuses": "PRICE_FORBIDDEN:<reason>"
          },
          {
            "label": "Atomic commit — ราคาและ audit ไปด้วยกัน",
            "detail": "apply_material_price_atomic เขียนราคา + audit ใน transaction เดียว และไม่มีเส้น 'เขียนแล้วค่อยย้อน' เพราะ compensation รับประกันไม่ได้",
            "evidence": "engines/canonical/material-price-write.ts · applyMaterialPriceAtomic",
            "refuses": "RPC ยังไม่ apply → PRICE_ATOMIC_RPC_MISSING (ไม่ลดชั้นเป็นเขียนครึ่งเดียว)"
          }
        ]
      },
      {
        "id": "agent-authority",
        "sectionId": "agents",
        "title": "Agent เรียกของจริงได้แค่ประตูที่เปิด",
        "titleEn": "Agent Authority",
        "summary": "ไม่มีเอเจนต์ตัวไหนถือสิทธิ์ของตัวเอง: ทุกคำขอผ่าน session, whitelist, ช่องทางที่เปิด และหลักฐานยืนยันเจ้าของ",
        "stages": [
          {
            "label": "Trigger — agent/MCP client เรียกคำสั่ง",
            "detail": "คำขอเข้ามาที่ route ของ MCP/dispatch ตามชนิดงาน (อ่านข้อมูล / วิเคราะห์ / รับงาน / อนุมัติ)",
            "evidence": "app/api/v1/mcp/business/dispatch/route.ts"
          },
          {
            "label": "Gate — session แอดมิน",
            "detail": "business.dispatch ใช้ requireAuth(ADMIN_TIER) เป็นประตูเดียวกับ route แอดมินอื่น",
            "evidence": "app/api/v1/mcp/business/dispatch/route.ts · requireAuth(ADMIN_TIER)",
            "refuses": "ไม่มี session/ชั้นไม่ถึง → ปฏิเสธก่อนอ่านข้อมูล"
          },
          {
            "label": "Gate — whitelist คำสั่งอ่าน",
            "detail": "รับเฉพาะคำสั่งอ่านที่อยู่ในรายการ; คำสั่งอื่นไม่ถูกส่งต่อให้ dispatcher",
            "evidence": "app/api/v1/mcp/business/dispatch/route.ts · COMMAND_NOT_PERMITTED",
            "refuses": "403 COMMAND_NOT_PERMITTED"
          },
          {
            "label": "Gate — Execution Channel",
            "detail": "channelIsEnabled('execute') ยังเป็น false ระบบจึงรายงานความสามารถตรง ๆ ว่า NOT_IMPLEMENTED แทนการจำลองสำเร็จ",
            "evidence": "app/api/admin/workspace/ai-agents/route.ts · engines/integration-gateway/policy/gate.ts",
            "refuses": "execute_capability.status = 'NOT_IMPLEMENTED'"
          },
          {
            "label": "Gate — เจ้าของต้องยืนยันเอง",
            "detail": "การอนุมัติที่แตะ production ต้องแนบ owner_verification_artifact; session แอดมินอย่างเดียวไม่พอ",
            "evidence": "app/api/gateway/proposals/[id]/decide/route.ts",
            "refuses": "403 OWNER_VERIFICATION_REQUIRED"
          },
          {
            "label": "Handoff — ส่งสัญญางาน ไม่ใช่สิทธิ์",
            "detail": "contractor ได้ JobContract + WorkerRequest พร้อม permittedReadScope/permittedWriteScope และ approvalPolicy; runtime ยัง PAUSED ตาม TASK-082",
            "evidence": "app/api/v1/mcp/contractor/task-claim/route.ts · registry/TASK_QUEUE.json",
            "refuses": "intake ปิด → ไม่จ่ายงานใหม่"
          }
        ]
      }
    ]
  }
}