הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

כזה ניסיתי: Agent OS

23/12/2025

אנחנו כבר יודעים שהגדרה טובה של המשימה היא המפתח להצלחה בעבודה עם סוכני קידוד ולחלופין שבחירת מילים לא מספיק טובה בפרומפט יכולה להרוס את כל הפיצ'ר, אבל דבר אחד עדיין לא ברור כשאנחנו עדיין מנסים להבין מה ההשפעה של סוכני קידוד על תהליך העבודה שלנו כמפתחים - מהו שיווי המשקל הנכון בין כתיבת פרומפטים לכתיבת קוד? האם עיקר העבודה שלנו תצטרך להיות ניסוח קבצי טקסט בצורה מספיק מדויקת, או שקוד הוא ה Spec המדויק ביותר ועלינו להשאר ברמת הקוד?

התומכים בגישה הראשונה כבר יצרו שיטת עבודה מלאה שנקראת Spec Driven Development. הרעיון הוא לדחות את כתיבת הקוד כמה שיותר, להשתמש בסוכני AI כדי לכתוב אפיונים ולבנות אפיון כל כך טוב שאפילו סוכן קידוד יוכל להשתמש בו כדי לבנות את הפיצ'ר. אחד הכלים שמקדמים גישה זו נקרא Agent OS ואפשר לקרוא עליו ולהתקין אותו (לגמרי בחינם) מהאתר:

https://buildermethods.com/agent-os

שיטת העבודה ב Agent OS מוכוונת אפיונים: האפיונים הם חלק בלתי נפרד מהקוד, הם נשמרים ב git, הם מסודרים בתיקיות והם נכתבים על ידי סוכן AI לפי פרדיגמת כתיבת אפיונים מסודרת.

אחרי התקנת Agent OS לתוך פרויקט הסוכן ישקיע דקות ארוכות בניתוח קוד הפרויקט כדי להבין מה המטרה שלו, מה הסטאק הטכנולוגי, מה החלקים המרכזיים שבו ויבנה לעצמו שלושה קבצי טקסט שהולכים ללוות אותנו לכל אורך חיי הפרויקט: mission.md, roadmap.md ו tech-stack.md. הוא באמת כותב המון שם המון. הבעיה הראשונה בעיניי עם כמות הטקסט הזאת היא שמישהו צריך לתחזק את זה. תיאורטית אולי מישהו תכנן שסוכן הקידוד יתחזק את הקבצים האלה כשהגדרת המוצר משתנה אבל במציאות אנחנו יודעים שטקסט מיותר בפרומפטים רק מסבך סוכני קידוד ולבני אדם קשה לתחזק קבצי טקסט.

אחרי הגדרת המוצר Agent OS ממשיך לפיתוח פיצ'ר וכאן דברים נהיים מעניינים. אני רגיל בפיתוח פיצ'ר עם AI לכתוב פרומפט ראשוני, אולי לשכלל קצת את הפרומפט בעזרת AI אחר ולתת לסוכן לקודד. מקסימום אם יש פה משימה מסובכת אני אבקש ממנו לתכנן תוכנית פעולה לקובץ טקסט ואז אתן לו לרוץ על התוכנית. ב Agent OS בניית פיצ'ר מורכבת ממספר שלבים:

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

  2. ממסמך הדרישות הסוכן יוצר מסמך אפיון שכולל כותרות כמו "מטרת הפיצ'ר", "User Stories", "עיצוב ויזואלי", "קוד קיים שכדאי להשתמש", ו"מחוץ לסקופ".

  3. אחרי מסמך האפיון הסוכן מייצר מסמך משימות שבנוי כמו Checklist וכל פעם שהוא מסיים משימה הוא מסמן לעצמו x לידה.

כל המסמכים האלה נשמרים בתוך תיקיה עם תאריך ושם הפיצ'ר שהתחלתם לבנות. אני מניח שהכוונה היא לשלב את זה עם גיט ולעשות קומיט לכל פיצ'ר בנפרד, ואז מתוך מזהה הקומיט (למשל אחרי git blame) אפשר לחזור לכל מסמכי האפיון, הדרישות והמשימות ולהבין מה קרה שם ולמה שורה מסוימת נכנסה לקוד.

