CH3-002 · מכונת מצבים דטרמיניסטית ל-Trade

גרסה 1.0 · נעילה 7.8.2026 · P0 · Approved for Implementation

Locked & Approved — Approved for Implementation

הכרעת Architect: Locked & Approved — גרסה 1.0 (Approved for Implementation).

גרסה 0.31 סגרה את כל החסמים המהותיים. הסקירה הארכיטקטונית הושלמה והמפרט מאושר ליציאה לביצוע. הכרעת Architect: השינויים בסכמת Trade וב-SystemSettings מאושרים לביצוע. אין צורך בגרסה 0.32 או בסבב סקירה נוסף; כל שינוי עתידי במפרט ייעשה בגרסה חדשה וב-ADR. המימוש טרם בוצע — CH3-002 ייחשב Completed רק לאחר מימוש, בדיקות הקבלה והגשת Ch3ExecutionReport002.

0 · מפת אימות — תיקוני סקירה (0.3 + 0.31)
הערהנושאתיקון
#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↔waitpending→approved רק Approval/משתמש. wait→approved ו-approved→wait — EXECUTION_GATE (Actor קנוני אחד). §14 תוקן; נוספו טבלת הרשאויות Actor ורשימת reason_code.
1 · בעיה ויעד (השורש)

היום Trade.status הוא enum שטוח [pending, approved, executed, rejected]. המעברים מפוזרים ומרומזים בלבד — ב-runAgentScan, executeTrade, refreshPortfolioPrices וב-UI הידני. אין מצבים ל"ממתין לתנאי", "פג תוקף", "הועבר לבדיקה". תוצאה: עסקאות pending ישנות שנותרות תלויות, TTL אישור אינו מיוצג כמצב, ואין מקור אמת אחד למעברים החוקיים.

מכונת מצבים אחת, דטרמיניסטית, אטומית, אידמפוטנטית וניתנת לשחזור מלא, שכל שינוי סטטוס במערכת חייב לעבור דרכה. פונקציית מעבר אחת, טבלת מעברים אחת, רשומת Audit אחת לכל מעבר (כולל יצירה ואישור מחדש).

2 · שלושת המישורים (הפרדה מוחלטת)
מישורהיקףערכיםשמור?
מצב מחזור ה-Orchestratorפנימי ל-backend בלבד — לא נשמר ב-DBPRICE_REFRESHED → VALIDITY_CHECKED → CIO_SCANNED → RISK_GATED → APPROVAL_SET → EXECUTION_DONEלא
הסטטוס העסקי של ה-TradeTrade.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.

3 · סטטוסים קנוניים (כולם lowercase)
סטטוססוגתיאורטרמינלי?
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חדשהועבר לבדיקה אנושית (מניה מוגנת / סיכון / סכסוך החלטה)לא
4 · מטריצת מעברים חוקיים — שחקן וטריגר
מ-אלשחקן מפעילטריגר
nullpendingיצירהCREATION (ייעודי)יצירת הצעה חדשה — Trade + status_version ראשוני + Audit ראשון נוצרים יחד
pendingapprovedAPPROVAL / ידניאישור עסקי (רכיב Approval או משתמש) — לא Risk Gate
pendingrejectedידני / RISK_GATEדחייה ידנית או דחיית סיכון מפורשת
pendingexpiredTRADE_VALIDITYProposal TTL פג / סטיית מחיר
pendingescalated_for_reviewRISK_GATE / CIO_SCANהסלמת הגנה / סיכון
approvedwaitEXECUTION_GATEחסימת ביצוע זמנית לפני שליחה (שוק סגור / Freeze / תנאי לא התקיים)
approvedexecutedEXECUTION (CH3-003)מילוי סופי מאושר מ-CH3-003 בלבד — לא שליחה/Timeout/Partial Fill אוטומטי
approvedrejectedRISK_GATEהון אזל / דחיית סיכון מפורשת (לא סטיית מחיר)
approvedexpiredTRADE_VALIDITYTTL פג / סטיית מחיר חריגה
waitapprovedEXECUTION_GATEהחזרת אישור קיים לאחר אימות TTL + Trade Validity + Risk Gate מחדש — אינו אישור חדש
waitexpiredTRADE_VALIDITYTTL פג / סטיית מחיר
waitrejectedRISK_GATEהון אזל / דחיית סיכון מפורשת באימות מחדש
escalated_for_reviewapprovedידני (פתרון סקירה)המשך / החזק
escalated_for_reviewrejectedידני (פתרון סקירה)מכור / דחה

