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

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

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

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

כזה ניסיתי: הדפדפן החדש של קרסר מעולה

13/12/2025

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

  1. אנחנו רואים את גרפיקה, AI רואה את הקוד.

  2. אנחנו מרגישים איטיות, AI רואה את הלוגים.

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

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

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

  2. שינוי CSS דרך מסך עריכה ויזאולי בסגנון Dazl.

  3. לקיחת צילום מסך ישירות לתוך ה Chat, מה שמאפשר לי לשאול על מצב ויזואלי מסוים בקלות.

עכשיו אפשר להצביע על כפתור ולהמשיך עם פרומפט כמו

what happens when I click on this? show full trace from frontend to php

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

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

לפרטים נוספים שווה להעיף מבט בפוסט הפרסום שלהם שכולל גם גיפים: https://cursor.com/blog/browser-visual-editor

פתרון AOC 2025 יום 4 (ומה קרה להערות)

12/12/2025

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

..@@.@@@@.
@@@.@.@.@@
@@@@@.@.@@
@.@@@@..@.
@@.@@@@.@@
.@@@@@@@.@
.@.@.@.@@@
@.@@@.@@@@
.@@@@@@@@.
@.@.@@@.@.

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

class Day4
  attr_accessor :map

  def initialize(fname)
    @map = {}

    File.read(fname).lines.each_with_index do |line, row|
      line.strip.chars.each_with_index do |char, column|
        map[[row, column]] = char
      end
    end
  end

  def part1
    map
      .filter {|k, v| v == "@" }
      .map {|k, v| [k, count_taken_neighbors(k), count_taken_neighbors(k) < 4 ] }
      .count {|(_, _, c)| c }
  end

  def part2
    removed = 0

    loop do
      next_remove_round = removable.keys
      break if next_remove_round.empty?

      removed += next_remove_round.size
      self.map = map.except(*next_remove_round)
    end

    removed
  end

  def removable
    map
      .filter {|k, v| v == "@" && count_taken_neighbors(k) < 4 }
  end

  def count_taken_neighbors((row, column))
    map.values_at(*neighbor_indexes(row, column)).filter {|v| v == "@" }.count
  end

  def neighbor_indexes(row, column)
    [
      [row-1, column-1],
      [row-1, column],
      [row-1, column+1],
      [row, column-1],
      [row, column+1],
      [row+1, column-1],
      [row+1, column],
      [row+1, column+1],
    ]
  end
end

if __FILE__ == $PROGRAM_NAME
  d = Day4.new('input.txt')
  pp d.part1
  pp d.part2
end

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

