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

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

חדש ב node: בדיקה אם זה הסקריפט הראשי

17/06/2025

הנה פיצ'ר מפייתון שלא היה חסר לי במקומות אחרים ובכל זאת נכנס לאחרונה ל node בגירסה 24.2 - האפשרות לבדוק אם המודול רץ מתוך התוכנית הראשית או בתור מודול שמיובא לתוכנית אחרת. ניקח לדוגמה את הקוד הבא:

export function foo() {
  return 'Hello, world';
}

function main() {
  const message = foo();
  console.log(message);
}

if (import.meta.main) {
  main();
}

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

node main.mjs

דרך שניה היא מתוך קובץ אחר לייבא פונקציה לדוגמה מתוך קובץ JavaScript אחר כותבים:

import {foo} from './main.mjs';

console.log(foo());

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

import assert from 'node:assert/strict';
import { test } from 'node:test';

export function twice(x) {
  return x * 2;
}

if (import.meta.main) {
  test('twice(3) == 6', () => {
    assert.equal(twice(3), 6);
  });
}

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

חישוב סטטיסטיקות מונים עם DOM

16/06/2025

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

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

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

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

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app">
      <div class="counter-group">
        <counter-stats></counter-stats>
        <counter-component></counter-component>
        <counter-component></counter-component>
        <counter-component></counter-component>
      </div>
    </div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

בשביל לחשב את הסטטיסטיקות קומפוננטת counter-stats צריכה בסך הכל לטייל צעד אחד למעלה בעץ ל counter-group ומשם לחפש את כל הילדים שהם counter-component ולקרוא את מספר הלחיצות שלהם. זאת הפונקציה:

  updateStats() {
    if (!this.counterGroup) return;

    // Get all counter components from the parent group
    const counters = this.counterGroup.querySelectorAll('counter-component');
    const counts = Array.from(counters).map(counter => {
      return parseInt(counter.dataset.count) || 0;
    });

    if (counts.length === 0) {
      this.updateStatValues(0, 0);
      return;
    }

    const minValue = Math.min(...counts);
    const maxValue = Math.max(...counts);

    this.updateStatValues(minValue, maxValue);
  }

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

// Emit custom event for stats tracking
this.dispatchEvent(new CustomEvent('counter:update-count', {
  bubbles: true,
  detail: { count: this.count }
}));

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

הקוד המלא לשתי הקומפוננטות הוא:

