Plan — WorkflowService Integration
แผน implement การต่อ WorkflowService เข้ากับ orchestrator
อัปเดต: 2026-06-15
For agentic workers: REQUIRED SUB-SKILL: Use superpowers
(recommended) or superpowers to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking. ⚠️ Contract status: Workflow↔Orchestrator contract ยังไม่ lock — ส่วน A (Phase-0 coordination) ต้องเสร็จก่อนส่วน B
Goal: ให้ WorkflowService emit semantic integration events (Pickup/SentToReviewer/Decision) ผ่าน Azure Service Bus ให้ Orchestrator saga กิน + รับ HTTP create/resubmit/force-stop ที่คืน WfInstanceId และ idempotent + notify maker เอง — โดยไม่เปลี่ยน phase-model internals ของ Workflow
Architecture: Workflow ภายในเป็น multi-phase / multi-step state machine (WfInstance → WfPhaseInstance → WfStepInstance → WfStepActionInstance) ที่มี action แค่ APPROVE/REJECT + phase progression + OnRejectToPhaseOrder. งาน integration = เพิ่ม ASB publisher layer บาง ๆ ที่ แปลง native transition → semantic event (native→semantic mapping) แล้ว publish; ตัว phase engine (TakeActionHandler) ไม่แตะ logic เดิม แค่เพิ่ม emit point. WebhookService.SendWorkflowEventAsync ปัจจุบัน เป็น no-op (body ว่าง) — เป็นจุดที่ ASB layer จะเสียบแทน/ขนานกัน
Tech Stack: .NET, EF Core, Azure Service Bus (Azure.Messaging.ServiceBus), xUnit + Moq (test project WorkFlow05.UnitTest มีอยู่แล้ว)
สรุป code จริงที่สำรวจ (survey 2026-06-15 — อย่าเดา)
| สิ่ง | สถานะจริงในโค้ด | ไฟล์ |
|---|---|---|
| ASB / messaging infra | ไม่มีเลย (0) — มีแค่ secret-provider.yaml อ้าง | — |
WebhookService.SendWorkflowEventAsync | no-op — body ว่างเปล่า (บรรทัด 24-26) | WorkFlow02.Application/Services/WebhookService.cs |
| Native actions | APPROVE / REJECT เท่านั้น | TakeActionHandler.cs:116,133,173 |
| Rework | REJECT + templateStep.OnRejectToPhaseOrder > 0 → set instance.CurrentPhaseId=target, IsEditable=true (ไม่ reset step ตอนนี้) | TakeActionHandler.cs:148-167 |
| Reject ถาวร | REJECT + OnRejectToPhaseOrder == 0 → instance.Status="REJECTED" | TakeActionHandler.cs:149-153 |
| Approve final phase | APPROVE phase สุดท้าย → instance.Status="COMPLETED" | TakeActionHandler.cs:284-286 |
| SentToReviewer | ไม่มี action แยก = APPROVE phase ที่ไม่ใช่ phase สุดท้าย → เปิด next phase | TakeActionHandler.cs:230-281 |
| Pickup / claim / InReview | ไม่มี concept ในโค้ดเลย — step ถูก assign ตอน create; ไม่มีกลไก “หยิบงาน” | (ไม่พบ) |
| Cancel | ไม่มีใน TakeAction; มีแค่ ForceStopStep (admin force-stop) | ForceStopStepHandler.cs |
PostInstances คืนอะไร | PostInstancesResponse.DocumentId (string) + payload — ไม่คืน WfInstanceId | PostInstancesResponse.cs:8 |
WfInstance.InstanceId | int identity (นี่คือ WfInstanceId ที่ orchestrator ต้องเก็บ) | WfInstance.cs:17 |
ForceStopStep idempotent? | ไม่ — throw "not found: No active step" ถ้าไม่มี IN_PROGRESS step (บรรทัด 38-40) | ForceStopStepHandler.cs |
| History | AddHistoryAsync มี แต่ ไม่ถูกเรียกที่ไหนเลย (dead) | TakeActionHandler.cs:370 |
occurredAt / decisionId | ไม่มีในโค้ด — ต้อง mint ใหม่ทั้งคู่ | — |
| Webhook payload | OnActionChangedPayload { InstanceId, BusinessRefId, BusinessType, StepInstanceId, ActionBy, ActionType, Remark, IsEdit, NextAssignees } | WebhookService.cs:29 |
| Test pattern | xUnit + Moq, mock ITemplateService/IWfInstanceRepository/IWebhookService; builder BuildSingleStepTemplate/BuildTwoPhaseTemplate | tests/WorkFlow05.UnitTest/Handlers/TakeActionHandlerTests.cs |
| ASB publisher reference (อีก service, same SuperApp pattern) | ServiceBusClient.CreateSender(topic), fire-and-forget never-throw, MessageId=eventId, camelCase + JsonStringEnumConverter, + Null fallback | Backend_UserService/.../Messaging/AsbFlowApprovalEventPublisher.cs |
นัยสำคัญ: native→semantic mapping ทั้งหมด ตีความได้จากโค้ดจริง ยกเว้น Pickup/InReview ที่ “ไม่มีที่มา” และ Cancel ที่ “ต้องเพิ่ม path ใหม่” — สองอย่างนี้คือแกนของ Part A
Part A — Phase-0 Coordination (ต้องเสร็จก่อนเริ่ม Part B)
เป้าหมาย: lock 5 จุดสัญญา กับทีม Workflow (ทีมเดียวกัน ยืนยันแล้ว แต่ design ภายในยังไม่นิ่ง) ก่อนเขียน code ที่ผูกกับ payload/ความหมาย. แต่ละ item มี context จากโค้ดจริง + ตัวเลือก + default ที่แนะนำ + กระทบใคร
A.1 — Pickup / claim มีไหม → ตัดสิน InReview (🔴 สำคัญสุด — กระทบ status list ฝั่ง Lego)
- คำถาม: Workflow มีกลไก “maker หยิบงานจากกองกลาง” (claim/pickup) ไหม
- Context (โค้ดจริง):
PostInstancesHandlerassign step ตอน create เลย (step.Assigneesหรือrequest.Steps);TakeActionHandlerแค่เช็คaction.UserId==request.UserId || action.Role==request.Role— ไม่มี state “หยิบแล้ว/ยังไม่หยิบ”; ไม่มี endpoint claim - ตัวเลือก:
- (a) ไม่มี pickup (โค้ดปัจจุบัน):
InReviewไม่มี trigger → ตัดInReviewทิ้ง หรือ map “phase แรก IN_PROGRESS (เข้าคิว)” = UnderReview ตั้งแต่ create - (b) เพิ่ม pickup: ต้องเพิ่ม endpoint + state ใหม่ใน Workflow (นอก scope grill — งานใหญ่)
- (a) ไม่มี pickup (โค้ดปัจจุบัน):
- แนะนำ: (a) — ตัด
InReview/ รวมเป็น UnderReview ตอน create; การเพิ่ม pickup = scope ใหม่ ไม่อยู่ใน integration seam - กระทบ: ถ้าตัด
InReview→ Lego ตัด statusInReview+ saga ตัด stateUnderReview-from-pickup (เหลือ entry ที่ create) + M3 (Pickup event) หายไปทั้ง message → แจ้ง dev Lego (01) + dev Orchestrator (02) ทันทีเมื่อ decide - บล็อก: B.2 (publisher event set), B.6 (TakeAction emit point)
A.2 — แปลง native (APPROVE/REJECT/OnRejectToPhaseOrder) → Decision vocab ที่ไหน
-
คำถาม: publisher ฝั่ง Workflow เป็นคนแปลง native→semantic ใช่ไหม (ไม่ใช่ orchestrator)
-
Context (โค้ดจริง): ตอนจบ
TakeActionHandlerรู้ครบแล้วว่า: เป็น APPROVE/REJECT, phase ไหน,instance.Statusกลายเป็น COMPLETED/REJECTED/RUNNING,templateStep.OnRejectToPhaseOrder,instance.CurrentPhaseId,IsEditable. ข้อมูลพอแปลง native→semantic ครบ -
mapping ที่จะ encode (จากโค้ด):
semantic event เงื่อนไข native (หลัง transition) SentToReviewer ActionType==APPROVE&&stepFullyApproved&& มีnextPhase(ยังไม่ COMPLETED)Decision(Approve) ActionType==APPROVE&&instance.Status=="COMPLETED"Decision(Reject) ActionType==REJECT&&templateStep.OnRejectToPhaseOrder==0(instance.Status=="REJECTED")Decision(Rework) ActionType==REJECT&&templateStep.OnRejectToPhaseOrder>0(IsEditable==true)Pickup ขึ้นกับ A.1 Decision(Cancel) ขึ้นกับ A.5 (ไม่มีใน TakeAction) -
แนะนำ: แปลงที่ publisher ฝั่ง Workflow (Workflow รู้ phase semantic ของตัวเอง) — สอดคล้อง 00 §7.2
-
บล็อก: B.2, B.6
A.3 — decisionId + occurredAt — ใคร/ตอนไหน mint
- คำถาม: Workflow เป็นคน mint
decisionId(Guid) +occurredAt(UTC) ต่อ Decision event ใช่ไหม + format อะไร - Context (โค้ดจริง): ไม่มีทั้งคู่ในโค้ด. Lego ordering ใช้
occurredAt(00 §3 progress guard); orchestrator ใช้decisionIdเป็น idempotency key ของ Decision (00 §7.2) - แนะนำ: Workflow mint ทั้งคู่ใน publisher ตอนจะ publish:
decisionId = Guid.NewGuid(),occurredAt = DateTime.UtcNow(เก็บ/อิงจังหวะ commit transition); ทุก semantic event มีoccurredAt, เฉพาะ Decision มีdecisionId - เปิดประเด็น: ถ้าต้อง replay/republish event เดิม →
decisionIdต้อง stable (mint ตอน transition แล้ว persist) ไม่ใช่ mint ตอน publish ทุกครั้ง → ถาม: ต้อง persistdecisionIdบนWfInstance/history ไหม (กระทบ B.1 ว่ามี migration หรือไม่) - บล็อก: B.2 (event schema), B.6
A.4 — PostInstances ต้องคืน WfInstanceId (M12)
- คำถาม: เปลี่ยน
PostInstancesResponseให้คืนWfInstanceId(=WfInstance.InstanceId, int) ได้ไหม — มี consumer เดิมของ fieldDocumentIdที่จะพังไหม - Context (โค้ดจริง):
PostInstancesResponseคืนแค่DocumentId(string) +PayloadForCallback.instance.InstanceIdถูก set หลังCommit()(identity). Orchestrator (M12) ต้องเก็บWfInstanceIdใน Correlations เพื่อเรียก resubmit (M13) / force-stop (M14) - แนะนำ: เพิ่ม field
WfInstanceId(ไม่ลบDocumentId— additive, ไม่ทำ caller เดิมพัง). ดู B.3 - เปิดประเด็น: orchestrator เก็บ correlation ด้วย
WfInstanceId(int) หรือrefNo/DocumentId(string) เป็น key — กระทบว่า M13/M14 รับ param อะไร - บล็อก: B.3
A.5 — Cancel path (Workflow-initiated, M5 Decision(Cancel) + sweep)
- คำถาม: Cancel ฝั่ง Workflow ใช้กลไกไหน — reuse
ForceStopStep(มีอยู่) แล้ว publishDecision(Cancel)? หรือเพิ่ม cancel path ใหม่? - Context (โค้ดจริง):
ForceStopStepHandlerปิด instance ทั้งหมด →Status="FORCE_STOPPED". ใกล้เคียง Cancel ที่สุด แต่: (1) ต้องการ user assignee อยู่ใน step ปัจจุบัน (บรรทัด 43-46) — admin sweep อาจไม่ผ่าน; (2) ไม่ idempotent (throw ถ้าไม่มี active step). 00 §4.3 ขา B = “Workflow cancel local + publish Decision(Cancel)/workflow-cancel” - ตัวเลือก:
- (a) Cancel =
ForceStopStep+ publishDecision(Cancel, mode)— reuse, แต่ต้องแก้ idempotent + ผ่อน auth สำหรับ sweep - (b) เพิ่ม cancel path แยก
- (a) Cancel =
- แนะนำ: (a) ตามทิศ 00 §4.3 + แก้ idempotent (B.5) — แต่ ยืนยันกับทีมก่อน ว่า FORCE_STOPPED semantics = Cancel
- บล็อก: B.4 (Cancel publish), B.5 (idempotent force-stop), B.7 (sweep)
A.6 — payload field-level M3/M4/M5 (ปิดท้ายเมื่อ A.1-A.5 ตกผลึก)
- ยืนยัน field สุดท้าย ของแต่ละ event ให้ตรง 00 §7.1 + ฝั่ง orchestrator consumer (02):
- SentToReviewer (M4):
eventId, flowInstanceId(=refNo? หรือ WfInstanceId?), refNo, reviewerIdentity(=next assignees), occurredAt - Decision (M5):
eventId, flowInstanceId, refNo, action(Approve/Reject/Rework/Cancel), decisionId, reworkTargets, reason, occurredAt - (Pickup M3): ตาม A.1
- SentToReviewer (M4):
- ยืนยัน key correlation: orchestrator match saga ด้วย
refNoหรือWfInstanceId— ฝั่ง WorkflowBusinessRefId= document id (= refNo?). ต้องตรงกับ 01/02 ว่า field ชื่ออะไร map กับ LegoflowInstanceId - บล็อก: B.2 (schema final)
A.7 — reviewer identity = assignee ของ next phase (ยืนยัน model)
- คำถาม:
reviewerIdentityใน SentToReviewer =NextAssignees(assignee ของ step IN_PROGRESS ใน next phase) ใช่ไหม - Context (โค้ดจริง):
TakeActionHandler:336-343คำนวณNextAssigneesจากinstance.WfStepInstanceItems.FirstOrDefault(s => s.Status=="IN_PROGRESS")อยู่แล้ว (เฉพาะ non-REJECT) → reuse ได้ - แนะนำ: reuse logic เดิม
- บล็อก: B.6
A.8 — Notify maker: audience + channel (W2)
- คำถาม: Workflow เป็นเจ้าของ notify maker (grill D5). ยิงผ่านอะไร — เรียก NotificationService (HTTP/ASB) ที่มีอยู่? maker pool resolve จากไหน (role-based assignee)?
- Context (โค้ดจริง): ไม่มี notification publisher ใน WorkflowService เลย; assignee เก็บใน
WfStepActionInstance { UserId, Role } - 2 จังหวะ (grill): (1) create → notify maker pool (unassigned/role); (2) resubmit → notify assignee เดิม
- บล็อก: B.8 — เขียน task ได้แต่ payload/channel ขึ้นกับคำตอบ
Part A — Definition of Done
- A.1-A.8 มีคำตอบเขียนเป็นลายลักษณ์ (update ไฟล์ 03-workflowservice.md “Open items” → resolved)
- update 00-OVERVIEW §7.1 (M3-M5, M12) ถ้า field/topic เปลี่ยน + แจ้ง dev 01 (Lego) ถ้า
InReviewถูกตัด - ระบุ topic name จริงของแต่ละ semantic event (orchestrator consumer ต้องรู้)
Part B — Implementation (provisional — เริ่มได้หลัง Part A lock ตามที่ระบุ)
งานที่ ไม่ขึ้นกับคำตอบ Part A ทำได้เลย (B.1 infra, B.3 คืน WfInstanceId, B.5 idempotent force-stop). งานที่ ขึ้นกับ Part A flag ไว้ “blocked on A.N” — เขียน skeleton/test ได้แต่ assert ตาม contract สุดท้าย
B.1 — ASB publisher infra (sender + DI + Null fallback) [ทำได้เลย — ไม่ blocked]
อิงแบบ AsbFlowApprovalEventPublisher (UserService) 1
ServiceBusClient.CreateSender(topic), fire-and-forget never-throw, MessageId=eventId, camelCase + JsonStringEnumConverter, Null fallback เมื่อไม่มี ASB (local dev)
- B.1.1 เพิ่ม PackageReference
Azure.Messaging.ServiceBusในWorkFlow02.Application(หรือ Infrastructure ตาม layer ที่ทีมใช้ — DI อยู่WorkFlow01.API/ContainerModules)- verify: restore ผ่าน, solution build เขียว
- B.1.2 นิยาม port
IWorkflowEventPublisher(1 method ต่อ semantic event — เพิ่มตาม B.2): วาง interface ใน Application/Interfaces (คู่กับIWebhookService) - B.1.3
AsbWorkflowEventPublisher : IWorkflowEventPublisher, IAsyncDisposable— copy โครง publisher อ้างอิง (try/catch + LogWarning, ไม่ throw); 1 sender ต่อ topic (หรือ sender map ต่อ event type) - B.1.4
NullWorkflowEventPublisher— log +Task.CompletedTask(fallback) - B.1.5 DI: register
ServiceBusClient(ถ้ายังไม่มี) + bind Asb vs Null ตาม connection string present (ตาม pattern UserService DI)- verify (test): unit test ที่ inject mock
IWorkflowEventPublisherแล้วยืนยันถูกเรียก (ไม่ต้องต่อ ASB จริง)
- verify (test): unit test ที่ inject mock
- TDD: เขียน test ก่อน —
AsbWorkflowEventPublisherserialize event เป็น camelCase JSON + setMessageId= eventId (mockServiceBusSenderหรือ assert ผ่าน abstraction); never-throw เมื่อ sender ขว้าง exception
B.2 — Event contracts (DTO ของ semantic events) [blocked on A.1, A.2, A.3, A.6]
- B.2.1 นิยาม event record/class ตาม schema สุดท้ายจาก A.6:
SentToReviewerEvent { EventId, FlowInstanceId/RefNo, ReviewerIdentity, OccurredAt }WorkflowDecisionEvent { EventId, FlowInstanceId/RefNo, Action(enum Approve/Reject/Rework/Cancel), DecisionId, ReworkTargets, Reason, OccurredAt }PickupEvent { ... }— เฉพาะถ้า A.1 = มี pickup; ถ้าตัด → ข้าม
- B.2.2 เพิ่ม method ใน
IWorkflowEventPublisherต่อ event (PublishSentToReviewerAsync,PublishDecisionAsync, [PublishPickupAsync])- DO NOT เดา field — รอ A.6; เขียน record หลัง lock เท่านั้น
- TDD: serialize round-trip ของแต่ละ event ตรง wire vocab (
APPROVE/REJECT/REWORK/CANCELหรือ enum string ที่ตกลง — ดู 00 §7.2)
B.3 — PostInstances คืน WfInstanceId (M12) [ทำได้เลย — blocked เฉพาะ field-name จาก A.4]
- B.3.1 (test-first) เพิ่ม test ใน
PostInstancesHandlerTests: หลัง handle สำเร็จ →response.WfInstanceId == instance.InstanceId(> 0) - B.3.2 เพิ่ม
public int WfInstanceId { get; set; }ในPostInstancesResponse(additive — คงDocumentIdไว้) - B.3.3 set
WfInstanceId = instance.InstanceIdในPostInstancesHandler(หลังCommit()— บรรทัด ~134, instance.InstanceId พร้อมแล้ว) ทั้ง 2 return path (UseHook + non-hook)- verify:
PostInstancesHandlerTestsเดิมยังเขียว + test ใหม่ผ่าน
- verify:
B.4 — Emit Decision/SentToReviewer จาก TakeAction [blocked on A.1, A.2, A.3, A.7]
- B.4.1 inject
IWorkflowEventPublisherเข้าTakeActionHandler(ctor — ปัจจุบันมี 3 dep) - B.4.2 หลัง
Commit()(บรรทัด 313) เพิ่ม emit point ตาม mapping A.2:- APPROVE non-final + phaseComplete + มี nextPhase →
PublishSentToReviewer(reviewerIdentity = NextAssignees logic, A.7) - APPROVE →
instance.Status=="COMPLETED"→PublishDecision(Approve) - REJECT +
OnRejectToPhaseOrder==0→PublishDecision(Reject) - REJECT +
OnRejectToPhaseOrder>0→PublishDecision(Rework, reworkTargets, reason=Remark) - ห้าม แตะ phase-progression logic เดิม — เพิ่มเฉพาะ emit หลัง state ตกผลึก
- APPROVE non-final + phaseComplete + มี nextPhase →
- B.4.3
reworkTargetsmap จาก target phase/step ที่ตีกลับ (instance.CurrentPhaseIdหลัง rework = target phase order) — รอ A.6 ยืนยัน shape - TDD: ต่อยอด
TakeActionHandlerTests(builder มีแล้ว) — mock publisher, assert ถูกเรียกด้วย action ที่ map ถูกต้อง ต่อแต่ละ scenario (single-step approve=Decision(Approve); two-phase approve phase1=SentToReviewer; reject onReject=0=Reject; onReject>0=Rework) - flag: ถ้า A.1 = ไม่มี pickup → ไม่มี Pickup emit ที่ create; UnderReview เกิดตอน create (อาจต้อง emit จาก
PostInstancesแทน — ดู A.1 outcome)
B.5 — ForceStopStep idempotent + ทน “ยังไม่ถูกสร้าง” (M14) [ทำได้เลย]
- B.5.1 (test-first) เพิ่ม test ใน
ForceStopStepHandlerTests:- force-stop instance ที่ ไม่มี IN_PROGRESS step (เช่น COMPLETED/REJECTED/FORCE_STOPPED แล้ว) → คืน Ok (no-op) ไม่ throw
- force-stop ซ้ำ 2 ครั้ง → ครั้งที่ 2 no-op
- B.5.2 แก้
ForceStopStepHandler: เปลี่ยน throw"not found: No active step"(บรรทัด 38-40) → ถ้าไม่มี active step และ instance terminal แล้ว → return Ok (idempotent no-op); ถ้า instance ไม่พบ → ยังคง behavior เดิม (ดู A.5 ว่า treat ยังไงตอน sweep)- flag: การผ่อน auth check (บรรทัด 43-46) สำหรับ admin sweep = blocked on A.5 (sweep actor ไม่ใช่ step assignee)
- verify: test เดิม (happy force-stop) ยังเขียว + idempotent test ผ่าน
B.6 — Cancel publish จาก force-stop [blocked on A.5]
- B.6.1 เมื่อ A.5 = (a): หลัง force-stop สำเร็จ →
PublishDecision(Cancel, mode)ผ่านIWorkflowEventPublisher - B.6.2 mode (Manual/Expiry) มาจาก caller (sweep ส่ง Expiry, admin ส่ง Manual) — รอ A.5/A.6 field
- TDD: assert publisher ถูกเรียกด้วย
Decision(Cancel)หลัง force-stop
B.7 — Background sweep หมดอายุ → Cancel (W5) [blocked on A.5]
- B.7.1
BackgroundService(IHostedService) scan instance ที่ expired (เกณฑ์หมดอายุ = ? — ต้องนิยามกับทีม) → cancel local (reuse force-stop path) + publishDecision(Cancel, mode=Expiry) - B.7.2 ต้อง idempotent (พึ่ง B.5) + ทน double-sweep กับขา Lego (00 §6)
- flag: เกณฑ์ expiry + ว่ามีอยู่จริงในขอบเขต sprint นี้ไหม = ยืนยันกับทีม (อาจ defer)
B.8 — Notify maker เอง (W2) [blocked on A.8]
- B.8.1 ตอน
PostInstances(create) → notify maker pool (role/unassigned audience) - B.8.2 ตอน
Resubmit→ notify assignee เดิม (current step assignees) - B.8.3 channel = ตาม A.8 (HTTP NotificationService / ASB) — อย่าเขียน code จนกว่า A.8 ตอบ (ไม่มี notification infra ใน Workflow ตอนนี้)
- TDD: mock notify port, assert audience ถูกต้องต่อ create vs resubmit
B.9 — Wire publisher แทน/ขนาน webhook no-op [blocked on B.2]
- B.9.1 ตัดสินใจกับทีม:
WebhookService.SendWorkflowEventAsync(no-op) ถูกแทนด้วย ASB publisher หรือคงไว้ขนานกัน — ปัจจุบัน no-op จึงไม่มี behavior ให้รักษา - B.9.2 ตรวจ emit points ครบ: create (B.8), TakeAction (B.4), force-stop (B.6) — ไม่มี semantic transition ที่ตกหล่น
Part B — Definition of Done
-
dotnet buildทั้ง solution เขียว -
WorkFlow05.UnitTestเขียวทั้งหมด (รวม test ใหม่ B.1/B.3/B.4/B.5) - publisher emit semantic event ครบทุก transition ที่ contract กำหนด (ตาม A ที่ lock)
-
PostInstancesคืนWfInstanceId;ForceStopStepidempotent - ไม่มี migration ใหม่ เว้นแต่ A.3 ตัดสินให้ persist
decisionId(แล้วต้องทำ migration บน MIGRATION branch — ดู MEMORY: MIGRATION branch workflow)
ลำดับแนะนำ
- Part A ทั้งหมด (coordination) — โดยเฉพาะ A.1 (Pickup/InReview) ก่อน เพราะกระทบ Lego/Orchestrator
- B.1, B.3, B.5 (ไม่ blocked) — ทำขนานระหว่างรอ A
- B.2 → B.4 → B.6 → B.7 → B.8 → B.9 (ตามลำดับ dependency หลัง A lock)