מעברים אסורים:

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.

5 · כללים מחייבים

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)

6 · מקביליות, אטומיות ושחזור
מנגנוןתיאור
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)
7 · יצירת Trade (null → pending) — פעולה ייעודית

null → pending אינו ניתן למימוש דרך transitionTradeStatus(tradeId) — זה מניח שהרשומה כבר קיימת, אך בשלב היצירה היא עדיין לא קיימת. (תיקון 0.3)

פעולת יצירה ייעודית ואטומית createTradeWithAudit(...) יוצרת יחד: Trade חדש ב-pending, status_version התחלתי, רשומת Message Audit ראשונה, transition_id ו-idempotency_key.

היא משתמשת באותו מנגנון Audit כמו transitionTradeStatus — שרשרת ההיסטוריה מתחילה בראשית, לא באמצע.

8 · מדיניות מילוי (approved → executed)

approved → executed מותר רק לאחר אישור סופי מ-CH3-003 בהתאם למדיניות המילוי שתיקבע שם. (תיקון 0.3)

שליחה לבד ≠ executed. Timeout ≠ executed. Partial Fill אינו עובר אוטומטית ל-executed.

RECONCILIATION_REQUIRED (CH3-003) אינו משנה את Trade.status.

approved נשאר בתוקף לאחר שליחת פקודה — execution_state מנהל את הפקודה; Trade.status ממתין לאישור הסופי.

9 · גבול TTL מדויק (UTC)

כל הזמנים נשמרים ומושווים ב-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)

10 · קישור אישור מחדש (expired → Trade חדש)

אישור מחדש של רשומה שפגה (expired) יוצר Trade חדש עם idempotency_key חדש — וגם כותב Message ייעודי המקשר ביניהן. (תיקון 0.3)

ה-Message כולל לפחות: expired_trade_id, new_trade_id, סיבת האישור מחדש, והמשתמש/הרכיב שיזם אותו.

כך ניתן לשחזר את שרשרת ההיסטוריה המלאה גם כשהצעה נפתחת מחדש.

11 · Actor ו-Reason קנוניים — טבלת הרשאות ו-reason_code (תיקון 0.31)

אסור שהקרא יוכל לשלוח actor="APPROVAL" באופן חופשי ולהעניק לעצמו סמכות. Actor נגזר מהקשר משתמש מאומת או משירות פנימי מורשה. טבלת הרשאות Actor לכל מעבר נאכפת; reason_code קנוני לבדיקות ודוחות; טקסט חופשי רק כהסבר נוסף.

טבלת הרשאות Actor:

Actorמעברים מותריםתנאי/הערה
CREATIONnull → pendingcreateTradeWithAudit (ייעודי)
APPROVAL / משתמשpending → approvedאישור עסקי ראשוני בלבד
ידני / RISK_GATEpending → rejectedדחייה ידנית / דחיית סיכון מפורשת
TRADE_VALIDITYpending/approved/wait → expiredטרום-שליחה בלבד (execution_state = NOT_SENT)
RISK_GATEpending/approved/wait → rejected; pending → escalatedטרום-שליחה בלבד
CIO_SCANpending → escalated_for_reviewהגנה / סיכון
EXECUTION_GATEapproved → wait; wait → approvedחסימה זמנית / החזרת אישור קיים לאחר TTL+Validity+Risk Gate (תיקון 0.31)
EXECUTION (CH3-003)approved → executedאירוע סופי מאומת מחוזה הסיום בלבד
ידני (פתרון סקירה)escalated → approved / rejectedפתרון סקירה אנושית

ערכי reason_code קנוניים:

