# שער החלטה עם Jev: קוד מלא וגיליון הטמעה

מבוסס על המדריך https://otomatzia.com/guides/jev-decision-model
המדידות המקוריות בוצעו מול jev-latest (jev-1.13.0) ב-5 באוקטובר 2026, עם typesafe-sdk 0.7.2. הגרסה המתוקנת למטה נבדקה בבדיקות רגרסיה מקומיות עם תשובות מודל מדומות; לא בוצעה מדידת API חדשה.

זו דוגמת עזר לביקורת קוד במצב צל. ship הוא המלצה בלבד, לא הרשאה לפריסה. משאירים את בדיקות ה-CI ואת אישור הסוקר הקיימים. נתוני ה-state וה-diff המלא חייבים להגיע ממקור מהימן, כמו Git ו-CI, ולא מטענת הסוכן שכתב את השינוי.

## 1. התקנה

```bash
python3 -m venv .venv
.venv/bin/pip install typesafe-sdk==0.7.2
export TYPESAFE_API_KEY="your-key"
.venv/bin/python gate.py
.venv/bin/python gate.py --show-model
```

## 2. gate.py (שבעה תרחישים; הוסיפו --show-model כדי לראות גם את תשובת Jev בתרחישים שהקוד מכריע)

```python
from typesafe_sdk import TypeSafeClient, Choice, Score, Noul

def build_state(plan_paths, changed_paths, lines_added, tests_exit_code, has_migration, full_diff):
    """Everything here is computed by code. Jev only reads the result."""
    outside = sorted(set(changed_paths) - set(plan_paths))
    return {
        "planned_paths": plan_paths,
        "changed_paths": changed_paths,
        "paths_outside_plan": outside,
        "lines_added": lines_added,
        "tests_passed": tests_exit_code == 0,
        "has_migration": has_migration,
        "diff_excerpt": full_diff[:2000],
        "diff_complete": bool(full_diff.strip()) and len(full_diff) <= 2000,
    }

QUESTIONS = {
    "verdict": Choice(
        instructions="Decide what should happen to this code change.",
        criteria={
            "ship": "Tests passed, every changed path is in the plan, and nothing is irreversible or sensitive.",
            "return": "A reversible deviation from the plan, such as extra files or refactors outside "
                      "the planned paths, with nothing irreversible or sensitive.",
            "escalate": "Irreversible or sensitive: database migrations, authentication, permissions, "
                        "billing or deleted data. If both return and escalate apply, choose escalate.",
        },
    ),
    "blast_radius": Score(
        instructions="How much damage could this change do if it is wrong?",
        criteria=[
            "Contained: one module, easy to revert.",
            "Moderate: several modules or shared code.",
            "Severe: data, auth, billing or anything hard to undo.",
        ],
    ),
    "touches_auth": Noul(instructions="Does this change touch authentication or permissions?"),
}

def precheck(state):
    """Known restrictions cannot be overridden by a model response."""
    if (state["has_migration"] or state["paths_outside_plan"]
            or not state["diff_complete"]):
        return "escalate"
    if not state["tests_passed"]:
        return "return"
    return None

def decide(state, result):
    early = precheck(state)
    if early:
        return early
    verdict = result.choices["verdict"]
    blast = result.scores["blast_radius"].score          # 0.0 to 2.0
    auth = result.nouls["touches_auth"].noul             # P(yes), 0.0 to 1.0

    # 1. Hard risk rules first: they beat any verdict, however confident.
    if blast >= 1.5 or auth >= 0.3:   # missing auth costs more than a false alarm
        return "escalate"
    # 2. The confident path, on the probability of the action itself.
    if verdict.choice == "ship" and verdict.probabilities["ship"] >= 0.88:
        return "ship"
    if verdict.choice == "escalate" and verdict.probabilities["escalate"] >= 0.6:
        return "escalate"
    # 3. Everything else goes back: the cheapest failure.
    return "return"

RANKING = "def rank(results):\n-    return sorted(results, key=lambda r: r.score)\n+    return sorted(results, key=lambda r: (r.score, r.recency), reverse=True)"
SCENARIOS = {
    "clean": (["app/search/ranking.py", "tests/test_ranking.py"], 0, False, RANKING),
    "migration": (["app/search/ranking.py", "tests/test_ranking.py", "db/migrations/0042_add_column.sql"], 0, True,
                  RANKING + "\n+ALTER TABLE results ADD COLUMN recency integer;"),
    "scope_creep": (["app/search/ranking.py", "tests/test_ranking.py", "app/search/cache.py", "app/utils/strings.py"], 0, False,
                    RANKING + "\n+def normalize(s):\n+    return s.strip().lower()\n+CACHE_TTL = 600"),
    "auth_in_diff": (["app/search/ranking.py", "tests/test_ranking.py"], 0, False,
                     RANKING + "\n-    if not user.can_view(result):\n-        continue"),
    "truncated_auth": (["app/search/ranking.py", "tests/test_ranking.py"], 0, False,
                       "# harmless context\n" * 120 + "\n-    if not user.can_view(result):\n-        continue"),
    "migration_tests_failed": (["app/search/ranking.py"], 1, True, RANKING),
    "tests_failed": (["app/search/ranking.py", "tests/test_ranking.py"], 1, False, RANKING),
}

if __name__ == "__main__":
    import sys
    show_model = "--show-model" in sys.argv   # also ask Jev where precheck already decided
    plan = ["app/search/ranking.py", "tests/test_ranking.py"]
    with TypeSafeClient() as client:
        for label, (changed, exit_code, has_mig, diff) in SCENARIOS.items():
            state = build_state(plan, changed, 84, exit_code, has_mig, diff)
            early = precheck(state)
            if early and not show_model:
                print(label, "->", early, "| decided by code, no API call")
                continue
            result = client.system_one(state, QUESTIONS, model="jev-1.13.0")
            v = result.choices["verdict"]
            print(label, "->", early or decide(state, result), "| model", result.model, "| tokens", result.usage.input_tokens)
            print("  verdict", v.choice, {k: round(p, 2) for k, p in v.probabilities.items()},
                  "blast", round(result.scores["blast_radius"].score, 2), "auth", round(result.nouls["touches_auth"].noul, 2))
```