עכשיו לשאלה השניה מהכותרת - מה עם הערות? כל אחד חושב שהקוד שלו הוא שירה אבל האמת שרובכם בטח קוראים את הרובי הזה ולא מבינים מה רציתי מכם, מה זה הכוכבית בשם הפרמטר ל except, מה ההבדל בין size ל count, מה כל הקווים האנכיים ומהם המקרים הלא מטופלים. בעולם הישן הייתם יכולים להמשיך לחלום על תיעוד כי ממילא אף אחד לא כתב אותו. היום אפשר להדביק את הסקריפט הזה ל ChatGPT ולקבל את כל התמונה. זה מה שיוצא:

    # Day4
    #
    # This class loads a 2D grid of characters from an input file and performs two
    # computations ("part1" and "part2") on that grid. The grid is stored in a hash
    # mapping `[row, column]` coordinate pairs to single-character strings.
    #
    # The characters of interest are '@', which represent "taken" cells. The script
    # is effectively analyzing a grid of occupied/unoccupied positions and applying
    # neighbor-based rules to count or remove cells.
    #
    class Day4
      # @!attribute [rw] map
      #   @return [Hash{Array(Integer,Integer) => String}]
      #   A hash where keys are [row, column] coordinate pairs and values are
      #   characters from the input grid. This is the central data structure on
      #   which all computations operate.
      attr_accessor :map

      # Create a new grid from a filename.
      #
      # @param fname [String] path to the input file
      #
      # Reads the file line by line, strips whitespace, and inserts each character
      # into the `map` hash keyed by its (row, column) position. This establishes
      # the full input state for later analysis.
      def initialize(fname)
        @map = {}

        File.read(fname).lines.each_with_index do |line, row|
          line.strip.chars.each_with_index do |char, column|
            map[[row, column]] = char
          end
        end
      end

      # Compute Part 1's result.
      #
      # Part 1 counts how many '@' cells have **fewer than four '@' neighbors**.
      # The logic:
      #   1. Filter to only "taken" cells (value == '@')
      #   2. For each such cell, compute the number of '@' neighbors
      #   3. Count how many of those have < 4 neighbors
      #
      # @return [Integer] number of '@' cells with fewer than four '@' neighbors
      def part1
        map
          .filter { |_, v| v == "@" }
          .map { |k, v| [k, count_taken_neighbors(k), count_taken_neighbors(k) < 4] }
          .count { |(_, _, needs_removal)| needs_removal }
      end

      # Compute Part 2's result.
      #
      # Part 2 repeatedly removes removable '@' cells until none remain.
      # A cell is removable if it has fewer than four '@' neighbors (via #removable).
      #
      # This simulates a stabilizing pruning process: at each round, all currently
      # removable '@' cells are removed simultaneously. The process continues until
      # no '@' cell qualifies for removal. The method returns the number of cells
      # removed across all rounds.
      #
      # @return [Integer] total number of removed '@' cells
      def part2
        removed = 0

        loop do
          next_remove_round = removable.keys
          break if next_remove_round.empty?

          removed += next_remove_round.size
          self.map = map.except(*next_remove_round)
        end

        removed
      end

      # Determine which '@' cells are currently removable.
      #
      # A cell is removable if:
      #   - Its value is '@'
      #   - It has fewer than four '@' neighbors
      #
      # Used both by part1 (conceptually identical criteria) and part2 (for iterative
      # removal rounds).
      #
      # @return [Hash] subset of `map` containing only removable '@' entries
      def removable
        map
          .filter { |k, v| v == "@" && count_taken_neighbors(k) < 4 }
      end

      # Count how many neighbors of a given cell are '@'.
      #
      # @param row_column [Array(Integer,Integer)] the (row, column) location
      #
      # Uses #neighbor_indexes to obtain adjacent coordinates in an 8-connected
      # neighborhood, then counts the ones whose value in the map is '@'.
      #
      # @return [Integer] the number of occupied ('@') neighbors
      def count_taken_neighbors((row, column))
        map
          .values_at(*neighbor_indexes(row, column))
          .count { |v| v == "@" }
      end

      # Compute the 8 neighboring coordinates around (row, column).
      #
      # This includes diagonals:
      #
      #   (-1,-1) (-1,0) (-1,+1)
      #   ( 0,-1)   X    ( 0,+1)
      #   (+1,-1) (+1,0) (+1,+1)
      #
      # The returned coordinates may or may not exist in the map; callers handle
      # missing keys implicitly via Hash#values_at returning nil.
      #
      # @return [Array<Array(Integer,Integer)>] list of neighbor coordinate pairs
      def neighbor_indexes(row, column)
        [
          [row - 1, column - 1],
          [row - 1, column],
          [row - 1, column + 1],
          [row,     column - 1],
          [row,     column + 1],
          [row + 1, column - 1],
          [row + 1, column],
          [row + 1, column + 1],
        ]
      end
    end

    # If this file is run directly, load the puzzle input and print Part 1 and Part 2 solutions.
    if __FILE__ == $PROGRAM_NAME
      d = Day4.new('input.txt')
      pp d.part1
      pp d.part2
    end

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

על מה להסתכל

11/12/2025

כשאני בוחן טכנולוגיה או ספריה חדשה לשילוב בפרויקט הדבר המעניין לבדוק הוא אף פעם לא האם "אפשר להשתמש בזה". כמעט כל ספריה אפשר לשלב בפרויקט ולהסתדר. זה מה שכל כך מבלבל אנשים בהתלבטויות בין ריאקט ל Vue, בין node ל deno, בין פייתון לרובי. הנה כמה שאלות יותר מעניינות ששווה לחקור:

  1. כמה טוב הספריה גדלה. משווים בין פרויקטים גדולים ובשלים בכל אחת מהספריות כדי להבין האם פרויקטים ב X נוטים להיות יותר קלים לתחזוקה מפרויקטים דומים ב Y.

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

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

  4. כמה מפתחים אני יכול למצוא לטכנולוגיה זו. כמה קל יהיה לי לגייס מפתחים לפרויקט.

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

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

פתרון בית ספר

10/12/2025

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

הבעיה היא שבמקום לקבל מערכות יותר טובות אנחנו רק מרדדים את השיח.

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

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

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

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