APPROVAL_EXPIREDAPPROVAL_GRANTEDFUNDS_EXHAUSTEDGLOBAL_FREEZEMANUAL_REJECTMARKET_CLOSEDMARKET_DATA_STALEORDER_FILLED_FINALORDER_SENTORDER_TIMEOUTPARTIAL_FILL_HELDPRICE_DEVIATION_EXCEEDEDPROPOSAL_EXPIREDPROTECTION_ESCALATIONRECONCILIATION_REQUIREDREVIEW_RESOLVED_HOLDREVIEW_RESOLVED_SELLRISK_REJECTED

קנוניים לבדיקות ודוחות; טקסט חופשי רק כהסבר נוסף ב-Audit.

12 · שדות סכמה ב-Trade (⚠ דורש אישור Architect)
שדהסוגתיאור
status (enum)stringמורחב: pending, approved, executed, rejected, wait, expired, escalated_for_review. כולם lowercase.
status_versionnumberמונה אופטימי לנעילה מקבילה (CAS); מתעדכן ב-$inc רק אם תנאי הגרסה התקיים.
last_transition_idstring | nullCache בלבד — מזהה המעבר האחרון. נדרס בכל מעבר. מקור האמת ב-Outbox/Journal. (תיקון 0.31)
last_idempotency_keystring | nullCache בלבד — מפתח ה-Idempotency האחרון. מקור האמת ב-Outbox/Journal (תוצאה מקורית שאינה נדרסת). (תיקון 0.31)
proposal_expires_atdate-time | nullתוקף Proposal ל-pending; נבדק now >= proposal_expires_at. מאותחל ב-createTradeWithAudit מ-proposal_ttl_hours. (תיקון 0.31)
approval_expires_atdate-time | nullתוקף Approval ל-approved/wait; נבדק now >= approval_expires_at. מאותחל במעבר ל-approved מ-approval_ttl_hours.
expiration_stagestring | nullAPPROVAL_TTL_EXPIRED | PROPOSAL_TTL_EXPIRED | PRICE_DEVIATION_EXCEEDED | MARKET_DATA_STALE. ריק כאשר status אינו expired.
expiration_reasonstring | nullטקסט חופשי לתחקור. ריק כאשר status אינו expired.

אינם נשמרים ב-Trade:

expectedFrom / expectedVersion — פרמטרים של הקריאה (חלק מהסכם ה-CAS), אינם נשמרים ב-Trade. (תיקון 0.3)

actor / reason_code — נגזרים מהקשר מאומת ונכתבים ל-Audit; אינם שדות חופשיים ב-Trade שהקרא יכול להזין ישירות.

13 · Migration בטוח (חד-פעני, הפיך)

לא לשנות רשומות 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 נשארות כשתפקידן — תוספתי בלבד.

14 · אינטגרציה עם צינור CH3-001
שלב בצינורמעברים שמופעלים
TRADE_VALIDITYpending/approved/wait → expired (Proposal TTL / Approval TTL / סטיית מחיר / נתונים ישנים) — ב-UTC
RISK_GATEpending/approved/wait → rejected (הון אזל / דחיית סיכון מפורשת); pending → escalated_for_review. אינו מעביר ל-approved.
CIO_SCANpending → escalated_for_review (הגנה / סיכון)
APPROVAL / ידניpending → approved (אישור ראשוני בלבד) (תיקון 0.31)
EXECUTION_GATEapproved → 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.
15 · תפוגה ואישור מחדש

אישור מחדש של 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 נוסף.

16 · קבצים מושפעים

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

17 · קריטריוני קבלה

מעבר לא-חוקי (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)

18 · סדר ביצוע

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).

Locked & Approved — צא לביצועהמפרט ננעל בגרסה 1.0 ומאושר לביצוע. יש לצאת למימוש לפי סדר הפעולות בסעיף 18, בלי לחרוג מהמפרט. בסיום יש להגיש Ch3ExecutionReport002 עם ראיות לכל קריטריוני הקבלה. כל שינוי עתידי — גרסה חדשה + ADR.