מסקנות בפועל:

  1. בניית פיצ'ר ניסיון לקחה לי יותר זמן מאשר פיתוח של הפיצ'ר בעצמי.

  2. הצורך לעבור על הקוד אחרי שה AI מסיים לכתוב עדיין קיים אם אתם רוצים שהקוד ישרוד לאורך זמן. כלומר ל AI עדיין יש בעיות של חזרה על קוד, כתיבת קוד מסורבל וחוסר שימוש באבסטרקציות שגורמות לזה שתהיה נקודה של AI כבר יהיה קשה להמשיך לעבוד על קוד שרק AI כתב.

  3. אבל - מסמכי האפיון כן שיפרו משמעותית את הביצועים של סוכן הקידוד בהשוואה לסוכן שרק מקבל פרומפט חלקי ועובד עצמאית.

  4. בנוסף, החוויה של אפיון פיצ'ר בעזרת סוכן, מענה על שאלות ושיחה על הדברים וכתיבתם לפני תחילת הקידוד עזרה בפיתוח הפיצ'ר בלי קשר אם את הפיתוח עצמו עשה ה AI או אני.

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

רוצים לראות Spec Driven Development עם Agent OS בפעולה? הצטרפו אליי לזום בחמישי בבוקר וננסה לכתוב איתו מערכת לא טריוויאלית או לפחות פיצ'ר או שניים ממנה. מצטרפים בדף הוובינרים בקישור הזה:

https://www.tocode.co.il/talking_ai

לימודי תכנות עם AI - מה יותר קל, מה יותר קשה ולאן זה הולך

22/12/2025

חבר שואל - אני רוצה ללמוד תכנות אבל לא בטוח אם יש טעם. אני רוצה לעבוד בפיתוח אבל כל מה שאני רוצה לכתוב ה AI כותב טוב יותר. מי צריך אותי שם בכלל?

בואו נתחיל בלשים את הקלפים על השולחן: ג'מיני יודע היום לכתוב משחק סנייק יותר טוב ויותר מהר ממה שאני כותב. קלוד כתב לי משחק כרטיסיות ללימוד ספרדית עם מנגנון Spaced Repetition כשכל המידע שמור בקבצי טקסט והתרגול משורת הפקודה (ראסט כמובן). לא יהיה הזוי להגיד שכבר היום או בעתיד המאוד קרוב מנועי AI יוכלו לפתור כל תרגיל תכנות של בית ספר יותר טוב מהתלמידים וכנראה גם יותר טוב מהמורה.

עכשיו מה זה אומר עלינו?

המשך קריאה

פתרון AoC 2025 יום 5 חידת הטווחים

21/12/2025

אתמול כתבתי על בדיקת cover? של רובי והיום נראה איך היא עוזרת לנו לפתור בעיה אמיתית מתוך Advent Of Code האחרון ואני מתכוון לחלק השני של יום 5. מה שאהבתי בחלק הזה היה שכשניסיתי לקחת את הפתרון של חלק 1 ולהרחיב אותו לחלק 2 זה פשוט לא עבד וכך הבנתי את הטעות שהיתה לי בחלק 1. אבל בואו לא נקדים את המאוחר ונלך לראות את התרגיל.

המשך קריאה

היום למדתי: בדיקת שייכות ל Range ב Ruby

20/12/2025

תמיד השתמשתי ב include? כדי לבדוק אם מספר נמצא בטווח מסוים והקוד הזה עובד ודי יעיל:

3.3.5 :007 > (1..10).include?(5)
 => true

רובי לא מתאמץ לעבור איבר-איבר בטווח מאחר וזה טווח של מספרים שלמים ו-5 הוא מספר שלם אז מספיק לבדוק שהוא גדול מההתחלה וקטן מהסוף. אגב קל לראות את זה כשמחפשים שבר בתוך הטווח:

3.3.5 :010 > (1..10).include?(4.5)
 => true

כשברור שאם ננסה לעבור איבר-איבר לא נמצא את ארבע וחצי:

3.3.5 :011 > (1..10).to_a.include?(4.5)
 => false