class CounterComponent extends HTMLElement {
  constructor() {
    super();

    // Create shadow DOM with open mode for styling
    this.attachShadow({ mode: 'open' });

    // Initialize count from data attribute or default to 0
    this.count = parseInt(this.dataset.count) || 0;

    this.render();
    this.setupEventListeners();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: inline-block;
          margin: 10px;
        }
      </style>
      <div class="counter-container">
        <button class="counter-button">Click me!</button>
        <p class="counter-text">
          Count: <span class="counter-value">${this.count}</span>
        </p>
      </div>
    `;
  }

  setupEventListeners() {
    const button = this.shadowRoot.querySelector('.counter-button');
    button.addEventListener('click', () => this.incrementCounter());
  }

  incrementCounter() {
    this.count++;

    // Update the data attribute to persist state in DOM
    this.dataset.count = this.count;

    // Update only the counter value span, not the entire component
    const valueSpan = this.shadowRoot.querySelector('.counter-value');
    valueSpan.textContent = this.count;

    // Emit custom event for stats tracking
    this.dispatchEvent(new CustomEvent('counter:update-count', {
      bubbles: true,
      detail: { count: this.count }
    }));
  }

  // Called when component is connected to DOM
  connectedCallback() {
    // Update count from data attribute if it changed
    const dataCount = parseInt(this.dataset.count);
    if (!isNaN(dataCount) && dataCount !== this.count) {
      this.count = dataCount;
      const valueSpan = this.shadowRoot.querySelector('.counter-value');
      if (valueSpan) {
        valueSpan.textContent = this.count;
      }
    }

    // Emit initial event for stats tracking
    this.dispatchEvent(new CustomEvent('counter:update-count', {
      bubbles: true,
      detail: { count: this.count }
    }));
  }

  // Observe data-count attribute changes
  static get observedAttributes() {
    return ['data-count'];
  }

  // Handle attribute changes
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'data-count' && this.shadowRoot) {
      const newCount = parseInt(newValue) || 0;
      if (newCount !== this.count) {
        this.count = newCount;
        const valueSpan = this.shadowRoot.querySelector('.counter-value');
        if (valueSpan) {
          valueSpan.textContent = this.count;
        }

        // Emit custom event for stats tracking
        this.dispatchEvent(new CustomEvent('counter:update-count', {
          bubbles: true,
          detail: { count: this.count }
        }));
      }
    }
  }
}

// Define the custom element
customElements.define('counter-component', CounterComponent);

// Counter Stats Component
class CounterStatsComponent extends HTMLElement {
  constructor() {
    super();

    // Create shadow DOM with open mode for styling
    this.attachShadow({ mode: 'open' });

    this.render();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          margin: 10px;
          padding: 15px;
          border: 2px solid #ccc;
          border-radius: 8px;
          background-color: #f9f9f9;
          font-family: Arial, sans-serif;
        }
        .stats-container {
          display: flex;
          gap: 20px;
          justify-content: center;
        }
        .stat-item {
          text-align: center;
        }
        .stat-label {
          font-weight: bold;
          color: #555;
          margin-bottom: 5px;
        }
        .stat-value {
          font-size: 1.2em;
          color: #333;
          font-weight: bold;
        }
      </style>
      <div class="stats-container">
        <div class="stat-item">
          <div class="stat-label">Min Value:</div>
          <div class="stat-value min-value">0</div>
        </div>
        <div class="stat-item">
          <div class="stat-label">Max Value:</div>
          <div class="stat-value max-value">0</div>
        </div>
      </div>
    `;
  }

  connectedCallback() {
    // Find the parent .counter-group element
    this.counterGroup = this.closest('.counter-group');

    if (this.counterGroup) {
      // Listen for counter update events
      this.counterGroup.addEventListener('counter:update-count', this.handleCounterUpdate.bind(this));

      // Calculate initial stats
      this.updateStats();
    }
  }

  disconnectedCallback() {
    if (this.counterGroup) {
      this.counterGroup.removeEventListener('counter:update-count', this.handleCounterUpdate.bind(this));
    }
  }

  handleCounterUpdate(event) {
    // Update stats whenever any counter changes
    this.updateStats();
  }

  updateStats() {
    if (!this.counterGroup) return;

    // Get all counter components from the parent group
    const counters = this.counterGroup.querySelectorAll('counter-component');
    const counts = Array.from(counters).map(counter => {
      return parseInt(counter.dataset.count) || 0;
    });

    if (counts.length === 0) {
      this.updateStatValues(0, 0);
      return;
    }

    const minValue = Math.min(...counts);
    const maxValue = Math.max(...counts);

    this.updateStatValues(minValue, maxValue);
  }

  updateStatValues(min, max) {
    // Update only the specific value spans, not the entire element
    const minSpan = this.shadowRoot.querySelector('.min-value');
    const maxSpan = this.shadowRoot.querySelector('.max-value');

    if (minSpan) minSpan.textContent = min;
    if (maxSpan) maxSpan.textContent = max;
  }
}

// Define the custom element
customElements.define('counter-stats', CounterStatsComponent);

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

https://tocode.ravpage.co.il/tocodeai

הזמנה לוובינר: פיתוח פיצ'רים מלאים עם AI

15/06/2025

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

