V2-CH2-004 — קידום וקישור Decision
כרך 2 · פרק 2 · משימה 004 · promoteCandidate אטומי, PROMOTING, Reconciliation
v1.0 Approved · 11.8.2026
מטרת המסמך
הגדרה פורמלית של שלב הקידום (Promotion): מעבר מ-analyzing ל-promoting ל-promoted, יצירת Decision אטומית, נעילת promotion_locked_at, ומנגנון Reconciliation ל-Candidates תקועים. סגירת קריטריונים C5 + C6 מ-V2-CH2-001.
גבול כרך 1: מסמך תיאורי — הלוגיקה כבר ממומשת ב-candidateService.ts (Shadow). אינו מוסיף שדות.
שלבי קידום
| שלב | פעולה | תיאור | חוסם? |
|---|---|---|---|
| 1 | evaluateCandidate() | בדיקת quorum: outputs_received ≥ quorum_threshold. אם לא — המתנה. אם כן — מעבר ל-PROMOTING. | חוסם |
| 2 | מעבר ל-PROMOTING | status: analyzing → promoting. שמירת promotion_locked_at = now(). נעילת ה-Candidate למעברים מקבילים. | חוסם |
| 3 | promoteCandidate() | קריאת createDecision() עם candidate_id. יצירת Decision וקבלת decision_id. | חוסם |
| 4 | עדכון candidate.decision_id | שמירת decision_id ב-Candidate. קישור דו-כיווני. | חוסם |
| 5 | מעבר ל-PROMOTED | status: promoting → promoted. שמירת promoted_at = now(). Candidate עם status=promoted אינו בר-שינוי. | חוסם |
Invariants
| מזהה | חומרה | כלל |
|---|---|---|
| I1 | P0 | מעבר analyzing→promoting→promoted אטומי: יצירת Decision + עדכון decision_id + מעבר ל-PROMOTED בטרנזקציה אחת לוגית. |
| I2 | P0 | promotion_locked_at נשמר בכניסה ל-PROMOTING. אם ה-Candidate תקוע ב-PROMOTING > 60 שניות — reconcileStuckPromoting() מופעל. |
| I3 | P0 | Candidate עם status=promoted אינו בר-שינוי — אין עדכון שדות, אין מחיקה, אין קידום חוזר. |
| I4 | P0 | ב-Shadow: promoteCandidate() אינו יוצר Decision מקביל — הוא מקשר ל-Decision הקיים מהנתיב הסטנדרטי. |
| I5 | P0 | אין קידום אוטומטי מחמת תום זמן (deadline). Candidate שפג תוקפו → EXPIRED, לא PROMOTED. |
| I6 | P1 | Candidate שנדחה (rejected) אינו ניתן לקידום חוזר. נדרש Candidate חדש ב-cycle חדש. |
Reconciliation
| טריגר | תנאי | פעולה | תוצאה |
|---|---|---|---|
| PROMOTING > 60s | Decision נוצר (decision_id קיים) | השלמת קידום: status → promoted, promoted_at = now(). | PROMOTED |
| PROMOTING > 60s | Decision לא נוצר (decision_id ריק) | סימון כ-rejected עם rejected_reason="stuck_promoting_no_decision". | REJECTED |
| expires_at עבר | status ב-discovered/analyzing | מעבר ל-expired. אין קידום. | EXPIRED |
| analysis_deadline עבר | quorum לא הושג | מעבר ל-expired. אין קידום. | EXPIRED |
קריטריוני קבלה
| מזהה | קריטריון | תוצאה |
|---|---|---|
| P1 | promoteCandidate() מופעל רק לאחר quorum (≥60%). אין קידום ללא quorum. | PASS |
| P2 | מעבר analyzing→promoting→promoted אטומי: Decision + decision_id + status בטרנזקציה אחת. | PASS |
| P3 | promotion_locked_at נשמר בכניסה ל-PROMOTING. | PASS |
| P4 | reconcileStuckPromoting() מטפל ב-Candidates תקועים > 60 שניות: משלים או דוחה. | PASS |
| P5 | Candidate עם status=promoted אינו בר-שינוי. | PASS |
| P6 | ב-Shadow: אין יצירת Decision מקביל — מקשר ל-Decision הקיים. | PASS |
| P7 | אין קידום אוטומטי מחמת תום זמן. פג תוקף → EXPIRED. | PASS |
מפרט · v1.0 Approved · 11.8.2026 · נעול
חזרה ל-V2-CH2-003