השבוע נתקלתי במקרה בפונקציה conver? שנראתה כאילו עושה אותו דבר:

3.3.5 :008 > (1..10).cover?(5)
 => true
3.3.5 :009 > (1..10).cover?(4.5)
=> true

גם היא לא צריכה לרוץ על כל הטווח ומסתפקת בבדיקת קצוות. אז איפה זה מתחיל להיות מעניין? מסתבר שאנחנו צריכים 2 פונקציות כדי לטפל אחרת בטווחים של דברים יותר מסובכים ממספרים, למשל מחרוזות. ברובי גם זה טווח:

3.3.5 :013 > ('a'..'z').class
 => Range

וכמו עם שברים גם בטווח של מחרוזות אנחנו יכולים לבדוק שמחרוזת מסוימת נמצאת בין ההתחלה לסוף:

3.3.5 :014 > ('a'..'z').include?('t')
 => true
3.3.5 :015 > ('a'..'z').cover?('t')
 => true

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

3.3.5 :018 > ('a'..'d').to_a
 => ["a", "b", "c", "d"]
3.3.5 :019 > ('a'..'d').include?('bob')
 => false
3.3.5 :020 > ('a'..'d').cover?('bob')
 => true

עכשיו צודקים מי שיגידו שזה לא הוגן כלפי השברים עם הנקודה העשרונית, הרי הם היו צריכים לקבל את אותו יחס של מחרוזות. צודקים גם אם תגידו שזה מבלבל שאותה פונקציה include? לפעמים מחזירה תוצאה ב O(1) ופעמים אחרות מחזירה את התוצאה ב O(n). אני יכול רק לענות שהחיים לא הוגנים ורובי באמת יכולה להיות לא עקבית אבל טוב שגיליתי את cover? שייחודית לטווחים ותמיד מחזירה תשובה ב O(1).

איך אפשר לפרוץ למישהו לטלגרם ולמה חשוב לשים לב

19/12/2025

בהנחה שהאיראנים באמת פרצו רק לטלגרם של בנט בלי לגעת בשאר הטלפון שלו עולה השאלה איך זה עובד, איך אפשר לפרוץ למישהו לחשבון הטלגרם, איך אני שומר שלא יפרצו לי לחשבון הטלגרם ומה עושים כדי לצמצם נזקים אם פריצה התרחשה. אני כמובן לא יודע כלום על מה באמת קרה בין האיראנים לבנט אבל אפשר לדמיין תרחישים:

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

  2. השתלטות על סשן פעיל - אם לבנט היה טלגרם מחובר על המחשב וגם על הטלפון, ואולי גם על טלפון ישן, אפשר לגנוב את אחד המכשירים האחרים המחוברים לטלגרם ודרכו להגיע לכל השיחות ואנשי הקשר בטלגרם. זה לא מאוד מסתדר עם הדיווח שהדבר היחיד שנפרץ היה הטלגרם אבל אף פעם אי אפשר לדעת.

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

  4. שימוש בלקוח טלגרם צד שלישי תמים או מרושע - אם בנט השתמש בתוכנת טלגרם צד שלישי והיא לא שמרה כמו שצריך על פרטי ההתחברות האיראנים יכלו לנצל פרצה באותה תוכנת צד שלישי כדי להגיע לפרטי הכניסה.

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

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

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

תשעת אלפים בדיקות עברו

18/12/2025

סיימון ווילסון נתן ל AI לתרגם ספריית פענוח HTML מפייתון ל JavaScript. הקוד המקורי בפייתון נמצא כאן:

https://github.com/EmilStenstrom/justhtml/

והקוד של סיימון ב JavaScript נמצא כאן:

https://github.com/simonw/justjshtml/

לספריה יש חבילת בדיקות של 9,200 בדיקות וה AI נעזר בבדיקות אלה בזמן כתיבת הקוד. כל פעם ה AI כתב קוד, הריץ את הבדיקות, זיהה מה לא עבר ותיקן. התוצאה היא חבילה עם תאימות כמעט מלאה שנכתבה בזמן שסיימון קישט עץ חג מולד וצפה בסרט.

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