אנחנו נראה איך ליישם את העקרונות של Prompt Engineering בחיים שלנו כמפתחים עם קופיילוט:

  1. איך לארגן מראש את הפרויקט כדי שאפשר יהיה להוסיף אליו פיצ'רים מהר עם AI.

  2. איך להשתמש בקונטקסט המתאים בפרומפט כדי לכוון את ה AI למימוש שאני רוצה שהוא יכתוב.

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

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

https://tocode.ravpage.co.il/tocodeai

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

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

כמה זמן לוקח להתקין ספריה מ npm?

14/06/2025

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

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

npm add moment

ולא תצטרכו לכתוב את הלוגיקה המסובכת לבד.

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

  1. "כמה הספריה עולה" כלומר מה הגודל שהיא מוסיפה לגודל הבאנדל או כמה זמן היא מוסיפה לזמן טעינת העמוד?

  2. מה האלטרנטיבות? איזה ספריות אחרות עושות עבודה דומה? למה החלטתי דווקא על הספריה הזו? לדוגמה ספריית day.js מציעה אלטרנטיבה קטנה בהרבה מ moment עם אותו API.

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

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

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

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

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

הבעיה של ריאקט ו State

13/06/2025

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

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

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

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

ניקח דוגמה פשוטה של מונה לחיצות:

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div className="counter" >
      <p>Count = {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  )
}

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

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import Counter from './Counter'
import CounterGroup from './CounterGroup'
function App() {

  return (
    <CounterStatistics />
    <Counter />
    <Counter />
    <Counter />
  )
}

export default App

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

function App() {
  const [counterValues, inc] = useCounters(3);
  return (
    <>
      <CountersStats values={counterValues} />      
      <Counter value={counterValues[0]} inc={inc(0)} />
      <Counter value={counterValues[1]} inc={inc(1)} />
      <Counter value={counterValues[2]} inc={inc(2)} />
    </>
  )
}

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

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

function App() {
  const [min, setMin] = useState(0);
  const [max, setMax] = useState(0);

  function update(value) {
    setMin(Math.min(min, value));
    setMax(Math.max(max, value));
  }

  return (
    <>
      <CountersStats />
      <Counter onChange={update} />
      <Counter onChange={update} />
      <Counter onChange={update} />
    </>
  )
}

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

מה דעתכם?

בלי ריאקט

12/06/2025

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

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

if (this.miniPlayerValue) {
  this.playButtonTarget.textContent = '❚❚';
} else {
  this.playButtonTarget.classList.add('hidden');
}

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

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

ספרדית עם השיר El principio de algo https://www.langlets.app/courses/el-principio-de-algo

צרפתית עם Et si tu n'existais pas https://www.langlets.app/lessons/et-si-tu-n-existais-pas0

אנגלית עם מיילי סירוס https://www.langlets.app/lessons/flowers0

וערבית עם ג'ואן ספדי https://www.langlets.app/lessons/haifa-jenin0

אבטחת מידע בעידן ה AI? אולי זה לא כזה שונה

11/06/2025

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

  1. הזרקת פרומפט
  2. חשיפת מידע רגיש
  3. פגיעה בשרשרת האספקה
  4. הרעלת מידע
  5. שימוש בפלט מ LLM בלי לנקות אותו קודם
  6. עודף הרשאות לסוכן
  7. שימוש במידע סודי בפרומפט
  8. הזרקת פרומפט במנועי RAG
  9. ניצול לרעה של ההזיות
  10. מניעת שירות באמצעות שאילתות שייתקעו את ה LLM או יבזבזו הרבה טוקנים.

רוב הפריטים פה אמורים להיראות לכם מאוד מוכרים!

הזרקת פרומפט היא גלגול חדש של הזרקת SQL והזרקת JavaScript (ולזקנים - ניצול Buffer Overflow לצורך הזרקת קוד). חשיפת מידע רגיש זה בדיוק המערכת שלא בודקת זהות לפני שמאפשרת גישה לנתיב פרטי. שרשראות האספקה הן תוהו ובוהו כבר יותר מדי שנים ומזמן למדנו שלפני ששולחים משהו לפקודת Shell צריך לנקות אותו.

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

