V2-CH2-001 — חבילת Candidate רשמית
כרך 2 · פרק 2 · משימה 001 · ישות Candidate ושלב קדם-ניתוח בצינור ההחלטה
מטרת המסמך
הגדרת ישות Candidate כשלב רשמי בצינור ההחלטה — הזדמנות מזוהה ולא-כפולה שממתינה לניתוח מרובה-סוכנים לפני יצירת Decision. כיום (As-Is, V2-CH1-001) כל סריקה יוצרת Trade/Decision ישירות מפלט של סוכן בודד, ללא דה-דופליקציה וללא המתנה להשלמת כל הסוכנים. פער זה (G6) מונע את מנוע ההחלטה המרובה-סוכנים שכרך 2 בונה. מסמך זה מגדיר את הסכמה, מחזור החיים, לוגיקת הדה-דופליקציה, נקודות השילוב וקריטריוני הקבלה.
הבעיה (G6 — מתוך V2-CH1-001)
אין דה-דופליקציה: שני סוכנים שונים (למשל US Scanner ו-Technical Analyst) עשויים לזהות את אותו symbol באותו cycle. כיום כל אחד יוצר Decision משלו — נוצרות שתי הצעות כפולות לאותו נכס.
אין המתנה להשלמת ניתוח: Decision נוצר מיד כשסוכן בודד מסיים, לפני ששאר הסוכנים סיימו. אין איחוד דעות.
אין תיעוד הזדמנות: אם סוכן זיהה הזדמנות אך היא לא הפכה ל-Decision — אין עקבות. Candidate משמר את היסטוריית הגילוי לצורך audit ושיפור.
מיפוי End-to-End — עם שלב Candidate ★
תרשים זרימה End-to-End — עם שלב Candidate ★
Trigger runAgentScan | ← | Agent (LLM) analyzeAgent → InvokeLLM | ← | Candidate ★ createCandidate (dedup, queue) | ← | Multi-Agent Analysis N × adaptAgentOutput | ← | AnalysisOutput[] conclusion, confidence, evidence |
| ↓ | ||||||||
Evidence Fusion V2-CH5 (future) | ← | Decision createDecision + publishDecision | ← | Risk Gate assertDecisionActive + checkRiskGatePassed | ← | Approval checkApprovalValid | ← | Execution executeTrade |
| שלב | פאזה | פונקציה · קובץ | קלט → פלט | ישות נשמרת |
|---|---|---|---|---|
| 1 | CIO_SCAN Cron 10min / Manual | analyzeAgent() runAgentScan/entry.ts:452 | קלט: portfolioSymbols, agent config, LLM prompt פלט: LLM result: { symbols[], sentiment, summary } | AgentLog, Message |
| 1c ★ | CANDIDATE_CREATE After LLM result | createCandidate() candidateService.ts (V2-CH2 new) | קלט: symbol → asset_id (AssetMaster), market, discovered_by, initial_score, expected_agent_ids (active+relevant) פלט: Candidate (discovered) — dedup by asset_id+window. צילום expected_agent_ids נשמר. | Candidate ★ |
| 1d ★ | CANDIDATE_DEDUP Within createCandidate (atomic) | resolveAssetId() + idempotency check candidateService.ts (V2-CH2 new) | קלט: symbol → asset_id, idempotency_key, active candidates פלט: new Candidate OR No-Op (returns existing). AuditEvent נרשם על ניסיון כפול. אין רשומות SUPERSEDED | Candidate (no SUPERSEDED records) |
| 2 ★ | CANDIDATE_QUEUE After dedup | queueForAnalysis() candidateService.ts (V2-CH2 new) | קלט: Candidate (discovered, non-duplicate) פלט: Candidate (status: analyzing) — ממתין ל-quorum מתוך expected_agent_ids (צילום קבוע) | Candidate |
| 3 ★ | MULTI_AGENT_ANALYSIS Candidate queue populated | adaptAgentOutput() × N agents agentAdapters.ts:11 (existing, reused) | קלט: Candidate + each agent LLM result פלט: N × AnalysisOutput with candidate_id (unique on candidate_id+agent_id; Idempotent writes — no override after Quorum) | AnalysisOutput (candidate_id = source of truth) |
| 4 ★ | CANDIDATE_PROMOTE Quorum (≥60% expected_agent_ids) הושג עד analysis_deadline | promoteCandidate() candidateService.ts (V2-CH2 new) | קלט: Candidate (analyzing) + linked AnalysisOutputs פלט: Candidate (status: promoting) — מעבר אטומי: יצירת Decision + עדכון decision_id + מעבר ל-PROMOTED בטרנזקציה אחת | Candidate (promoting→promoted) |
| 4e ★ | CANDIDATE_EXPIRE analysis_deadline הגיע, quorum לא הושג | sweepExpired() candidateService.ts (V2-CH2 new) | קלט: Candidate (analyzing) עם analysis_deadline שעבר פלט: Candidate (status: expired) — אין קידום אוטומטי מחמת תום זמן | Candidate (status: expired) |
| 4r ★ | PROMOTING_RECONCILE Candidate תקוע ב-PROMOTING > 60 שניות | reconcileStuckPromoting() candidateService.ts (V2-CH2 new) | קלט: Candidate (status: promoting, promotion_locked_at stale) פלט: השלמת קידום (אם Decision נוצר) או סימון כ-rejected. נעילה משוחררת. AuditEvent נרשם | Candidate (promoting→promoted/rejected) |
| 5 | DECISION Candidate promoted (אטומי) | createDecision() + publishDecision() decisionObject.ts:243,306 (existing, reused) | קלט: DecisionData from promoted Candidate + AnalysisOutputs פלט: Decision: DRAFT → ACTIVE (frozen snapshot) | Decision |
| 5a | TRADE_CREATION After Decision published | createTradeWithAudit() stateMachine.ts (existing) | קלט: Trade data + decision_id + candidate_id פלט: Trade (pending) | Trade |
| 5b | APPROVAL After Trade creation | checkApprovalValid() decisionObject.ts:617 (existing) | קלט: Decision (ACTIVE) + Trade (pending) פלט: ApprovalRecord (approved / rejected) | ApprovalRecord |
| 6 | RISK_GATE Before each execute | assertDecisionActive + checkRiskGatePassed decisionObject.ts:569,607 (existing) | קלט: Decision (ACTIVE) פלט: Pass / Fail (veto) | RiskVerdict |
| 7 | EXECUTION After gates pass | executeTrade() tradeExecution.ts (existing, reused) | קלט: Approved trades פלט: Executed trades | Trade (executed) |
סכמת ישות Candidate
| שדה | סוג | חובה | תיאור |
|---|---|---|---|
| symbol | string | חובה | סמל נכס (למשל NVDA, AAPL) |
| name | string | אופציונלי | שם חברה |
| market | enum | חובה | TASE / NYSE / NASDAQ / LSE / TSE / HKEX |
| asset_type | enum | אופציונלי | stock / etf / fund / bond / index / reit / commodity / crypto |
| discovered_by | string | חובה | שם הסוכן שזיהה את ההזדמנות |
| discovery_cycle_id | string | חובה | מזהה מחזור הסריקה (לקיבוץ Candidates מאותו cycle) |
| expected_agent_ids | array<string> | חובה | צילום קבוע של מזהי הסוכנים הצפויים בעת יצירת ה-Candidate. כולל רק סוכנים פעילים (status=active) ורלוונטיים לנכס ולשוק (לפי asset_types ו-market ב-AgentConfig). מקפיא את קבוצת הסוכנים למחזור זה. quorum = ceil(60% × |expected_agent_ids|). פלט תקף אחד בלבד לכל סוכן (ייחודיות על candidate_id+agent_id; כתיבה חוזרת Idempotent — אין דריסה לאחר השגת Quorum). |
| analysis_deadline | date-time | חובה | מועד אחרון להשלמת quorum. חייב להיות ≤ expires_at. אם לא הושג עד מועד זה — ה-Candidate עובר ל-EXPIRED (אין קידום אוטומטי מחמת תום זמן). |
| initial_score | number (0-10) | אופציונלי | ציון ביטחון ראשוני מהסוכן המגלה |
| initial_direction | enum | אופציונלי | buy / sell / hold / investigate (ברירת מחדל: investigate) |
| initial_reasoning | string | אופציונלי | נימוק ראשוני מהסוכן המגלה |
| current_price | number | אופציונלי | מחיר בעת גילוי |
| status | enum | חובה | discovered / analyzing / promoting / promoted / rejected / expired |
| asset_id | string (ref) | חובה | מזהה קנוני מ-AssetMaster (CH5). משמש לדה-דופליקציה — מונע כפילויות הנובעות מכינויים וסימולים שונים לאותו נכס. |
| dedup_key | string | חובה | מפתח דה-דופליקציה: hash(asset_id + window_bucket). מבוסס על asset_id הקנוני, לא על symbol. |
| idempotency_key | string | חובה | מפתח Idempotency: hash(asset_id + discovery_cycle_id). מאפשר אטימיות רק באמצעות Unique/Atomic Upsert או Claim ייחודי. ללא מנגנון כזה — Best Effort ב-Shadow בלבד. |
| analysis_outputs | array<string> (derived) | אופציונלי | נגזר מ-AnalysisOutput.candidate_id. אינו מקור אמת — לקריאה בלבד. |
| decision_id | string (ref) | אופציונלי | מזהה Decision שנוצר (מתמלא בקידום) |
| promotion_locked_at | date-time | אופציונלי | מועד תחילת נעילת PROMOTING. אם ה-Candidate תקוע ב-PROMOTING מעל 60 שניות — Reconciliation פעיל. |
| priority_class | enum | אופציונלי | P0_PROTECTION / P1_ANALYSIS / P2_BACKGROUND (ברירת מחדל: P1_ANALYSIS) |
| expires_at | date-time | חובה | תפוגה (TTL) — ברירת מחדל: created_date + 30 דקות |
| promoted_at | date-time | אופציונלי | מועד קידום (מתמלא ב-promoteCandidate) |
| rejected_reason | string | אופציונלי | סיבת דחייה (אם נדחה) |
מחזור חיים ומעברי מצבים
סוכן זיהה הזדמנות. נוצר Candidate עם dedup_key (על בסיס asset_id קנוני), idempotency_key, וצילום expected_agent_ids (סוכנים פעילים+רלוונטיים). אם כפיל — No-Op מחזיר את המקורי + AuditEvent.
ה-Candidate ממתין להשלמת quorum (ceil(60% × |expected_agent_ids|)). כל AnalysisOutput מקושר אליו דרך candidate_id (מקור האמת). פלט תקף אחד לכל סוכן (ייחודיות על candidate_id+agent_id; כתיבה חוזרת Idempotent). אין קידום אוטומטי מחמת תום זמן.
מצב ביניים אטומי: יצירת Decision + עדכון decision_id + מעבר ל-PROMOTED בטרנזקציה אחת. promotion_locked_at נשמר. אם תקוע > 60 שניות — Reconciliation פעיל.
המעבר האטומי הושלם. ה-Candidate מכיל decision_id ואינו בר-שינוי. קישור דו-כיווני ל-Decision.
נדחה על-ידי המערכת (רוב NO_OPINION, סכסוך בלתי-פתיר, ספים לא עברו, Reconciliation של PROMOTING תקוע).
analysis_deadline הגיע ו-quorum לא הושג, או expires_at עבר. אין קידום אוטומטי מחמת תום זמן.
| מ | אל | טריגר | תנאי |
|---|---|---|---|
| discovered | analyzing | queueForAnalysis() | non-duplicate (No-Op returned existing, or new created) |
| analyzing | promoting | promoteCandidate() | quorum הושג: ceil(60% × |expected_agent_ids|) עד analysis_deadline — לא קידום אוטומטי מחמת תום זמן |
| promoting | promoted | atomic transaction | יצירת Decision + עדכון decision_id + מעבר ל-PROMOTED בטרנזקציה אחת |
| promoting | rejected | reconcileStuckPromoting() | תקוע ב-PROMOTING > 60 שניות — Reconciliation משלים (אם Decision נוצר) או מסמן כ-rejected |
| analyzing | rejected | evaluateCandidate() | quorum הושג אך רוב NO_OPINION, או סכסוך בלתי-פתיר, או ספים לא עברו |
| discovered | expired | sweepExpired() | expires_at passed without promotion |
| analyzing | expired | sweepExpired() | analysis_deadline הגיע ו-quorum לא הושג — אין קידום אוטומטי |
| promoted | (terminal) | Decision created | decision_id set — Candidate lifecycle complete |
אילוצי עיצוב (Invariants)
חד-פעמיות אטומית (No-Op): לכל asset_id+window קיים Candidate פעיל אחד לכל היותר. דה-דופליקציה במודל No-Op — createCandidate עם dedup_key/idempotency_key קיים מחזיר את ה-Candidate המקורי, לא יוצר רשומת SUPERSEDED. AuditEvent (DUPLICATE_DISCOVERY_ATTEMPT) נרשם.
דה-דופליקציה קנונית: dedup_key מבוסס על asset_id מ-AssetMaster (CH5), לא על symbol — מונע כפילויות הנובעות מכינויים וסימולים שונים לאותו נכס.
קידום אטומי (PROMOTING): מעבר מ-analyzing ל-promoted עובר דרך מצב ביניים PROMOTING. יצירת Decision + עדכון decision_id + מעבר ל-PROMOTED מתבצעים בטרנזקציה אחת — מונע מצב שבו Candidate מקודם ללא decision_id.
Reconciliation: אם Candidate תקוע ב-PROMOTING מעל 60 שניות — reconcileStuckPromoting() משלים את הקידום (אם Decision נוצר) או מסמן כ-rejected. promotion_locked_at משוחרר.
מצב קפוא: לאחר קידום ל-promoted, ה-Candidate אינו בר-שינוי. ניתן לקרוא אותו לצורך Decision אך לא לעדכן שדות ניתוח.
קישור ל-Decision: promoted Candidate חייב להכיל decision_id. Decision חייב להכיל candidate_id. קישור דו-כיווני מאומת.
Shadow אינו יוצר Decision: ב-shadow, promoteCandidate() אינו יוצר Decision מקביל. הוא מתעד איזה Decision היה נוצר ומקשר ל-Decision הקיים מהנתיב הסטנדרטי.
אין קידום אוטומטי מחמת תום זמן: אם analysis_deadline הגיע ו-quorum לא הושג — ה-Candidate עובר ל-EXPIRED. קידום מתבצע רק כאשר quorum הושג.
אין וטו: Candidate אינו Risk Gate. הוא אוסף ומסנן הזדמנויות אך אינו חוסם ביצוע. וטו שייך רק ל-Risk Gate הדטרמיניסטי.
עסקאות ידניות: ב-enforced, candidate_id חובה רק לעסקאות origin=agent. עסקה ידנית (origin=manual, מהגרף) מותרת ללא candidate_id עם Audit מלא.
Quorum חד-משמעי: expected_agent_ids כולל רק סוכנים פעילים (status=active) ורלוונטיים לנכס ולשוק. quorum = ceil(60% × |expected_agent_ids|). פלט תקף אחד בלבד לכל סוכן (ייחודיות על candidate_id+agent_id; כתיבה חוזרת Idempotent — אין דריסה לאחר השגת Quorum). analysis_deadline ≤ expires_at.
כללי דה-דופליקציה
| מזהה | חומרה | כלל |
|---|---|---|
| D1 | P0 | חלון דה-דופליקציה: אותו asset_id (קנוני מ-AssetMaster, CH5) בתוך אותו discovery_cycle_id. אם קיים Candidate פעיל (discovered/analyzing/promoting) — הקריאה היא No-Op: מחזירה את ה-Candidate הקיים. אין יצירת רשומת SUPERSEDED. |
| D2 | P1 | חלון חוצה-cycle: אם קיים Candidate פעיל מ-cycle קודם שטרם פג תוקפו (expires_at) — הקריאה היא No-Op: מחזירה את ה-Candidate הקיים. AuditEvent נרשם על ניסיון הגילוי הכפול. |
| D3 | P0 | dedup_key = hash(asset_id + window_bucket). מבוסס על asset_id הקנוני מ-AssetMaster (CH5), לא על symbol — מונע כפילויות הנובעות מכינויים וסימולים שונים לאותו נייר (למשל BRK.B ו-BRK-B — סימונים שונים לאותו נייר; להבדיל מ-GOOG/GOOGL שהם סוגי מניות שונים ואסור למפותם לאותו asset_id). |
| D4 | P1 | אין מחיקת Candidates. סגירה אך ורק דרך מעבר מצב (expired/rejected). |
| D5 | P0 | מודל No-Op אטומי: createCandidate עם dedup_key/idempotency_key קיים מחזיר את ה-Candidate המקורי מבלי ליצור רשומה חדשה. AuditEvent נרשם על ניסיון הגילוי הכפול (actor = הסוכן שניסה, entity_ref = ה-Candidate המקורי, event_type = DUPLICATE_DISCOVERY_ATTEMPT). |
| D6 | P0 | אטימיות דה-דופליקציה: filter + return existing אינו אטומי — אינו מונע משני סוכנים ליצור Candidate במקביל. נדרש Unique/Atomic Upsert אמיתי על dedup_key/idempotency_key, או ישות נעילה/Claim בעלת מפתח ייחודי. אם הפלטפורמה אינה מאפשרת זאת — המנגנון מסומן Best Effort ב-Shadow בלבד, ואסור לעבור ל-Enforced או לאשרר את C2 עד להוכחת אטימיות. במעבר ל-PROMOTING, נשמר promotion_locked_at. אם ה-Candidate תקוע ב-PROMOTING מעל 60 שניות — reconcileStuckPromoting() משלים (אם Decision נוצר) או מסמן כ-rejected. |
נקודות שילוב בקוד הקיים
runAgentScan/entry.ts (לאחר analyzeAgent): ב-Shadow — קוראים ל-createCandidate() בנוסף לנתיב הקיים (createDecision), באופן לא-חוסם. אין לשנות או לעצור את יצירת ה-Decision הקיימת. ה-Candidate נכנס לתור וממתין לשאר הסוכנים — לצורך תיעוד והכנה ל-Evidence Fusion בלבד.
agentAdapters.ts:adaptAgentOutput(): נוסף פרמטר candidate_id ב-AnalysisOutput. מקור האמת הוא AnalysisOutput.candidate_id עם ייחודיות על (candidate_id, agent_id) — פלט אחד תקף לכל סוכן. המערך candidate.analysis_outputs[] הוא נגזר בלבד (לקריאה). כתיבה חוזרת Idempotent — אין "האחרון דורס" לאחר השגת Quorum.
decisionObject.ts:createDecision(): נוסף שדה אופציונלי candidate_id ב-DecisionData. במצב enforced — חובה רק לעסקאות מקור סוכן (origin=agent). עסקה ידנית (origin=manual, מהגרף) אינה דורשת candidate_id — נשמרת עם Audit מלא. ב-shadow — אופציונלי לכל. ה-Decision מקושר בחזרה דרך candidate.decision_id.
candidateService.ts (חדש): מודול חדש ב-base44/shared/ המכיל: resolveAssetId(), createCandidate(), queueForAnalysis(), promoteCandidate(), evaluateCandidate(), sweepExpired(), reconcileStuckPromoting().
טרנזקציה והתאוששות: (1) אטימיות דה-דופליקציה — נדרש Unique/Atomic Upsert אמיתי על idempotency_key, או ישות נעילה/Claim בעלת מפתח ייחודי. filter + return existing אינו אטומי ואינו מספיק. אם הפלטפורמה אינה מאפשרת — המנגנון מסומן Best Effort ב-Shadow בלבד, ואסור לעבור ל-Enforced או לאשרר את C2; (2) נעילה — promotion_locked_at נשמר בכניסה ל-PROMOTING; (3) Reconciliation — reconcileStuckPromoting() (רקע) מזהה Candidates תקועים ב-PROMOTING מעל 60 שניות, משלים את הקידום (אם Decision נוצר) או מסמן כ-rejected.
shadow mode: בשלב ראשון, השילוב פעיל ב-shadow בלבד — Candidate נוצר ומתועד, ומקושר ל-Decision שנוצר על-ידי הנתיב הקיים (סוכן → Decision). promoteCandidate() אינו יוצר Decision מקביל ב-shadow — הוא רק מתעד איזה Decision היה נוצר ומקשר את ה-Candidate ל-Decision הקיים מהנתיב הסטנדרטי. מעבר ל-enforced רק לאחר אימות קריטריוני הקבלה וסגירת V2-CH5 (Evidence Fusion).
קריטריוני קבלה
| מזהה | סוג | קריטריון קבלה | תוצאה |
|---|---|---|---|
| C1 | Schema | קיימת ישות Candidate עם שדות: asset_id (קנוני מ-AssetMaster), symbol, market, discovered_by, discovery_cycle_id, expected_agent_ids (סוכנים פעילים+רלוונטיים), analysis_deadline (≤ expires_at), idempotency_key, initial_score, initial_direction, status (כולל promoting), dedup_key (hash(asset_id+window)), promotion_locked_at, decision_id, expires_at (created_date + 30 דקות) | PASS |
| C2 | Dedup | createCandidate() מבצע דה-דופליקציה אטומית במודל No-Op: אם קיים Candidate פעיל עם אותו dedup_key/idempotency_key — הקריאה מחזירה את המקורי מבלי ליצור רשומה חדשה. AuditEvent (DUPLICATE_DISCOVERY_ATTEMPT) נרשם. אין רשומות SUPERSEDED. dedup_key מבוסס על asset_id קנוני (לא symbol). דורש Unique/Atomic Upsert אמיתי או ישות נעילה/Claim — filter+return אינו מספיק. אם הפלטפורמה אינה מאפשרת — Best Effort ב-Shadow בלבד, ואסור לעבור ל-Enforced או לאשרר C2 | EXCEPTION |
| C3 | Linkage | מקור האמת: AnalysisOutput.candidate_id עם ייחודיות על (candidate_id, agent_id) — פלט אחד תקף לכל סוכן. המערך candidate.analysis_outputs[] הוא נגזר בלבד (לקריאה). כתיבה חוזרת Idempotent — אין "האחרון דורס" לאחר השגת Quorum | PASS |
| C4 | Promotion | promoteCandidate() מופעל רק לאחר ש-quorum הושג: quorum = ceil(60% × |expected_agent_ids|). expected_agent_ids כולל רק סוכנים פעילים (status=active) ורלוונטיים לנכס ולשוק (לפי asset_types ו-market ב-AgentConfig). פלט יחיד לכל (candidate_id, agent_id); כתיבה חוזרת Idempotent ואין דריסה לאחר השגת Quorum. analysis_deadline ≤ expires_at. אם quorum לא הושג עד analysis_deadline — EXPIRED. אין קידום אוטומטי מחמת תום זמן | PASS |
| C5 | Linkage | Decision שנוצר מ-Candidate מכיל candidate_id בחוזה. Candidate מכיל decision_id. קישור דו-כיווני מאומת. ב-shadow: promoteCandidate() אינו יוצר Decision מקביל — מקשר ל-Decision הקיים מהנתיב הסטנדרטי | PASS |
| C6 | Invariant | מעבר analyzing→promoting→promoted אטומי: יצירת Decision + עדכון decision_id + מעבר ל-PROMOTED בטרנזקציה אחת. promotion_locked_at נשמר בכניסה ל-PROMOTING. אם ה-Candidate תקוע ב-PROMOTING > 60 שניות — reconcileStuckPromoting() משלים (אם Decision נוצר) או מסמן כ-rejected. Candidate עם status=promoted אינו בר-שינוי | PASS |
| C7 | TTL | sweepExpired() מסמן Candidates שעברו את expires_at או ש-quorum לא הושג עד analysis_deadline כ-expired. אין קידום של Candidate שפג תוקפו | PASS |
| C8 | Mode | נתיב legacy פעיל רק ב-shadow. ב-enforced — candidate_id חובה רק לעסקאות מקור סוכן (origin=agent). עסקה ידנית (origin=manual, מהגרף) מותרת ללא candidate_id עם Audit מלא. ב-shadow: Candidate אינו יוצר Decision מקביל | PASS |
| C9 | Invariant | Candidate אינו Risk Gate — אינו חוסם ביצוע. וטו שמור ל-Risk Gate בלבד (G4) | PASS |
| C10 | Audit | AuditEvent נרשם לכל מעבר מצב של Candidate, וכן על ניסיון גילוי כפול (DUPLICATE_DISCOVERY_ATTEMPT) — ה-Candidate המקורי מוחזר (No-Op) ואין רשומת SUPERSEDED | PARTIAL |
ראיות יישום Shadow (11.8.2026)
ישות Candidate: base44/entities/Candidate.jsonc — סכמה מלאה עם כל השדות הנדרשים (asset_id, dedup_key, idempotency_key, expected_agent_ids, analysis_deadline, 6 מצבים כולל promoting, promotion_locked_at, decision_id, expires_at).
candidateService.ts: base44/shared/candidateService.ts — 7 פונקציות: resolveAssetId, createCandidate (דה-דופליקציה No-Op + AuditEvent), queueForAnalysis, evaluateCandidate, promoteCandidate (PROMOTING אטומי), sweepExpired, reconcileStuckPromoting (60s timeout).
AnalysisOutput.candidate_id: שדה נוסף לישות — מקור האמת לקישור (ייחודיות על candidate_id+agent_id).
שילוב ב-runAgentScan: base44/functions/runAgentScan/entry.ts — createCandidate נקרא לאחר analyzeAgent בנוסף לנתיב הקיים (תוספתי, לא-חוסם). הנתיב הקיים (סוכן → Decision) לא השתנה.
תחזוקה אוטומטית: base44/functions/sweepCandidates/entry.ts + base44/workflows/Candidate Lifecycle Sweep.jsonc — workflow מתוזמן כל 5 דקות קורא ל-sweepExpired + reconcileStuckPromoting. נבדק: { expired: 3, reconciled: 0 }.
חריג C2 (אטימיות): דה-דופליקציה פועלת ב-Best Effort (filter + return existing) — אינה אטומית. חסום מ-Enforced עד Unique/Atomic Upsert או Claim ייחודי. פירוט מלא בדוח הביצוע.
סיכום והמלצה
סגירת G6: ישות Candidate מביאה דה-דופליקציה, תיעוד הזדמנויות, והמתנה להשלמת כל הסוכנים — הבסיס לאיחוד ראיות (V2-CH5) ול-CIO (V2-CH7).
אי-פגיעה בכרך 1: כל השינויים הם תוספתיים — ישות חדשה + שדות אופציונליים. כרך 1 נעול ונשאר תקף.
תלות הדדית: קידום Candidate (promoteCandidate) מצפה ל-Evidence Fusion (V2-CH5). בינתיים, קידום יתבצע על בסיס רוב פשוט (majority vote) או הסוכן בעל הציון הגבוה ביותר. מעבר מלא ל-Fusion ב-V2-CH5.
אין וטו: Candidate אינו Risk Gate. הוא אוסף, מסנן ומתעד הזדמנויות אך אינו חוסם ביצוע. וטו שמור ל-Risk Gate הדטרמיניסטי בלבד (G4).
המלצה: המפרט אושר (v1.0) ליישום ב-shadow: יצירת ישות Candidate, מודול candidateService.ts, וחיבור ל-runAgentScan במקביל לנתיב הקיים (תוספתי, לא-חוסם). אין מעבר ל-Enforced לפני: (1) השלמת 10 קריטריוני הקבלה, (2) הוכחת אטימיות C2 (Unique/Atomic Upsert או Claim ייחודי), (3) סגירת Evidence Fusion ב-V2-CH5.