הקוד עצמו של ה AI של סיימון באמת מאוד דומה לקוד המקורי רוב הזמן, אבל מדי פעם הוא בורח, למשל ה AI מחק הערות בחלק מהקבצים, מחק מגבלות בקבצים אחרים ולפעמים אפילו שינה לוגיקה. דוגמה אחת היא הקלאס StreamSink שבגרסת הפייתון נראה כך:

class _DummyNode:
    namespace: str = "html"


class StreamSink:
    """A sink that buffers tokens for the stream API."""

    tokens: list[StreamEvent]
    open_elements: list[_DummyNode]

    def __init__(self) -> None:
        self.tokens = []
        self.open_elements = []  # Required by tokenizer for rawtext checks

    def process_token(self, token: Tag | CommentToken | DoctypeToken | Any) -> int:
        # Tokenizer reuses token objects, so we must copy data
        if isinstance(token, Tag):
            # Copy tag data
            self.tokens.append(
                (
                    "start" if token.kind == Tag.START else "end",
                    (token.name, token.attrs.copy()) if token.kind == Tag.START else token.name,
                )
            )
            # Maintain open_elements stack for tokenizer's rawtext checks
            if token.kind == Tag.START:
                # We need a dummy object with namespace for tokenizer checks
                # Tokenizer checks: stack[-1].namespace
                # We can just use a simple object
                self.open_elements.append(_DummyNode())
            else:  # Tag.END
                if self.open_elements:
                    self.open_elements.pop()
                # If open_elements is empty, we ignore the end tag for rawtext tracking purposes
                # (it's an unmatched end tag at the root level)

        elif isinstance(token, CommentToken):
            self.tokens.append(("comment", token.data))

        elif isinstance(token, DoctypeToken):
            dt = token.doctype
            self.tokens.append(("doctype", (dt.name, dt.public_id, dt.system_id)))

        return 0  # TokenSinkResult.Continue

    def process_characters(self, data: str) -> None:
        """Handle character data from tokenizer."""
        self.tokens.append(("text", data))

ובגרסת ה JavaScript נראה כך:

class StreamSink {
  constructor() {
    this.events = [];
    this.openElements = [{ namespace: "html" }];
  }

  processToken(token) {
    if (token instanceof Tag) {
      if (token.kind === Tag.START) {
        this.events.push(["start", [token.name, { ...(token.attrs || {}) }]]);
      } else {
        this.events.push(["end", token.name]);
      }
      return TokenSinkResult.Continue;
    }

    if (token instanceof CommentToken) {
      this.events.push(["comment", token.data]);
      return TokenSinkResult.Continue;
    }

    if (token instanceof DoctypeToken) {
      const dt = token.doctype;
      this.events.push(["doctype", [dt?.name ?? null, dt?.publicId ?? null, dt?.systemId ?? null]]);
      return TokenSinkResult.Continue;
    }

    return TokenSinkResult.Continue;
  }

  processCharacters(data) {
    this.events.push(["text", data]);
  }
}

גם בלי לקרוא לעומק אנחנו רואים שההערות נמחקו. ב JavaScript המידע עדיין מועתק אבל בלי ההסבר שהיה בהערה בפייתון. מי שרק מסתכל על השורה הזו ב JavaScript אולי לא מבין מה מטרתה:

this.events.push(["start", [token.name, { ...(token.attrs || {}) }]]);
      } else {

יותר מזה גם את הלוגיקה של open_elements ה AI מחק לגמרי וכעת משתנה זה נשאר בתור משתנה של המחלקה בלי שימוש אמיתי שם.

אבל הבדיקות עברו...

אני שומע אתכם. סט של 9,200 בדיקות חייב לשכנע אותנו שהספריה תואמת "100%" מבחינת התנהגות לספריה המקורית. וזה בדיוק מה שהופך את הדוגמה הזאת למוצלחת כדי שנשים לב שקוד הוא לא רק התוצאה שלו. ההערות, ההכנה שאנחנו בונים פנימה לשינויים עתידיים, אפילו הבחירה בשמות מסוימים ובסגנון כתיבה מסוים, כל אלה משפיעים על איכות המערכת. משפיעים על היכולת לתחזק, לשפר ולהרחיב אותה מחר ובהמשך.

הבעיה כאן היא לא ש AI תרגם את הקוד. כמו ש AI תרגם את הקוד הוא היה יכול לתרגם גם את ההערות ולשמור על הרוח המקורית והקריאות של הספריה המקורית. הבעיה היא של AI אין "טעם", אין את החוויה האנושית של לקרוא קוד ולחשוב האם זה קוד הספריה שאני רוצה, האם הקוד הזה מבטא את הידיעות והניחושים שלי לגבי העתיד של הספריה.

הבעיה היא לא ש AI כתב את הקוד אלא שאף אחד לא קרא את הקוד והתלבט בכל השאלות שהטרידו את היוצר של ספריית הפייתון המקורית. הכל בסדר ש AI יכתוב קוד כל עוד יש בסוף בן אדם שקורא אותו.

"לא חשבתי על זה" כבר לא אופציה

17/12/2025

אני מסתכל שוב על החלק הראשון של יום 2 של Advent Of Code האחרון. התרגיל היה למצוא מספרים שהחצי השמאלי שלהם שווה לחצי הימני למשל 1313 או 123123. אני פתרתי את זה ברובי באמצעות ריצה על כל המספרים בטווח ובדיקת מחרוזות:

ids[0...ids.length / 2] == ids[ids.length / 2...]

משחק נחמד עם אינדקסים אבל רחוק מלהיות פתרון טוב. הנקודה היא שהיום לא צריך יותר משתי דקות של הדבקת הקוד ל AI בשביל לראות גרסה נכונה יותר, כלומר מהירה יותר, של הפתרון. בשביל המשחק הדבקתי את זה לקלוד, ג'מיני ו ChatGPT. צ'אט וקלוד הבינו מיד שאפשר לכתוב את הלולאה רק על חצי מהמספר ובגוף הלולאה להכפיל כלומר במקום ש n ירוץ מאלף לאלפיים ונבדוק כל מספר בטווח אפשר להריץ את n מעשר עד עשרים ולכל n לחשב את:

n.to_s + n.to_s

ג'מיני ראה עוד צעד קדימה והבין שכל מספר מהצורה שרציתי בעצם מתחלק בעשר בחזקת חצי ממספר הספרות ועוד אחד כלומר כל מספר בן 4 ספרות מהצורה שאנחנו צריכים יתחלק ב 101, כל מספר בן 6 ספרות יתחלק ב 1001 וכך הלאה. לב הלולאה אצל ג'מיני היה בסך הכל:

while curr <= end:
    print(f"Found {curr}")
    total += curr
    curr += basis

כש basis זה אותו עשר בחזקת חצי ממספר הספרות ועוד אחד. מדהים.

עכשיו וידוי, אני יודע שיש אנשים שרואים את הדברים האלה בשניה, יותר מהר מה AI. אני מאמין שיש גם דברים שאני רואה מהר אבל אינדקסים ומכפלות זה לא אחד מהם אז נשים את זה בצד לבינתיים. הנקודה שעד לפני כמה שנים יכולתי להגיד לעצמי טוב "לא חשבתי על זה" או "לא ראיתי את זה". היום כבר אין תירוצים. לוקח שלוש דקות להדביק קטע קוד ב AI ולראות את הדברים שלא חשבת עליהם.

תכנות הוא כבר לא היכולת לחשוב על הפתרון הכי נכון הכי מהר.

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

אם קשה לכם לראות למה היררכיית מחלקות שבחרתם אינה אופטימלית אתם יכולים היום לקבל פידבק בלחיצת כפתור. אם עד עכשיו לא שמתם לב ונזכרתם מאוחר להוסיף עמודה ל DB היום אתם יכולים לקבל פידבק בלחיצת כפתור על מבנה הטבלאות. לא בטוחים שהעמוד שלכם מספיק נגיש? קבלו דוח נגישות בלחיצת כפתור.

תמיד היה קוד עקום. היום בפעם הראשונה כל אחד יכול להדביק קוד עקום ב AI ולראות גרסה מתוקנת שלו (כנראה יותר מאחת). אי אפשר יותר להגיד "לא ידעתי שזה עקום" וגם לא "לא חשבתי על דרך יותר טובה". ה AI מעלה את הרף ואיתו את הציפיות.

אתה הקוד הגרוע

16/12/2025

כשנדמה שהעבודה שלי היא לתחזק קוד גרוע אני אוהב להזכיר לעצמי - "אני הקוד הגרוע". כל שורת קוד שאני מוסיף, מוחק או מעדכן היא הזדמנות. היא יכולה להיות צעד קדימה או צעד אחורה, וזה לא משנה איך המערכת נראית היום.

וזה מחייב.

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

כשמפתחים מתמודדים עם תיקון בעיות קטנות במערכת גדולה שכתובה רע אנחנו נוטים להתלונן, להאשים את המערכת ודי מהר מתרגלים ל"איך שדברים עובדים פה". פונקציה מוגדרת פרטית אבל אני צריך להפעיל אותה ממקום אחר? מה הבעיה אפשר להפוך אותה לציבורית. יש קוד שעושה כמעט את מה שאני צריך במקום אחר? מה הבעיה Copy-Paste קטן והכל מסתדר. יש קוד ששולף מה DB מידע לשורה אחת? מה הבעיה אני אפעיל אותו בלולאה על 200 שורות כדי לקבל את מה שאני צריך. אני לא כתבתי את המערכת אני רק עושה מה שכולם עושים.

אז כמו שסת אוהב להגיד, אנחנו לא תקועים בפקק, אנחנו הפקק. עכשיו בואו נתחיל לעבוד על לפרק אותו.

טיפ קריאות קוד: קודם מה שמעניין

15/12/2025

נתון הקוד הזה:

for i in range(50):
    insert("contacts", f"test{i}", f"test{i}@test.com", 12, [10, 15])

קל לראות את הבעיה הראשונה שלו - הפונקציה insert מסתיימת בשני ערכים שלא ברור מה הם מביעים, לכן תיקון ראשון יהיה העברת הפרמטרים האחרונים עם שמות:

for i in range(50):
    insert("contacts", f"test{i}", f"test{i}@test.com", owner_account=12, label_ids=[10, 15])

כבר יותר טוב ועכשיו אנחנו כבר מצליחים לראות את הבעיות האמיתיות:

  1. שורה ארוכה מדי, יותר מדי פרמטרים ל insert.
  2. סדר שורות לא תואם לסדר חשיבות, המטרה של הקוד היא "להוסיף אנשי קשר". הלולאה היא מימוש.

בעזרת פונקציה אני יכול לתקן את שתי הבעיות בקלות:

insert_contacts(50, prefix="test", owner_account=12, label_ids=[10, 15])

אבל פה יש בעיה כי הפסדתי חלק מהגמישות. אולי אני רוצה להשתמש ב i איפשהו באמצע השם או כתובת המייל. דרך אחת להתקדם עם זה היא להעביר במקום prefix פונקציה לחישוב המייל או שם המשתמש כלומר:

insert_contacts(
    50,
    username_fn=lambda i: f"test{i}",
    email_fn=lambda i: f"test{i}@test.com",
    owner_account=12,
    label_ids=[10, 15]
)

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

insert_contacts = make_insert_contacts(
    owner_account=12,
    label_ids=[10, 15])

insert_contacts(n=50, username_fn=lambda i: f"test{i}")
insert_contacts(n=20, username_fn=lambda i: f"test_user_{i}")

במצב כזה נגדיר את ברירת המחדל ליצירת אימייל בתור פונקציה שלוקחת את שם המשתמש ומוסיפה לו @test.com וכך לא צריך להעביר מחדש את email_fn כל פעם.

נ.ב. הדבקתי את ההצעה האחרונה ל ChatGPT זה מה שהיה לו להוסיף:

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

  2. כדאי לשמור בתוך הפונקציה make_insert_concats את ה-i האחרון שיצרנו כדי שהפעלה נוספת שלה תמשיך מאותו מקום.

  3. כדאי להחזיר את המזהים או הפריטים שהפונקציה יצרה כדי שאפשר יהיה להמשיך להשתמש בהם בבדיקה או בקוד.

מה דעתכם? איזה גרסה אתם מעדיפים? או האם יש לכם רעיונות נוספים יותר קריאים לכתוב קוד דומה?

היום למדתי: בדיקת Origin כחלק מהגנת CSRF

14/12/2025

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

הגנת CSRF בריילס (ולמעשה באופן כללי, פשוט היום אנחנו מדברים על המימוש בריילס) מורכבת מיצירת שדה מיוחד ונסתדר בטופס שמקבל ערך ייחודי כל פעם שמשתמש גולש לאתר, ובהגשת הטופס הערך שהוגש בטופס נבדק אל מול הערך המתאים שיוצר עבור אותו משתמש. בצורה כזאת אנחנו מונעים העתקה של הטופס - כי לכל משתמש ולכל טעינה של העמוד יש ערך חדש אקראי לאותו שדה "הגנה". אם אתר זדוני יעתיק את הטופס הוא לא ידע מה לכתוב שם.

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

curl -X POST http://localhost:3000/todos \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "commit=Create+Todo"

אבל זה לא עובד ומדפיס את השגיאה בלוג של השרת:

ActionController::InvalidAuthenticityToken (Can't verify CSRF token authenticity.):

בשביל שזה יעבוד אני צריך למשוך את הערך של טוקן ההגנה בבקשת GET ולהוסיף אותו לבקשת POST יחד עם העוגיה שאיתה הטוקן יוצר, כלומר:

TOKEN=$(curl http://localhost:3000/todos/new \
  -c cookies.txt \
  | sed -n 's/.*name="authenticity_token" value="\([^"]*\)".*/\1/p')

curl -X POST http://localhost:3000/todos \
  -b cookies.txt \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "authenticity_token=$TOKEN" \
  -d "commit=Create+Todo"

כל זה טוב ויפה רק שמסתבר שזה לא מכסה את כל המקרים. דפדפנים באופן אוטומטי מוסיפים כותרת בשם Origin לכל בקשה להגשת טופס שכוללת את כתובת האתר ממנו הוגש הטופס. בנוסף לכל הבדיקה שתיארתי כאן החל מריילס 5.1 ריילס מבצע בדיקה נוספת כדי לוודא שה Origin זהה לאתר שלכם - אבל רק אם כותרת Origin באמת נשלחה. זאת הסיבה שפקודת ה curl הצליחה, אין כותרת Origin ולכן מסתפקים בבדיקת הטוקן. נשים לב מה קורה כשאנחנו מצרפים כותרת Origin ואיך ההתנהגות משתנה. תחילה Origin נכון:

curl -X POST http://localhost:3000/todos \
  -b cookies.txt \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "Origin: http://localhost:3000" \
  -d "authenticity_token=$TOKEN" \
  -d "commit=Create+Todo"

הכל עובד והבקשה הצליחה. עכשיו Origin שגוי:

curl -X POST http://localhost:3000/todos \
  -b cookies.txt \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "Origin: http://demo:3000" \
  -d "authenticity_token=$TOKEN" \
  -d "commit=Create+Todo"

הפעם הבקשה נכשלה עם השגיאה:

ActionController::InvalidAuthenticityToken (HTTP Origin header (http://demo:3000) didn't match request.base_url (http://localhost:3000)):

ניתן לבטל את בדיקת ה Origin בעת בדיקת CSRF (למשל אם אתם יודעים שיש לכם קונפיגורציית רשת בעייתית ואיזשהו פרוקסי בדרך מחליט לשנות אותה) באמצעות ההגדרה:

config.action_controller.forgery_protection_origin_check = true

בקובץ הגדרת הסביבה הבעייתית. למרות שכמובן תמיד עדיף לתקן את הגדרות הסביבה הבעייתיות ולא לבטל הגנות.

נ.ב. דפדפנים היום מגדירים עוגיות Same Site כברירת מחדל מה שמייצר עוד שכבת הגנה נגד מתקפות CSRF. אפשר לקרוא על זה בפוסט קודם שכתבתי בנושא כאן:

https://www.tocode.co.il/blog/2025-09-samesite-cookies-and-csrf-protection