יש להעביר ל-build_state את ה-diff המלא, כולל תוכן של קבצים חדשים. אם האיסוף נכשל, התוכן בינארי או חסר חלק מהשינוי, עוצרים לבדיקה ידנית ולא מפעילים את המודל. diff_complete מסמן רק שהקלט שסופק לא ריק ולא נחתך; הוא אינו יכול לגלות שמערכת האיסוף השמיטה קובץ.

## 3. ההחלטה שאתם מעבירים ל-Jev

שם ההחלטה: __________
בעלים עסקי: __________
האפשרויות (2 עד 10) ותיאור של כל אחת:
1. __________ : __________
2. __________ : __________
3. __________ : __________
מה עולה טעות בכל כיוון: __________
שדות ה-state שמחושבים בקוד: __________

## 4. כללים קשים שקודמים לכל verdict

- __________ => תמיד לאדם
- __________ => תמיד חוזר

## 5. מצב צל

תאריך התחלה: __________   תאריך סיום: __________
מספר דוגמאות מתויגות: __________ (מומלץ 50 עד 200)
גרסת מודל נעולה: jev-1.13.0 / __________

| מזהה | תשובה נכונה | תשובת Jev | הסתברות הפעולה | confidence | הסכמה? |
|---|---|---|---|---|---|
| | | | | | |

## 6. קביעת ספים מהעקומה

| טווח הסתברות | מספר מקרים | דיוק | פעולה |
|---|---|---|---|
| 0.95 ומעלה | | | אוטומטי |
| 0.80 עד 0.95 | | | אישור או מודל חזק |
| מתחת ל-0.80 | | | אדם |

## 7. לפני מעבר לייצור

- [ ] ה-state נאסף מ-Git ו-CI מהימנים, וה-diff כולל את כל השינוי
- [ ] חריגה מהתוכנית, migration או diff חסר עוברים לאדם גם כשהמודל אומר ship
- [ ] ship נשאר המלצה עד להשלמת אישורי ה-CI והסוקר
- [ ] הספים נקבעו מנתונים, לא מפוסט
- [ ] רק הענף הבטוח אוטומטי
- [ ] השאלות, ההגדרות והספים שמורים ב-Git
- [ ] כל קריאה נרשמת בלוג עם הגרסה שענתה, בלי מידע אישי
- [ ] יש מתג כיבוי שמחזיר ללוגיקה הקודמת