עם כל החוכמה של הבינה המלאכותית, בנושא אבטחת מידע אין הרבה חדש תחת השמש. לא סומכים על אף אחד ובמיוחד לא על LLM.

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

10/06/2025

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

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

https://github.com/settings/tokens

  1. בוחרים מודל שמעניין אתכם מרשימת המודלים כאן:

https://github.com/marketplace?type=models

  1. במסך המודל לוחצים Use this model ואז גוללים למטה כדי להבין מה שם המודל (זה התוכן של המשתנה model בדוגמת הקוד שמופיעה על המסך).

  2. מתחברים למודל ומתחילים לקשקש עם הקוד הבא (טייפסקריפט):

import { generateText } from 'npm:ai';
import { createOpenAI } from 'npm:@ai-sdk/openai';

const ai = createOpenAI({
  apiKey: Deno.env.get('GITHUB_TOKEN'),
  baseURL: 'https://models.github.ai/inference',
});

const { text } = await generateText({
  model: ai.chat('openai/gpt-4.1'),
  prompt: 'Write a vegetarian lasagna recipe for 4 people.',
});

console.log(text);

בשביל שזה יעבוד יצרתי קובץ .env עם המפתח שלי והרצתי משורת הפקודה:

deno run --env-file=.env --allow-all demo.ts

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

רוצים להמשיך ללמוד על AI? אתם לא לבד. בימי חמישי בבוקר אנחנו נפגשים כל שבוע לדבר על נושא אחר מהתחום. השבוע נדבר על Code Reviews באמצעות AI. פרטים וכניסה בעמוד:

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

אנטי תבניות ו AI

09/06/2025

אנטי-תבניות ו AI זה לא שילוב טוב.

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

p = Person.find(7)

# new DB query
p.addresses.first

# new DB query
p.addresses.second

# new DB query
p.addresses.third

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

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

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

Code review my last commit:
{commit_text}

Refer to the following aspects:
    1. Security - it's a public facing system
    2. Performance - both load times and interaction time
    3. Architecture - code is core part of a big system and will be used by many developers
    4. Newer or better technology alternatives that weren't used
    5. Anti patterns in the code (for example N+1 queries)

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

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

האם פרומפט הוא טקסט טוב להודעת קומיט?

08/06/2025

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

Ask Claude to merge the auth code into the grant records.
Prompt: Let's give auth codes the same treatment we did refresh tokens, that is, store them in the grants table rather than separately. So, an auth code should have the format `{userId}:{grantId}:{randomSecret}` just like tokens do, and we should store the auth code hash inside the grant record. We should also make the grant record itself expire (by setting a TTL on the KV record) if the auth code is not claimed within (by default) ten minutes. But, once it is claimed and exchanged for a refresh token, then it no longer expires.

Once again there was some sort of technical glitch where Claude's changes to `completeAuthorization()` were not applied, so I had to ask it to read them out and copy/paste manually.

Also Claude did not fully update the doc, so I tried an additional prompt:

Additional prompt: You updated the storage schema to show that authCodeId is now part of grant: records, but you didn't remove the documentation of auth_code: records. Since those no longer exist, can you please remove that from the doc?

But Claude just edited some other parts without actually removing the docs about auth_code. So I will remove it myself in the next commit.

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

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

אני כן אשמח לראות את הפרומפטים מצורפים ל git repo אבל לא במקום הודעות קומיט. אני חושב שמנגנון כמו git-notes יכול לעבוד טוב יותר מהודעת הקומיט עצמה, ואז כלי כמו aider יוכל באופן אוטומטי אחרי כל קומיט להוסיף note עם כל פירוט השיחה שהובילה לקומיט זה, ואולי אפשר גם לכתוב תוספים ל VS Code או Cursor שיוכלו לקחת את כל השיחות שבוצעו בין שני קומיטים ולהפוך אותן ל note לקומיט האחרון.