CH3-002 · מכונת מצבים דטרמיניסטית ל-Trade
גרסה 1.0 · נעילה 7.8.2026 · P0 · Approved for Implementation
הכרעת Architect: Locked & Approved — גרסה 1.0 (Approved for Implementation).
גרסה 0.31 סגרה את כל החסמים המהותיים. הסקירה הארכיטקטונית הושלמה והמפרט מאושר ליציאה לביצוע. הכרעת Architect: השינויים בסכמת Trade וב-SystemSettings מאושרים לביצוע. אין צורך בגרסה 0.32 או בסבב סקירה נוסף; כל שינוי עתידי במפרט ייעשה בגרסה חדשה וב-ADR. המימוש טרם בוצע — CH3-002 ייחשב Completed רק לאחר מימוש, בדיקות הקבלה והגשת Ch3ExecutionReport002.
| הערה | נושא | תיקון |
|---|---|---|
| #1 (0.3) | סתירה סביב wait | הוסר pending→wait. wait = Trade שכבר אושר אך חסום זמנית; נכנס רק מ-approved. המסלול: pending→approved→wait→approved→executed. wait→approved = החזרת אישור קיים, לא אישור חדש. |
| #2 (0.3) | Idempotency מוגדר הפוך | Retry משתמש באותו מפתח ומחזיר אותה תוצאה (ללא מעבר/Audit). פעולה חדשה/אישור מחדש/Trade חדש → מפתח חדש. transition_id ≠ idempotency_key — שני שדות נפרדים. |
| #3 (0.3) | expected_from אינו שדה Trade | הוסר מ-Trade; נותר פרמטר קריאה (expectedFrom + expectedVersion). ב-Trade נשמרים רק status, status_version ושדות התפוגה. |
| #4 (0.3) | מנגנון התאוששות לא מספיק | נדרש Transaction אטומי של Trade+Message או Transition Journal/Outbox. "סריקה מתקנת" בלבד אינה עומדת בדרישת Message כמקור אמת. |
| #5 (0.3) | null→pending לא ממומש דרך ה-API | פעולת יצירה ייעודית createTradeWithAudit יוצרת יחד Trade+status_version+Audit ראשון+transition_id+idempotency_key. |
| #6 (0.3) | approved→executed רחב מדי | מותר רק לאחר אישור סופי מ-CH3-003. שליחה/Timeout/Partial Fill אינם executed. RECONCILIATION_REQUIRED אינו משנה status. approved נשאר בתוקף אחרי שליחה. |
| #7 (0.3) | גבול TTL אינו מדויק | now < approval_expires_at → תקף; now >= → פג. הכל ב-UTC. pending משתמש ב-Proposal TTL נפרד. |
| #8 (0.3) | חסר קישור expired→חדש | אישור מחדש כותב Message ייעוד: expired_trade_id, new_trade_id, סיבה, מזמים. |
| #9 (0.3) | Actor/Reason חייבים קנוניזציה | Actor נגזר מהקשר מאומת/שירות מורשה; טבלת הרשאות Actor לכל מעבר; reason_code קנוני; טקסט חופשי כהסבר נוסף בלבד. |
| #10 (0.31) | חסימה לאחר שליחת פקודה | כאשר execution_state != NOT_SENT — Trade Validity/Risk Gate/Global Freeze אינן רשאיות לשנות Trade.status. רק חוזה הסיום מ-CH3-003 רשאי למעבר. |
| #11 (0.31) | Idempotency מקור אמת ב-Outbox | מקור האמת למפתחות = Outbox/Journal (תוצאה מקורית שאינה נדרסת). ב-Trade נשמר Cache בלבד: last_transition_id, last_idempotency_key. |
| #12 (0.31) | proposal_expires_at חסר בסכמה | נוסף ל-Trade: proposal_expires_at. נוסף ל-SystemSettings: proposal_ttl_hours. אתחול ב-createTradeWithAudit, Migration ל-pending קיימות, בדיקת קבלה now >= proposal_expires_at. |
| #13 (0.31) | סתירות Actor approved↔wait | pending→approved רק Approval/משתמש. wait→approved ו-approved→wait — EXECUTION_GATE (Actor קנוני אחד). §14 תוקן; נוספו טבלת הרשאויות Actor ורשימת reason_code. |
היום Trade.status הוא enum שטוח [pending, approved, executed, rejected]. המעברים מפוזרים ומרומזים בלבד — ב-runAgentScan, executeTrade, refreshPortfolioPrices וב-UI הידני. אין מצבים ל"ממתין לתנאי", "פג תוקף", "הועבר לבדיקה". תוצאה: עסקאות pending ישנות שנותרות תלויות, TTL אישור אינו מיוצג כמצב, ואין מקור אמת אחד למעברים החוקיים.
מכונת מצבים אחת, דטרמיניסטית, אטומית, אידמפוטנטית וניתנת לשחזור מלא, שכל שינוי סטטוס במערכת חייב לעבור דרכה. פונקציית מעבר אחת, טבלת מעברים אחת, רשומת Audit אחת לכל מעבר (כולל יצירה ואישור מחדש).
| מישור | היקף | ערכים | שמור? |
|---|---|---|---|
| מצב מחזור ה-Orchestrator | פנימי ל-backend בלבד — לא נשמר ב-DB | PRICE_REFRESHED → VALIDITY_CHECKED → CIO_SCANNED → RISK_GATED → APPROVAL_SET → EXECUTION_DONE | לא |
| הסטטוס העסקי של ה-Trade | Trade.status — גלוי למשתמש ולכל הממשקים | null → pending → approved → wait → approved → executed (או rejected/expired/escalated) | כן |
| מצב פקודת הביצוע מול הברוקר | Trade.execution_state — שדה נפרד (תחום CH3-003) | NOT_SENT → SENT → CONFIRMED / FAILED / RECONCILIATION_REQUIRED | כן |
CH3-002 נוגע למישור 2 (Trade.status) בלבד. מישור 3 (execution_state) שייך ל-CH3-003 — מצב "ממתין למילוי" לאחר שליחת פקודה אינו מיוצג ב-wait.
| סטטוס | סוג | תיאור | טרמינלי? |
|---|---|---|---|
| pending | קיים | הצעה בהמתנה לאישור | לא |
| approved | קיים | מאושר לביצוע. נשאר בתוקף גם לאחר שליחת פקודה — execution_state מנהל את הפקודה עצמה. | לא |
| executed | קיים | מילוי סופי מאושר מ-CH3-003 בלבד (לא רק שליחה / Timeout / Partial Fill אוטומטי). | כן |
| rejected | קיים | החלטה שלילית מפורשת — ידנית או דטרמיניסטית של Risk Gate. אינו לתפוגה/סטיית מחיר/שוק סגור/נתונים ישנים/תקלה טכנית. | כן |
| wait | חדש | Trade שכבר אושר, אך ביצועו חסום זמנית (שוק סגור / Freeze / תנאי כניסה טרם התקיים). נכנס רק מ-approved; execution_state=NOT_SENT; לאחר שליחה → CH3-003. | לא |
| expired | חדש | תוקף פג / סטיית מחיר חריגה / נתוני שוק ישנים | כן |
| escalated_for_review | חדש | הועבר לבדיקה אנושית (מניה מוגנת / סיכון / סכסוך החלטה) | לא |
| מ- | אל | שחקן מפעיל | טריגר |
|---|---|---|---|
| null | pendingיצירה | CREATION (ייעודי) | יצירת הצעה חדשה — Trade + status_version ראשוני + Audit ראשון נוצרים יחד |
| pending | approved | APPROVAL / ידני | אישור עסקי (רכיב Approval או משתמש) — לא Risk Gate |
| pending | rejected | ידני / RISK_GATE | דחייה ידנית או דחיית סיכון מפורשת |
| pending | expired | TRADE_VALIDITY | Proposal TTL פג / סטיית מחיר |
| pending | escalated_for_review | RISK_GATE / CIO_SCAN | הסלמת הגנה / סיכון |
| approved | wait | EXECUTION_GATE | חסימת ביצוע זמנית לפני שליחה (שוק סגור / Freeze / תנאי לא התקיים) |
| approved | executed | EXECUTION (CH3-003) | מילוי סופי מאושר מ-CH3-003 בלבד — לא שליחה/Timeout/Partial Fill אוטומטי |
| approved | rejected | RISK_GATE | הון אזל / דחיית סיכון מפורשת (לא סטיית מחיר) |
| approved | expired | TRADE_VALIDITY | TTL פג / סטיית מחיר חריגה |
| wait | approved | EXECUTION_GATE | החזרת אישור קיים לאחר אימות TTL + Trade Validity + Risk Gate מחדש — אינו אישור חדש |
| wait | expired | TRADE_VALIDITY | TTL פג / סטיית מחיר |
| wait | rejected | RISK_GATE | הון אזל / דחיית סיכון מפורשת באימות מחדש |
| escalated_for_review | approved | ידני (פתרון סקירה) | המשך / החזק |
| escalated_for_review | rejected | ידני (פתרון סקירה) | מכור / דחה |
מעברים אסורים:
pending → wait חסום — wait מייצג Trade שכבר אושר; נכנס רק מ-approved. (תיקון 0.3)
wait → executed חסום (חובת wait → approved → executed) — כמו pending → executed.
pending → executed חסום (חובת approved בינתיים).
executed / rejected / expired הם טרמינליים — אין מעבר החוצה.
escalated_for_review נגיש מ-pending בלבד (לא מ-approved/wait ישירות).
Timeout לאחר שליחת פקודה אינו מחזיר ל-wait ואינו מאפשר שליחה נוספת — טופל ב-CH3-003 (execution_state).
approved נשאר בתוקף לאחר שליחת פקודה; שליחה / Timeout / Partial Fill אינם executed אוטומטית. RECONCILIATION_REQUIRED אינו משנה Trade.status.
transitionTradeStatus(tradeId, expectedFrom, expectedVersion, to, actorContext, reasonCode, idempotencyKey) היא הדרך היחידה לשנות status של Trade קיים. אף קוד אינו רשאי לכתוב Trade.update({ status }) ישירות (אכיפת grep).
rejected = החלטה שלילית מפורשת (ידנית או דטרמיניסטית של Risk Gate). תפוגה / סטיית מחיר / שוק סגור / נתונים ישנים / תקלה → expired.
wait מייצג Trade שכבר אושר אך ביצועו חסום זמנית — נכנס רק מ-approved. wait → approved אינו אישור חדש, אלא החזרת אישור קיים לאחר אימות TTL + Trade Validity + Risk Gate מחדש. ל-wait משמעות אחת בלבד.
Risk Gate אינו מעניק אישור עסקי — הוא רק מרשה המשך / דוחה / מסלים. אישור ראשוני pending→approved מבוצע רק על ידי Approval או משתמש מורשה; החזרת אישור קיים wait→approved מותרת ל-EXECUTION_GATE לאחר TTL, Trade Validity ו-Risk Gate. (תיקון 0.31)
approved → executed מותר רק לאחר אישור סופי מ-CH3-003 בהתאם למדיניות המילוי. שליחה לבד ≠ executed; Timeout ≠ executed; Partial Fill אינו עובר אוטומטית; RECONCILIATION_REQUIRED אינו משנה Trade.status.
approved נשאר בתוקף גם לאחר שליחת פקודה — execution_state מנהל את הפקודה עצמה (CH3-003).
אידמפוטנטיות: Retry של אותה בקשה משתמש באותו idempotency_key ומחזיר את אותה תוצאה — ללא מעבר או Audit נוסף. פעולה עסקית חדשה / אישור מחדש / Trade חדש מקבלים מפתח חדש. (תיקון 0.3)
transition_id ו-idempotency_key הם שני ערכים שונים — אסור לאחד אותם לשדה אחד. transition_id מזהה מעבר ספציפי; idempotency_key מאפשר Retry בטוח. (תיקון 0.3)
expectedFrom הוא פרמטר קריאה (חלק מהסכם ה-CAS), לא שדה שמור ב-Trade. ב-Trade נשמרים רק: status, status_version ושדות התפוגה הנדרשים. (תיקון 0.3)
Actor נגזר מהקשר משתמש מאומת או משירות פנימי מורשה — אסור לקרא לשלוח actor="APPROVAL" באופן חופשי ולהעניק לעצמו סמכות. טבלת הרשאות Actor לכל מעבר; reason_code קנוני לבדיקות ודוחות; טקסט חופשי רק כהסבר נוסף. (תיקון 0.3)
שחזור: Trade ללא Audit אינה ניתנת לשחזור בוודאות. נדרשת אחת משתי הכרעות — Transaction אטומי של Trade+Message, או Transition Journal/Outbox שנכתב לפני/יחד עם שינוי הסטטוס. "סריקה מתקנת" בלבד אינה עומדת בדרישת Message כמקור אמת. (תיקון 0.3)
גבול TTL מדויק ב-UTC: now < approval_expires_at → אישור תקף; now >= approval_expires_at → פג. pending משתמש ב-Proposal TTL נפרד מ-Approval TTL של approved/wait. (תיקון 0.3)
דטרמיניסטי לחלוטין — אף LLM אינו מעורב במכונה. כל הסטטוסים lowercase.
חסימת שינוי לאחר שליחה: כאשר execution_state != NOT_SENT, Trade Validity, Risk Gate, Global Freeze ושאר בדיקות טרום-שליחה אינן רשאיות לשנות את Trade.status. לאחר השליחה, רק אירוע סופי ומאומת מ-CH3-003 (חוזה הסיום) רשאי לבצע מעבר שהוגדר בחוזה הסיום. אחרת refreshPortfolioPrices עלול להפוך ל-expired עסקה שכבר נמצאת אצל הברוקר. (תיקון 0.31)
| מנגנון | תיאור |
|---|---|
| expectedFrom + expectedVersion (CAS) | פרמטרים של הקריאה, לא שדות ב-Trade. המעבר מצהיר על סטטוס וגרסה צפויים; אם שונים — נדחה. מונע מרוץ בין שתי בקשות על pending. |
| status_version (ב-Trade) | מונה אופטימי הנשמר ב-Trade. עדכון עם תנאי version=expected ו-$inc; מתקבל רק אם התנאי התקיים. |
| transition_id ≠ idempotency_key (Cache בלבד) | ב-Trade נשמרים last_transition_id ו-last_idempotency_key כ-Cache בלבד — בכל מעבר חדש הם נדרסים. מקור האמת הוא ה-Outbox/Journal, שם התוצאה המקורית לכל idempotency_key נשמרת באופן שאינו נדרס; Retry ישן שמגיע לאחר מעברים נוספים עדיין מוצא את התוצאה ומחזיר אותה. (תיקון 0.31) |
| חתימת הקריאה | transitionTradeStatus(tradeId, expectedFrom, expectedVersion, to, actorContext, reasonCode, idempotencyKey) — משנה נתונים וכותב Audit. assertTransitionAllowed(from, to) פנימית וטהורה. |
| שחזור מלא (Outbox נבחר) | Outbox (Transition Journal) שנכתב לפני/יחד עם שינוי הסטטוס ומאפשר התאוששות מלאה (מי/מה קדם/סיבה/idempotency_key). "סריקה מתקנת" בלבד אינה מספיקה. סעיף הקבצים כבר מניח Outbox — המפרט מכריע על Outbox ולא משאיר שתי אפשרויות. (תיקון 0.31) |
| מקור אמת למפתחות | ה-Outbox/Journal שומר את התוצאה המקורית לכל idempotency_key באופן שאינו נדרס; אותו מפתח מחזיר את אותה הצלחה/שגיאה גם לאחר שבוצעו מעברים נוספים. ב-Trade נשמר Cache בלבד (last_transition_id, last_idempotency_key). (תיקון 0.31) |
null → pending אינו ניתן למימוש דרך transitionTradeStatus(tradeId) — זה מניח שהרשומה כבר קיימת, אך בשלב היצירה היא עדיין לא קיימת. (תיקון 0.3)
פעולת יצירה ייעודית ואטומית createTradeWithAudit(...) יוצרת יחד: Trade חדש ב-pending, status_version התחלתי, רשומת Message Audit ראשונה, transition_id ו-idempotency_key.
היא משתמשת באותו מנגנון Audit כמו transitionTradeStatus — שרשרת ההיסטוריה מתחילה בראשית, לא באמצע.
approved → executed מותר רק לאחר אישור סופי מ-CH3-003 בהתאם למדיניות המילוי שתיקבע שם. (תיקון 0.3)
שליחה לבד ≠ executed. Timeout ≠ executed. Partial Fill אינו עובר אוטומטית ל-executed.
RECONCILIATION_REQUIRED (CH3-003) אינו משנה את Trade.status.
approved נשאר בתוקף לאחר שליחת פקודה — execution_state מנהל את הפקודה; Trade.status ממתין לאישור הסופי.
כל הזמנים נשמרים ומושווים ב-UTC. (תיקון 0.3)
now < approval_expires_at → האישור תקף. now >= approval_expires_at → האישור פג.
pending משתמש ב-Proposal TTL נפרד (proposal_expires_at) מ-Approval TTL של approved/wait (approval_expires_at). שני השעונים מוגדרים במפורש. proposal_expires_at נוסף לסכמת Trade ו-proposal_ttl_hours ל-SystemSettings; createTradeWithAudit מאתחל ומדיניות Migration מטפלת ב-pending קיימות. (תיקון 0.31)
אישור מחדש של רשומה שפגה (expired) יוצר Trade חדש עם idempotency_key חדש — וגם כותב Message ייעודי המקשר ביניהן. (תיקון 0.3)
ה-Message כולל לפחות: expired_trade_id, new_trade_id, סיבת האישור מחדש, והמשתמש/הרכיב שיזם אותו.
כך ניתן לשחזר את שרשרת ההיסטוריה המלאה גם כשהצעה נפתחת מחדש.
אסור שהקרא יוכל לשלוח actor="APPROVAL" באופן חופשי ולהעניק לעצמו סמכות. Actor נגזר מהקשר משתמש מאומת או משירות פנימי מורשה. טבלת הרשאות Actor לכל מעבר נאכפת; reason_code קנוני לבדיקות ודוחות; טקסט חופשי רק כהסבר נוסף.
טבלת הרשאות Actor:
| Actor | מעברים מותרים | תנאי/הערה |
|---|---|---|
| CREATION | null → pending | createTradeWithAudit (ייעודי) |
| APPROVAL / משתמש | pending → approved | אישור עסקי ראשוני בלבד |
| ידני / RISK_GATE | pending → rejected | דחייה ידנית / דחיית סיכון מפורשת |
| TRADE_VALIDITY | pending/approved/wait → expired | טרום-שליחה בלבד (execution_state = NOT_SENT) |
| RISK_GATE | pending/approved/wait → rejected; pending → escalated | טרום-שליחה בלבד |
| CIO_SCAN | pending → escalated_for_review | הגנה / סיכון |
| EXECUTION_GATE | approved → wait; wait → approved | חסימה זמנית / החזרת אישור קיים לאחר TTL+Validity+Risk Gate (תיקון 0.31) |
| EXECUTION (CH3-003) | approved → executed | אירוע סופי מאומת מחוזה הסיום בלבד |
| ידני (פתרון סקירה) | escalated → approved / rejected | פתרון סקירה אנושית |
ערכי reason_code קנוניים:
קנוניים לבדיקות ודוחות; טקסט חופשי רק כהסבר נוסף ב-Audit.
| שדה | סוג | תיאור |
|---|---|---|
| status (enum) | string | מורחב: pending, approved, executed, rejected, wait, expired, escalated_for_review. כולם lowercase. |
| status_version | number | מונה אופטימי לנעילה מקבילה (CAS); מתעדכן ב-$inc רק אם תנאי הגרסה התקיים. |
| last_transition_id | string | null | Cache בלבד — מזהה המעבר האחרון. נדרס בכל מעבר. מקור האמת ב-Outbox/Journal. (תיקון 0.31) |
| last_idempotency_key | string | null | Cache בלבד — מפתח ה-Idempotency האחרון. מקור האמת ב-Outbox/Journal (תוצאה מקורית שאינה נדרסת). (תיקון 0.31) |
| proposal_expires_at | date-time | null | תוקף Proposal ל-pending; נבדק now >= proposal_expires_at. מאותחל ב-createTradeWithAudit מ-proposal_ttl_hours. (תיקון 0.31) |
| approval_expires_at | date-time | null | תוקף Approval ל-approved/wait; נבדק now >= approval_expires_at. מאותחל במעבר ל-approved מ-approval_ttl_hours. |
| expiration_stage | string | null | APPROVAL_TTL_EXPIRED | PROPOSAL_TTL_EXPIRED | PRICE_DEVIATION_EXCEEDED | MARKET_DATA_STALE. ריק כאשר status אינו expired. |
| expiration_reason | string | null | טקסט חופשי לתחקור. ריק כאשר status אינו expired. |
אינם נשמרים ב-Trade:
expectedFrom / expectedVersion — פרמטרים של הקריאה (חלק מהסכם ה-CAS), אינם נשמרים ב-Trade. (תיקון 0.3)
actor / reason_code — נגזרים מהקשר מאומת ונכתבים ל-Audit; אינם שדות חופשיים ב-Trade שהקרא יכול להזין ישירות.
לא לשנות רשומות rejected על סמך תאריך approval_expires_at בלבד — Trade שנדחתה ידנית לפני חודש תיפגה כיום בתנאי אף שלא נדחתה בגלל TTL.
להמיר rejected → expired רק כאשר קיימת ראיית Audit מפורשת שהסיבה הייתה TTL או סטיית מחיר.
ללא ראיה חד-משמעית — להשאיר rejected.
שדות תפוגה חייבים להיות ריקים כאשר status אינו expired; להסיר expiration_reason="pending_manual_review" מרשומות שנשארות rejected.
חד-פעני, הפיך — סקריפט rollback מוכן לפני הרצה. מפתח Migration: trade.id בלבד.
רשומות קיימות עם pending/approved/executed נשארות כשתפקידן — תוספתי בלבד.
| שלב בצינור | מעברים שמופעלים |
|---|---|
| TRADE_VALIDITY | pending/approved/wait → expired (Proposal TTL / Approval TTL / סטיית מחיר / נתונים ישנים) — ב-UTC |
| RISK_GATE | pending/approved/wait → rejected (הון אזל / דחיית סיכון מפורשת); pending → escalated_for_review. אינו מעביר ל-approved. |
| CIO_SCAN | pending → escalated_for_review (הגנה / סיכון) |
| APPROVAL / ידני | pending → approved (אישור ראשוני בלבד) (תיקון 0.31) |
| EXECUTION_GATE | approved → wait; wait → approved (החזרת אישור קיים לאחר TTL+Validity+Risk Gate) — Actor קנוני אחד לשני המעברים (תיקון 0.31) |
| EXECUTION (CH3-003) | approved → executed רק לאחר אירוע סופי מאומת מחוזה הסיום. אסור: wait → executed, pending → wait. כאשר execution_state != NOT_SENT — Trade Validity/Risk Gate/Global Freeze חסומות משינוי Trade.status. |
אישור מחדש של expired → יצירת Trade חדש עם idempotency_key חדש (לא פתיחת הישנה) + כתיבת Message קישור (expired_trade_id, new_trade_id, סיבה, מזמים).
גבול TTL מדויק ב-UTC: now < approval_expires_at → תקף; now >= → פג. pending משתמש ב-Proposal TTL נפרד.
Global Freeze: חל על קנייה חדשה בלבד. מכירה / צמצום / יציאה מותרות גם בהקפאה — אינן חסומות במעבר ל-executed.
Partial Fill / Reconciliation: אינם משנים Trade.status. מטופלים ב-execution_state (CH3-003) בלבד.
Retry: אותה בקשה חוזרת עם אותו idempotency_key ומחזירה אותה תוצאה — ללא מעבר/Audit נוסף.
base44/entities/Trade.jsonc — enum + status_version + last_transition_id/last_idempotency_key (Cache) + proposal_expires_at + approval_expires_at + שדות תפוגה (⚠ דורש אישור Architect). expectedFrom אינו נשמר. (תיקון 0.31)
base44/entities/SystemSettings.jsonc — הוספת proposal_ttl_hours (תיקון 0.31).
base44/shared/stateMachine.ts (חדש) — transitionTradeStatus() + createTradeWithAudit() + assertTransitionAllowed() + CAS (status_version) + Outbox (Transition Journal) נבחר + Audit קנוני + טבלת הרשאויות Actor + חסימת טרום-שליחה (תיקון 0.31)
base44/shared/tradeExecution.ts — כל שינוי status דרך המכונה; approved→executed רק לאחר אישור סופי מ-CH3-003
base44/functions/refreshPortfolioPrices/entry.ts — תפוגה/סטיית מחיר → expired (UTC); Risk Gate אינו מאשר
base44/functions/runAgentScan/entry.ts — יצירת הצעה דרך createTradeWithAudit
base44/functions/executeTrade/entry.ts — wait→approved (החזרת אישור); approved→executed רק לאחר אישור סופי; איסור wait→executed, pending→wait
src/pages/Trades.jsx + src/components/invest/Badges.jsx — תצוגת wait / expired / escalated_for_review
מעבר לא-חוקי (pending→wait, pending→executed, wait→executed) נדחה בשגיאה ומתועד.
wait נכנס רק מ-approved; wait→approved אינו אישור חדש אלא החזרת אישור קיים לאחר TTL+Validity+Risk Gate מחדש.
approved→executed מתרחש רק לאחר אישור סופי מ-CH3-003; שליחה/Timeout/Partial Fill אינם executed; RECONCILIATION_REQUIRED אינו משנה status.
Risk Gate אינו מבצע מעבר ל-approved (רק APPROVAL / ידני).
createTradeWithAudit יוצר Trade+status_version+Audit+transition_id+idempotency_key יחד (null→pending מתועד).
Retry עם אותו idempotency_key מחזיר אותה תוצאה ללא מעבר/Audit נוסף; פעולה חדשה → מפתח חדש.
transition_id ו-idempotency_key שמורים בשני שדות נפרדים.
expectedFrom/expectedVersion אינם נשמרים ב-Trade (פרמטרי קריאה בלבד).
מרוץ מקבילי נבלם: שתי בקשות על pending → רק אחת מצליחה (CAS / status_version).
שחזור: Transition Journal/Outbox נבחר מאפשר התאוששות מלאה (מי/מה קדם/סיבה/idempotency_key). transition_id ו-idempotency_key נשמרים בנפרד ב-Outbox; ב-Trade נשמרים רק last_transition_id ו-last_idempotency_key כ-Cache.
Actor נגזר מהקשר מאומת; טבלת הרשאות Actor לכל מעבר נאכפת; reason_code קנוני.
גבול TTL ב-UTC: now < approval_expires_at → תקף; now >= → פג; pending משתמש ב-Proposal TTL נפרד.
אישור מחדש של expired → Trade חדש + Message קישור (expired_trade_id, new_trade_id, סיבה, מזמים).
Migration מומש רק עם ראיית Audit מפורשת; ללא ראיה — rejected; שדות תפוגה ריקים כשאינו expired.
grep בקוד מאפס Trade.update({ status }) מחוץ ל-stateMachine.
כל מעבר מתועד ב-Message עם from→to, actor, reason_code, trade_id, transition_id, idempotency_key.
מעבר לאותו סטטוס נוכחי = no-op שקט. כל הסטטוסים lowercase.
כאשר execution_state != NOT_SENT — Trade Validity/Risk Gate/Global Freeze אינן משנות Trade.status; רק חוזה הסיום מ-CH3-003. (תיקון 0.31)
מקור האמת ל-idempotency הוא ה-Outbox/Journal (תוצאה מקורית שאינה נדרסת); ב-Trade רק Cache (last_transition_id, last_idempotency_key). Retry ישן מחזיר את התוצאה המקורית גם לאחר מעברים נוספים. (תיקון 0.31)
proposal_expires_at קיים ב-Trade; proposal_ttl_hours ב-SystemSettings; createTradeWithAudit מאתחל; Migration ל-pending קיימות; קבלה now >= proposal_expires_at. (תיקון 0.31)
Actor קנוני: pending→approved = Approval/משתמש; approved↔wait = EXECUTION_GATE; approved→executed = EXECUTION(CH3-003). טבלת הרשאויות Actor ורשימת reason_code מצורפות. (תיקון 0.31)
1. base44/shared/stateMachine.ts: transitionTradeStatus() + createTradeWithAudit() + assertTransitionAllowed() + CAS (status_version) + Transition Journal/Outbox + Audit קנוני + טבלת הרשאות Actor.
2. base44/entities/Trade.jsonc + SystemSettings.jsonc: enum + status_version + last_transition_id/last_idempotency_key (Cache) + proposal_expires_at + approval_expires_at + שדות תפוגה; SystemSettings: proposal_ttl_hours (עם אישור Architect). expectedFrom אינו נשמר. (תיקון 0.31)
3. רפקטור כל כתיבת status ב-tradeExecution / refreshPortfolioPrices / runAgentScan / executeTrade למכונה; יצירה דרך createTradeWithAudit.
4. מדיניות מילוי: approved→executed רק לאחר אישור סופי מ-CH3-003; שליחה/Timeout/Partial Fill אינם executed.
5. Migration בטוח (ראיית Audit בלבד) + rollback; סריקת UTC ל-TTL.
6. Message קישור אישור מחדש (expired_trade_id/new_trade_id).
7. עדכון UI: Trades.jsx + Badges.jsx — תצוגת wait/expired/escalated_for_review.
8. בדיקת קבלה מתועדת + דוח ביצוע (Ch3ExecutionReport002).