diff --git a/GUIDES/code_quality_analysis.md b/GUIDES/code_quality_analysis.md new file mode 100644 index 000000000..13df7c5bb --- /dev/null +++ b/GUIDES/code_quality_analysis.md @@ -0,0 +1,538 @@ +# אנליזת איכות קוד — CodeBot (CodeKeeper) + +> תאריך: 2026-09-14 | סה"כ באגים מתועדים: 74 | טווח ההיסטוריה: 2025-08-06 ← 2026-09-14 + +--- + +## איך המסמך הזה נבנה (וממה להיזהר בו) + +הריפו הגיע לסשן הזה כ־clone שטוח של 50 קומיטים. הוא הועמק (`git fetch --unshallow`) ל־**2,718 קומיטים** על `main`, ומתוכם **423 קומיטים** שהם תיקון או revert. אלה לא נסרקו בעיניים אחת־אחת: הסריקה נעשתה בשלושה מעברים — כל 423 הכותרות נקראו, 87 הקומיטים שגוף ההודעה שלהם ארוך מ־1,000 תווים (הפוסט־מורטמים המפורטים של התקופה האחרונה) נקראו במלואם, וכ־45 קומיטים עם הודעה קצרה נפתחו כ־diff מלא כדי לראות מה בדיוק היה שבור. + +**כל באג כאן נראה בפועל** — או בדיף, או בגוף הקומיט שמתאר את הקוד הישן. באגים שלא הצלחתי לאמת לא נכנסו. מה שכן צריך לדעת על המסמך: + +- **הוא לא ממצה את 423 התיקונים.** חלק גדול מהם הם תיקוני CSS, תיקוני טסטים, ו־re־push של אותו branch שוב ושוב (למשל 11 קומיטים נפרדים בשם `Fix/asyncio resilient event loop policy`, כולם ב-2025-10-29). אלה לא באגים לוגיים ולא נכנסו לרשימה. +- **התקופה המוקדמת מתועדת פחות טוב מהמאוחרת.** בקומיטים מ־2025 גוף ההודעה הוא לרוב רשימת כל הקומיטים של ה־branch מעורבבים יחד, ולכן אי אפשר לייחס בעיה לשורה ספציפית. לעומת זאת מ־2026-07 ואילך כל קומיט תיקון הוא פוסט־מורטם מדויק עם מדידות. **זה לא אומר שהיו פחות באגים ב־2025 — זה אומר שפחות מהם ניתנים לתיעוד.** +- **הפילוג לפי חודשים מראה את זה במספרים:** 59 תיקונים ב־2025-08, שיא של 95 ב־2025-10, ואז ירידה ל־2 ב־2026-06 ו־18 ב־2026-09. הירידה משקפת גם פחות באגים וגם תיקונים גדולים יותר שמאגדים כמה באגים בקומיט אחד. + +מקור נוסף שנקרא: `code_review_2025_10/` (סקירה מ־2025-10-04 שיושבת בריפו), ההערות בקוד (`TODO`/`FIXME`/`DEBUG`), והריפו `amirbiron/amir-bug-patterns` — שכבר מתעד 12 דפוסים מ־CodeBot מהסשנים של אוגוסט־ספטמבר 2026. שמות הדפוסים כאן מיושרים אליו (K11, K13, K14, K15, U1, U3, H6) כדי שלא ייווצרו שני מילונים. + +--- + +## סיכום דפוסים + +| דפוס | כמות | % מסה"כ | דוגמה מרכזית | +|---|---|---|---| +| לוגיקה עסקית ומקרי קצה | 10 | 14% | `replace(tzinfo=...)` בלי `astimezone` — כל תאריך בוובאפ הוצג שעתיים אחורה בחורף ושלוש בקיץ (#3228) | +| API design — קוד מת, שכפול, חוזה שגוי | 9 | 12% | `Cursor.explain()` מעולם לא קיבל פרמטר `verbosity`; ה-`try` היה קוד מת, ה-`except` היה המסלול היחיד, וההודעה שלו תיארה את ההפך (#3333) | +| ניהול מצב, אתחול ו-concurrency (U1/K15) | 8 | 11% | `get_db` הציב את השומר `client` לפני הערך `db`; תקלת רשת חולפת אחת השאירה `None` לצמיתות עד ריסטארט (#3357) | +| ולידציית קלט וגבולות חיצוניים (U3/H6) | 8 | 11% | `gemini-embedding-001` חותך בשקט מעל 2,048 טוקנים; ה-chunker ספר שורות ולא בייטים, וחצי מהצ'אנקים בפרודקשן תיארו רק את תחילת עצמם (#3342) | +| עקביות DB וכתיבה חלקית | 7 | 9% | מחיקה מרובה סיננה לפי `_id` של הגרסה העליונה בלבד; מה שמתחתיה נשאר פעיל והקובץ חזר לרשימה ברענון (#3358) | +| async / control flow | 7 | 9% | שכבת `asyncio` מעל worker יחיד של gevent — כל בקשה חופפת ראתה את הלולאה כרצה ונפלה ב-500 (#3330) | +| DOM / UI / RTL | 7 | 9% | `startWidth + (clientX - startX)` היא נוסחת LTR; בפריסת RTL המפריד זז הפוך מהאצבע, עד 180px פער (#3286) | +| בטיחות נתונים ודליפת סודות (K13/K14) | 6 | 8% | מפתח Gemini עבר כ-`params={"key": ...}`, ואינטגרציית httpx של Sentry רשמה את שורת השאילתה בכל בקשה מוצלחת (#3270) | +| בליעת כשל בשקט (K11) | 5 | 7% | `delete_pattern` יצא ב-`return 0` כשהקאש כבוי, כלומר הפולבק המקומי היה קאש שאי אפשר לבטל — וממנו נגזרו מספרי גרסה שדרסו זה את זה (#3229) | +| סכימת DB ואינדקסים | 4 | 5% | `$ne` בתוך `partialFilterExpression` אינו נתמך במונגו — אותה טעות חזרה ארבע פעמים לפני שתועדה בקוד (#895, #2121, #2627) | +| הרשאות ובידוד בין משתמשים | 3 | 4% | `/repo/api/repos` החזיר את כל `repo_metadata` בלי שדה בעלים — כל משתמש מחובר ראה את כל הריפויים וגלש בקוד שלהם (#3239) | + +**סה"כ 74.** אין דפוס בודד ששולט: שלושת הגדולים — לוגיקה עסקית, API design, וניהול מצב — הם יחד 36%, וזנב ארוך מחלק את השאר. זה מה שמצפים לראות בפרויקט שרוחבו גדול (בוט טלגרם, ווב-אפ Flask, שרת MCP, מנוע חיפוש, שכבת תצפית) ולא בפרויקט עם באג יסודי אחד שחוזר. מה שכן בולט: **9 מתוך 74 (12%) הם הרשאות או סודות** — קטגוריה שהיא קטנה במספר אבל מחזיקה שמונה מתוך 21 ממצאי ה-MAJOR, כלומר כמעט כל מה שבה חמור. + +--- + +## רשימה מלאה לפי חומרה + +### MAJOR + +#### 1. סיסמת MongoDB בטקסט גלוי בקוד — ועדיין בריפו +- **בעיה:** `activity_reporter` נבנה עם `mongodb_uri="mongodb+srv://mumin:<סיסמה>@..."` מוקלד בקוד, בשני קבצים (`bot_handlers.py`, `conversation_handlers.py`). נכנס ב־`ccdd5f56` (2025-08-06) והוסר מקוד הפייתון רק ב־#828 (`cd32bc95`, 2025-10-17) — כלומר שהה בקוד כשני חודשים וחצי. +- **תוצאה:** גישה בלתי מורשית למסד. וחשוב מזה: **הסוד עדיין בריפו היום** — בשלושה מסמכי `FEATURE_SUGGESTIONS/*.md`, ב־`code_review_2025_10/SECURITY_FINDINGS.md` (שמצטט אותו במלואו כדי להתריע עליו), ובהיסטוריית הגיט לנצח. סיסמה שלא הוחלפה היא סיסמה חיה. +- **מקור:** נכנס `ccdd5f56`; הוסר מהקוד ב־#828; **פתוח** במסמכים ובהיסטוריה. +- **דפוס:** בטיחות נתונים ודליפת סודות + +#### 2. טוקן הבוט דלף להודעות שגיאה, ללוגים ול־Sentry +- **בעיה:** כתובות ה־API של טלגרם נבנות כ־`https://api.telegram.org/bot/method`, והכתובת המלאה נדחפה לתוך הודעת החריגה של `TelegramAPIError` וגם לשדה `url` שלה. המסנן של Sentry ניקה רק את `event["extra"]` לפי שמות שדות ולא נגע בגוף החריגה, ולוובאפ לא היה `before_send` בכלל. +- **תוצאה:** כל כשל בקריאה לטלגרם — שגיאה שכיחה לגמרי — נשא את הטוקן במלואו ללוגים ול־Sentry. אף שורת קוד לא "רשמה סוד"; הוא רכב על ה־URL. +- **מקור:** #3234 (`29371728`, 2026-08-18) +- **דפוס:** בטיחות נתונים ודליפת סודות (K13) + +#### 3. אירועי transactions של Sentry עקפו את הניקוי לחלוטין +- **בעיה:** אחרי תיקון #3234 הניקוי היה נכון — אבל `before_send` נקרא רק על אירועי **שגיאה**. אירועי ביצועים (transactions, נדגמים ב־10%/5%) נושאים את אותם URL־ים ב־spans ולא עוברים בו. +- **תוצאה:** אירוע אמיתי מפרודקשן (CODEKEEPER-2Z) הראה את הטוקן המלא בתוך breadcrumbs של httplib — אחרי שהדליפה "תוקנה". +- **מקור:** #3237 (`146b609a`, 2026-08-18) +- **דפוס:** בטיחות נתונים ודליפת סודות (K13) + +#### 4. מפתח Gemini בשורת השאילתה, ו־SDK הניטור תיעד אותו בעצמו +- **בעיה:** חמישה אתרי קריאה העבירו את המפתח כ־`params={"key": self.api_key}`. `HttpxIntegration` של Sentry נמצאת ב־`_AUTO_ENABLING_INTEGRATIONS` — היא נדלקת מעצמה כש־httpx מותקן, בלי להופיע בקונפיגורציה — וקוראת ל־`parse_url(..., sanitize=False)`, לא מותנה בשום דגל. +- **תוצאה:** המפתח יצא ל־Sentry **בכל בקשה מוצלחת**, לא רק בכשל. ורשימת דפוסי הניקוי הייתה בגודל אחד (רג'קס של טוקן טלגרם), כלומר כל ספק חדש היה דליפה חדשה. +- **מקור:** #3270 (`24ca4f52`, 2026-08-25) +- **דפוס:** בטיחות נתונים ודליפת סודות (K14) + +#### 5. `safe_rmtree` בדק קידומת מחרוזת ולא גבול נתיב +- **בעיה:** המתכון שהמדיניות בריפו מורה להעתיק בדק `str(p).startswith(str(base))`. זו השוואת תווים ולא נתיב: עבור `allow_under=/tmp/app-test`, המחרוזת `/tmp/app-test-evil` עוברת את הבדיקה. +- **תוצאה:** `shutil.rmtree` מוחק תיקייה שלמה שמחוץ ל־allowlist. שוחזר אמפירית לפני התיקון — נתיב־אח נמחק בפועל. החומרה כאן כפולה, כי זה היה **המתכון שהמדיניות מורה להעתיק**, כלומר כל שימוש עתידי היה יורש את החור. +- **מקור:** #3249 (`2fef6461`, 2026-08-20) — התיקון הוא `p == base or base in p.parents` +- **דפוס:** בטיחות נתונים ודליפת סודות + +#### 6. Gist של משתמש נוצר תחת חשבון המערכת, ובאופן ציבורי +- **בעיה:** `GitHubGistIntegration` נבנה כ־singleton בזמן import עם ה־`GITHUB_TOKEN` הגלובלי — הטוקן של החשבון שמריץ את הבוט. הבוט כבר החזיק טוקן אישי לכל משתמש (`db.get_github_token`), וכל שאר פעולות ה־GitHub עברו דרכו; רק הגיסט היה יוצא דופן. +- **תוצאה:** קוד של משתמשים התפרסם בפומבי תחת חשבון אחד, ולא תחת החשבון של מי שיצר אותו. +- **מקור:** #3241 (`e22ebdf9`, 2026-08-19) +- **דפוס:** הרשאות ובידוד בין משתמשים + +#### 7. דפדפן הקוד היה פתוח לכל משתמש מחובר +- **בעיה:** `/repo/api/repos` שלף את כל `repo_metadata` בלי שום סינון, ולקולקציה הזו אין בכלל שדה בעלים. הקישורים בתפריטים הוסתרו ללא־אדמין, אבל ה־routes עצמם היו פתוחים לכל מי שידע את הכתובת. +- **תוצאה:** כל משתמש מחובר יכול היה לראות את רשימת כל הריפויים במערכת (שמות ו־URLs, כולל פרטיים) ולגלוש בקוד של ריפו אחר דרך `?repo=`. 18 routes חשופים. +- **מקור:** #3239 (`8098b67e`, 2026-08-19) — נסגר ב־`before_request` אחד ברמת ה־blueprint, כדי שלא תיפתח דלת בשכחה של דקורטור +- **דפוס:** הרשאות ובידוד בין משתמשים + +#### 8. גיבויי ZIP פרטיים היו נגישים חוצה־משתמשים +- **בעיה:** רשימת הגיבויים והמחיקה שלהם לא תוחמו למשתמש המבקש. ההערה בקוד אמרה זאת במפורש: "ודא שתמיד מוצגים כל קבצי ה־ZIP ללא סינון לפי משתמש". +- **תוצאה:** משתמש ראה — ויכול היה למחוק — גיבויי ZIP של משתמשים אחרים. הגיבויים מכילים את כל הקוד שנשמר. +- **מקור:** #552 (`8ea0bdb4`, 2025-09-17) +- **דפוס:** הרשאות ובידוד בין משתמשים + +#### 9. מחיקה מרובה הורידה לסל גרסה אחת במקום את הקובץ +- **בעיה:** קובץ שהמשתמש רואה הוא `(user_id, file_name)`, אבל במסד כל גרסה היא מסמך נפרד. כל מסך רשימה מקבץ לפי `file_name` ומוסר לממשק את ה־`_id` של הגרסה האחרונה בלבד, ו־`POST /api/files/bulk-delete` סינן בדיוק לפי המזהה הזה. אותו באג היה גם בבוט, ושם `delete_file_by_id` גם **לא תחם למשתמש כלל**. השורש: השאילתה הייתה כתובה בשלושה עותקים, והוובאפ כלל לא עבר דרך `database/repository.py`. +- **תוצאה:** הגרסה העליונה ירדה לסל, מה שמתחתיה נשאר פעיל, והקובץ חזר לרשימה ברענון — גרסה אחת אחורה. קובץ עם גרסה יחידה נעלם סופית, ולכן שני קבצים שנמחקו באותה פעולה התנהגו שונה. הצ'אנקים הסמנטיים נשארו מאונדקסים לגרסאות ישנות בזמן שהקובץ בסל. +- **מקור:** #3358 (`15fbf49c`, 2026-09-08). הבאג אינו רגרסיה — המסנן קיים מאז שהראוט נולד (#759) +- **דפוס:** עקביות DB וכתיבה חלקית + +#### 10. אובדן תוכן בפתקים הדביקים — שני שורשים נפרדים +- **בעיה:** (א) התצוגה רינדרה **רק** שורות צ'קבוקס והסתירה את הטקסטריה, בלי שום דרך חזרה; (ב) השרת חתך ל־5,000 תווים **בשקט**. +- **תוצאה:** פתק עם 400 שורות ושורת משימה אחת נראה כאילו נמחק, והפך לקריאה־בלבד לתמיד. הדבקה של 16,291 תווים נשמרה כ־5,000, בלי אזהרה. הרינדור עצמו הוכח כלא־מסלול־כתיבה (אחרי blur התוכן המלא עדיין בטקסטריה ואפס בקשות כתיבה יצאו), ולכן שני התסמינים נראו כבאג אחד והיו שניים. +- **מקור:** #3259 (`94ee3b11`, 2026-08-21) — שמונה באגים בסך הכול, 13 מתוך 15 הבדיקות נופלות על הקוד הקודם +- **דפוס:** ולידציית קלט וגבולות חיצוניים + +#### 11. קאש שאי אפשר לבטל — ומספרי גרסה שדרסו זה את זה +- **בעיה:** הדקורטור `cached` נופל לקאש מקומי בזיכרון כש־Redis אינו זמין, אבל כל פונקציות הביטול עברו דרך `delete_pattern` שיצא מיד ב־`return 0` כשהקאש כבוי. כלומר הפולבק היה קאש שאי אפשר לבטל: ערך ישן שרד עד ל־TTL (180 שניות) גם אחרי כתיבה מוצלחת. +- **תוצאה:** `save_code_snippet` גזר את מספר הגרסה הבאה מאותו ערך מקוּש וחישב שוב ושוב 1+1=2; `edit_file` קרא את גוף הקובץ מאותו קאש, ולכן כל עריכה נבנתה מחדש על גבי גרסת הבסיס. עריכה דרך ה־MCP החזירה `ok:true` בזמן שהקובץ הפעיל לא השתנה, ובהיסטוריה נוצרו כמה רשומות עם אותו מספר גרסה. +- **מקור:** #3229 (`48dba2d6`, 2026-08-14). זה המופע השלישי של K11 באותו ריפו +- **דפוס:** בליעת כשל בשקט (K11) + +#### 12. `get_db` פרסם את השומר לפני הערך שהוא שומר עליו +- **בעיה:** `get_db` מחזיק שני גלובלים, `client` ו־`db`, ומשתמש ב־`client` כשומר של המסלול המהיר (`if client is None`). הקוד הציב את השומר לפני הערך. +- **תוצאה:** שני מסלולים, שניהם שוחזרו בהרצה. (א) **מרוץ:** `server_info()` הוא סיבוב רשת שלם, וקורא מקביל שנכנס באמצעו קיבל `None`. (ב) **הרעלה קבועה, החמורה:** כשל ב־`server_info()` השאיר את `client` מוצב על לקוח שבור, וכל קריאה עתידית דילגה על האתחול והחזירה `None` — **תקלת רשת חולפת אחת שיתקה את התהליך עד ריסטארט**. התסמין אינו חריגה: נתיבים כמו search מסתעפים על `if db is None`, כלומר המערכת מדווחת "מסד לא זמין" ומדרדרת בשקט בזמן שהמסד בריא. 211 קריאות ל־`get_db` בקוד הייצור, 96 מהן בלי `try`. +- **מקור:** #3357 (`cf758954`, 2026-09-08). `webapp/push_api.py` — תהליכון דמון עם `while True` שקורא ל־`get_db` בכל סבב — הוא הקורא המקביל שבזכותו המרוץ לא היה תיאורטי +- **דפוס:** ניהול מצב, אתחול ו־concurrency (K15) + +#### 13. חיתוך snippet בבייטים על תוכן עברי +- **בעיה:** `$regexFind` מחזיר את `idx` כאינדקס **תווים**, והקוד הזין אותו ל־`$substrBytes`, שמצפה ל**בייטים**. באנגלית שתי היחידות זהות ולכן זה עבר סקירה; בעברית כל אות היא שני בייטים, גבול החיתוך נוחת באמצע תו, ומונגו זורקת ומפילה את כל האגרגציה. +- **תוצאה:** בשני המוקדים הכשל היה שקט. ב**חיפוש** — הצינור המהיר מת, הקוד ירד לסריקת `$regex` מלאה בלי אינדקס (שנמדדה כשאילתה האיטית ביותר בלוח ושלחה את החקירה לכיוון הלא נכון), וכשגם היא נפלה המשתמש קיבל "לא נמצאו תוצאות" על חיפוש שמעולם לא רץ. ב**שיתוף קובץ** — ה־`except` בלע את החריגה והתשובה הייתה 404 "קובץ לא נמצא" על קובץ שקיים, בלי שום שורת לוג. +- **מקור:** #3357 (`cf758954`, 2026-09-08), אישו #3353. שלושה סבבי חקירה עד שהשורש נחשף +- **דפוס:** ולידציית קלט וגבולות חיצוניים (H6) + +#### 14. הפרודקשן קרס בלופ — ייבוא שהוקדם לפני הכנת `sys.path` +- **בעיה:** גוניקורן טוען `app:app` מתוך `webapp/`, כלומר `sys.path[0]` הוא `webapp/` ולא שורש הפרויקט. הקובץ פותר את זה בעצמו בשורה 159 (`sys.path.insert(0, ROOT_DIR)`), עם שתי הערות שאומרות את זה במפורש. הייבוא הועבר לשורה 21 — לפני ההכנה — כדי לספק את ה־linter (E402) במקום להוסיף `noqa`. +- **תוצאה:** `ModuleNotFoundError: No module named 'sticky_notes_target'` בכל boot של גוניקורן, בלולאה. הווב נפל. +- **מקור:** #3260 (`98a1e5b9`, 2026-08-21) +- **דפוס:** ניהול מצב, אתחול ו־concurrency + +#### 15. `explain(verbosity=...)` — פרמטר שאינו קיים, ו־`try` שהוא קוד מת +- **בעיה:** התפריט "Explain Verbosity" בדשבורד `/admin/profiler` לא עשה כלום. הערך נשלח לשרת, הגיע לשירות ונזרק. השורש: `get_explain_plan` קראה `cursor.explain(verbosity=...)`, אבל ל־`Cursor.explain` אין ומעולם לא היה פרמטר כזה — החתימה ב־pymongo 4.15.3 היא `def explain(self)`, והדוקסטרינג אומר במפורש שהיא משתמשת ב־`allPlansExecution`. +- **תוצאה:** ה־`try` היה קוד מת, ה־`except` היה המסלול היחיד שרץ אי פעם, וההודעה שלו ("falling back without execution stats") תיארה את ההפך ממה שקרה. מונגו הריצה תמיד `allPlansExecution` — האפשרות שכתוב לידה "debug בלבד" — גם כשנבחרה "queryPlanner (בטוחה)". גם רעש ה־traceback בלוגי הפרודקשן הגיע משם. שימו לב שקדם לכך #2939, שתיקן את הקריאה מ־positional ל־keyword — **טלאי על API שלא קיים**. +- **מקור:** #3333 (`64a37d0f`, 2026-09-06); הטלאי המקדים ב־#2939 (`84583622`) +- **דפוס:** API design — קוד מת, שכפול, חוזה שגוי + +#### 16. כל התאריכים בוובאפ הוצגו שעתיים־שלוש אחורה +- **בעיה:** מונגו שומר זמנים כ־UTC בלי תווית אזור זמן. הקוד הדביק את התווית (`replace`) אבל מעולם לא הזיז את השעון (`astimezone`). +- **תוצאה:** כל תאריך בוובאפ שגוי — עמוד הקובץ, היסטוריית הגרסאות, השוואת גרסאות, רשימת הקבצים, סל המחזור, הדשבורד, הטיימליין, דוחות האדמין ועמוד השיתוף הציבורי. שעתיים בחורף ושלוש בקיץ. +- **מקור:** #3228 (`241832e5`, 2026-08-14) +- **דפוס:** לוגיקה עסקית ומקרי קצה + +#### 17. `created_at` התאפס בכל עריכה +- **בעיה:** כל עריכה יוצרת מסמך חדש ב־`code_snippets`, וכל מסמך כזה נולד עם `created_at` טרי. מכיוון שכל מסך קורא את הגרסה האחרונה, "נוצר" הציג בפועל את זמן העריכה האחרונה. +- **תוצאה:** תאריך היצירה של קובץ אבד לגמרי — בוובאפ, במסך המידע של הבוט וב־MCP. בנוסף, קובץ שמעולם לא נערך קיבל `created_at` ו־`updated_at` שנבעו משלוש קריאות נפרדות ל־`datetime.now()`, ולכן שמירה שנפלה על גבול הדקה הציגה "עודכן" על קובץ טרי. +- **מקור:** #3306 (`b78e04b4`, 2026-09-01) — הכלל רוכז ב־`inherited_created_at`, והוחל גם על `large_files` שבו השמירה מוחקת ומכניסה מחדש +- **דפוס:** עקביות DB וכתיבה חלקית + +#### 18. צ'אנקים של החיפוש הסמנטי תיארו רק את תחילת עצמם +- **בעיה:** ל־`gemini-embedding-001` תקרת קלט של 2,048 טוקנים, והוא חותך בשקט כל מה שמעבר — בלי שגיאה ובלי אזהרה. ה־chunker ספר 220 שורות בלי לבדוק כמה טקסט יצא בפועל. +- **תוצאה:** חצי מהצ'אנקים בפרודקשן חצו את הסף, והווקטור שלהם תיאר רק את ההתחלה שלהם — **סוף של קובץ ארוך פשוט לא נמצא בחיפוש**. נמדד על הקוד הישן: שורת SVG אחת הפכה לצ'אנק של 80,000 בייט, ומארקדאון עברי צפוף ל־41,799. בנוסף, כשל ב־retry השאיר צ'אנקים יתומים עם `chunk_count=0`. +- **מקור:** #3342 (`190a63bd`, 2026-09-06); התיקון החלקי הקודם ב־#3059 +- **דפוס:** ולידציית קלט וגבולות חיצוניים (U3) + +#### 19. תהליך הגיבוי רץ גם בתהליך הבוט +- **בעיה:** `webapp/app.py` מפעיל את מתזמן הגיבויים בזמן import. כשה־`main.py` של הבוט מייבא משהו מ־`webapp.app` (למשל `get_db`), פייתון מריץ את כל ה־top-level של המודול — ומייצר תהליכון מתזמן כפול בתוך שירות הבוט. +- **תוצאה:** לשירות הבוט אין דיסק מתמיד ב־`/var/data`, ולכן גיבויי הדיסק נכשלו ב־`PermissionError`. שני מתזמנים במקום אחד. +- **מקור:** #3157 (`a04d613b`, 2026-04-26) +- **דפוס:** ניהול מצב, אתחול ו־concurrency + +#### 20. הבוט קרס בהפעלה על תקלת מסד חולפת +- **בעיה:** ל־`connect()` לא היה שום retry, ואחרי חלון ההמתנה של 120 שניות `main.py` עשה `SystemExit(1)`. בתוך החלון הזה נכנסו רק שני ניסיונות חיבור מחדש (t=30, t=75), כי ההשהיה הראשונה הייתה 30 שניות. +- **תוצאה:** CrashLoop וסופות Sentry על כל נפילה זמנית של Atlas — כולל האירועים הקטלניים של 2026-04-03. +- **מקור:** #3135 (`79dd3e72`, 2026-04-01) הוסיף retry עם backoff; #3155 (`752f43b7`, 2026-04-20) החליף את ה־exit בהמתנה פסיבית +- **דפוס:** async / control flow + +#### 21. route חירום שמוחק את כל האינדקסים — מאחורי `GET` +- **בעיה:** `/admin/db-emergency-cleanup` (מתודת `GET`, `@admin_required`) הריץ `killOp` על כל הפעולות הפעילות ואז **מחק את כל האינדקסים ב־`code_snippets`**. +- **תוצאה:** פעולה הרסנית ובלתי הפיכה מופעלת בניווט לכתובת. כל prefetch של דפדפן, היסטוריה, או סימנייה מפעילים אותה. הראוט הוסר. +- **מקור:** הוסר ב־#2525 (`e7368882`, 2026-01-01) +- **דפוס:** API design — קוד מת, שכפול, חוזה שגוי + +--- + +### MEDIUM + +#### 22. דגלי `$project` נשמרו כ־`""` — והשאילתה השמורה היא פעולה אחרת +- **בעיה:** `$project` נשאר בשלד המנורמל, ולכן כל שאילתה שנשמרה עם ערכים אמיתיים יצאה עם `{"file_name": ""}`. מחרוזת שאינה מתחילה ב־`$` בתוך היטלה נקראת כ**קבוע** — "החזר את הקבוע ``" ולא "החזר את השדה". +- **תוצאה:** שלב שמושך את `code` נותח כשלב שאינו קורא שום שדה. נמדד מול MongoDB 8.0.32: ה־`queryShapeHash` שונה, כלומר המנוע עצמו סופר אותן כשתי שאילתות. הכשל שקט לגמרי — בניגוד ל־`$limit`, מחרוזת בהיטלה אינה גורמת למונגו לזרוק. +- **מקור:** #3350 (`57734bcf`, 2026-09-07) +- **דפוס:** API design — קוד מת, שכפול, חוזה שגוי + +#### 23. תאריכים נשמרו כמחרוזות JSON, והדוח הצהיר את ההפך ממה שרץ +- **בעיה:** הערכים לא הולכים ישר ל־`explain` אלא עושים סיבוב דרך הדפדפן, ול־JSON פשוט אין טיפוס תאריך. +- **תוצאה:** נמדד מול הקלאסטר: התאריך האמיתי מתאים ל־1,157 מסמכים, ואותו תאריך כמחרוזת — ל־0. כלומר `explain` מהיר עם אפס סריקה ויעילות מושלמת, דוח שנראה מצוין ומסקנתו הפוכה. **זה גרוע מ־``, כי `` נראה שבור.** +- **מקור:** #3346 (`01546944`, 2026-09-07) — התיקון עבר ל־Extended JSON (`json_util`) +- **דפוס:** ולידציית קלט וגבולות חיצוניים + +#### 24. שלושה `except` שקטים במסלול החיפוש +- **בעיה:** כשל במסלול החיפוש המהיר נבלע בלי שורת לוג. +- **תוצאה:** שלושה סבבי חקירה על "הפרופיילר לא שומר ערכים" הובילו למקום אחר לגמרי. התסמין שהגיע לחוקר היה `unknown_field:code` — סימפטום של פולבאק, לא של השורש. +- **מקור:** #3349 (`b8b5d841`, 2026-09-07) +- **דפוס:** בליעת כשל בשקט (K11) + +#### 25. גודל קובץ חושב בחמישה מקומות שנסחפו זה מזה +- **בעיה:** אותו כלל פורמוט היה משוכפל בין `webapp/app.py` ל־`webapp/collections_api.py`, ובשלושה קבצי JS נוספים (`compare.js`, `global_search.js`, `live-preview.js`). `:.1f` הדפיס ספרה אחרי הנקודה גם כשהיא אפס. +- **תוצאה:** `105.0 KB` ו־`582.0 B` — תו נוסף בלי מידע נוסף. וחשוב מזה: חמישה עותקים שיכולים להיסחף, ובפועל נסחפו. הערך גם נקרא בסדר שגוי בהקשר עברי. +- **מקור:** #3345 (`09aa2731`, 2026-09-07) — נוצר `webapp/size_format.py` כמודול עלה +- **דפוס:** API design — קוד מת, שכפול, חוזה שגוי + +#### 26. השדות הכבדים נגררו דרך המיון והקיבוץ ב־`/files` +- **בעיה:** הצינור שבונה "הגרסה האחרונה לכל שם קובץ" שם את ה־`$project` שמסיר את `code` **אחרי** ה־`$sort` וה־`$group`. `allowDiskUse` הועבר בפועל, אבל שם השגיאה הוא `QueryExceededMemoryLimitNoDiskUseAllowed` — ב־Atlas זה לא עוזר. +- **תוצאה:** גוף הקובץ המלא נגרר דרך המיון, תקציב ה־100MB נגמר, מונגו החזירה שגיאה 292, והקוד נפל למסלול חלופי: לולאת `find` עם `skip` שסורקת עד 4,000 מסמכים במנות של 120 ומריצה `aggregate` נוסף לכל מנה. דף `/files` לקח 3.1–3.4 שניות, בטעינה רגילה ולא במקרה קצה. +- **מקור:** #3336 (`c16bbd2e`, 2026-09-06) +- **דפוס:** עקביות DB וכתיבה חלקית + +#### 27. חיפוש שלקח 191 שניות — שליפה קובץ־קובץ +- **בעיה:** `_text_search` ו־`_function_search` החזיקו בלוק זהה מילה במילה ששלף כל קובץ מתאים בשאילתה נפרדת, וכל שליפה כזו היא שלוש קפיצות רשת (GET ל־Redis, `find_one` **בלי היטלה** כלומר כל ה־`code`, ואז SETEX). ו־`limit` הוחל רק בסוף, אחרי הסינון והמיון. +- **תוצאה:** 191.74 שניות לחיפוש אחד. 745 קבצים פעילים, 19.6MB — כלומר ~258ms לקובץ. הוובאפ ביקש עשר תוצאות והמערכת משכה 745 מסמכים. +- **מקור:** #3361 (`49f586f4`, 2026-09-08) — הוחלף ב־`get_latest_versions_by_names` עם `$in` +- **דפוס:** עקביות DB וכתיבה חלקית + +#### 28. האינדקס בזיכרון נבנה בכל חיפוש, כולל חיפושים שלא קוראים ממנו +- **בעיה:** `AdvancedSearchEngine.search` בנה את `SearchIndex` לפני שבדק איזה סוג חיפוש התבקש. רק `TEXT` ו־`FUNCTION` קוראים מהאינדקס; `CONTENT`, `REGEX` ו־`FUZZY` סורקים את ה־DB בעצמם. `CONTENT` הוא ברירת המחדל של הוובאפ. +- **תוצאה:** המסלול הרגיל שילם על סריקה מלאה של כל קבצי המשתמש (כולל `code`, עם קיבוץ גרסאות בכל עמוד) שאיש לא קרא את תוצאתה — ומיד אחריה סריקה מלאה שנייה לחיפוש עצמו. מה שהסתיר את זה: `_fuzzy_search` קיבל פרמטר `index` שמעולם לא נקרא. +- **מקור:** #3351 (`86ade337`, 2026-09-07) +- **דפוס:** API design — קוד מת, שכפול, חוזה שגוי + +#### 29. שכבת `asyncio` מעל worker יחיד של gevent +- **בעיה:** ה־WebApp הוא Flask על WSGI עם worker יחיד של gevent. gevent מריץ את כל הבקשות כגרינלטים באותו OS thread, אבל asyncio שומר את מצב "הלולאה הרצה" ברמת ה־thread. שלוש שכבות ה־fallback של `_run_awaitable_blocking` כולן חיות באותו OS thread. והשירות מעולם לא היה אסינכרוני: כל מתודה הייתה `async def` שעוטפת pymongo סינכרוני ב־`asyncio.to_thread`. +- **תוצאה:** `/admin/profiler` נפל לסירוגין ב־500 — כשבקשה אחת הייתה בתוך `run_until_complete`, כל בקשה חופפת ראתה את הלולאה שלה כרצה ונפלה. שלוש שגיאות זהות בכל נפילה. +- **מקור:** #3330 (`73d8ad49`, 2026-09-05) — שכבת ה־asyncio הוסרה כליל +- **דפוס:** async / control flow + +#### 30. ה־MCP החזיר וקטור של 768 ממדים בכל קריאה +- **בעיה:** `_clean` העתיק כל מפתח של מסמך המונגו וסינן רק ארבעה שמות. שלושה כלים דלפו: `get_file`, `list_versions` (וקטור לכל גרסה), ו־`save_file`. +- **תוצאה:** `snippetEmbedding` — 768 floats, כ־10KB — בכל קריאה, ללא תלות בגודל הקובץ. על קובץ של 1.8KB הווקטור היה פי שש מכל השאר, ועל קריאת טווח של שש שורות פי עשרים וחמש. תשעה שדות ניהול נוספים רכבו איתו. ההפך ממה שפרמטר `lines` נועד לו. +- **מקור:** #3318 (`a752ff8a`, 2026-09-02) +- **דפוס:** API design — קוד מת, שכפול, חוזה שגוי + +#### 31. שחזור ZIP לריפו העלה רובד אחד עמוק מדי +- **בעיה:** ה־ZIP של גיבוי ריפו הוא ה־zipball הרשמי של GitHub, שכל תוכנו תחת תיקייה בשם `owner-repo-sha`, ואליו נוסף `metadata.json` בשורש. הקובץ בשורש הוא בדיוק מה שגורם ל־`detect_zip_common_root` להחזיר `None` — וזה הכלל הנכון שלה. +- **תוצאה:** השחזור העלה לריפו את המניפסט ואת כל התוכן רובד אחד עמוק מדי. +- **מקור:** #3314 (`4b4da824`, 2026-09-01), רגרסיה מ־#3205. התיקון מזהה מניפסט **לפי תוכן** ולא לפי שם, כי `metadata.json` הוא שם לגיטימי בריפו אמיתי +- **דפוס:** לוגיקה עסקית ומקרי קצה + +#### 32. `$ne` בתוך `partialFilterExpression` — אותה טעות ארבע פעמים +- **בעיה:** `partialFilterExpression` של מונגו אינו תומך ב־`$ne`/`$not`. הטעות הופיעה ב־`bookmarks_manager` (`anchor_id: {"$ne": ""}`), תוקנה, הוכנסה מחדש באינדקס `username_unique` (`{"$type": "string", "$ne": ""}`), והופיעה שוב ב־`note_reminders` (`needs_push: {"$ne": False}`). +- **תוצאה:** יצירת האינדקס נכשלת, ולכן האילוץ הייחודי או האינדקס לשאילתת הפולינג פשוט לא קיימים. באינדקס ייחודי זו הזמנה לכפילויות בנתונים. +- **מקור:** #895 (`46be8c07`, 2025-10-21) → #2121 (`9cc417ad`, 2025-12-13) → #2627 (`547e1dac`, 2026-01-08). הלקח כתוב היום כהערה ב־`database/manager.py:1994` וב־`sticky_notes_target.py:117`, אבל **אין לו שורת טריגר** ב־`CLAUDE.md` +- **דפוס:** סכימת DB ואינדקסים + +#### 33. סדר המפתחות באינדקס `code_snippets` היה הפוך +- **בעיה:** האינדקס `user_file_version_desc` נוצר בסדר `(user_id, file_name, version)` בזמן שהשאילתות דורשות `(file_name, user_id, version)`. הקוד שניסה לתקן זאת מחק אינדקסים בתוך `try/except: pass` כפול. +- **תוצאה:** אינדקס שאינו משמש את השאילתה שלמענה נבנה. ובנוסף — האינדקס הייחודי על `username` הוסר לחלוטין ב־#2525 (2026-01-01) באותו PR שנקרא "Database index restoration", כלומר זה היה שחזור מנפילה ולא שיפור. +- **מקור:** #2517 (`be01e2f7`, 2026-01-01); #2525 (`e7368882`) +- **דפוס:** סכימת DB ואינדקסים + +#### 34. אינדקס פולינג התזכורות עם סדר מפתחות שאינו מאפשר `sort+limit` +- **בעיה:** האינדקס נוצר כ־`(status, remind_at, needs_push)` בזמן שהשאילתה מסננת ומסדרת לפי `remind_at`. +- **תוצאה:** שאילתת הפולינג שרצה בלולאה לא יכולה להשתמש באינדקס ל־`sort+limit` יעילים. +- **מקור:** #2627 (`547e1dac`, 2026-01-08) — `remind_at` הועבר לראש +- **דפוס:** סכימת DB ואינדקסים + +#### 35. EWMA של זמן התגובה כלל 5xx וטיימאאוטים +- **בעיה:** `record_request_outcome` עדכן את ה־EWMA בכל בקשה, כולל 5xx וטיימאאוטים של gateway/worker. +- **תוצאה:** "זמן תגובה ממוצע" תיאר גם בקשות שלא הוגשו, ולכן `anomaly_detected` נדלק על סמך מספר שאינו מתאר את מה שהמשתמשים חווים. +- **מקור:** #2740 (`4175f402`, 2026-01-14) +- **דפוס:** לוגיקה עסקית ומקרי קצה + +#### 36. המנוע החזוי הפעיל ריסטארטים על סמך תקלות חיצוניות +- **בעיה:** `_get_current_values` ב־`predictive_engine` קרא את שיעור השגיאות והשיהוי בלי לתחום למקור פנימי, כלומר תקלות של שירותים חיצוניים ופתיחות Circuit Breaker נספרו כבריאות ה־worker. +- **תוצאה:** ריסטארט ל־worker בריא בגלל תקלה חיצונית. גם ה־gauges של הספים סבלו מאותה אי־עקביות — הספים חושבו על דגימות פנימיות והמספר ה"נוכחי" לא. +- **מקור:** #2750 (`dd991662`, 2026-01-17) +- **דפוס:** לוגיקה עסקית ומקרי קצה + +#### 37. מפתח `details` מקונן דרס את מטען ההתראה המקורי +- **בעיה:** `emit_internal_alert` "מקדם" שדות מ־`details` לרמה העליונה, ולא החריג את המפתח `details` עצמו. +- **תוצאה:** ה־details המקורי נעלם ורק שדות Sentry נשארו. כל מי שקרא את ההתראה קיבל תיאור חלקי של מה שקרה. +- **מקור:** #2354 (`fe87466c`, 2025-12-26) +- **דפוס:** לוגיקה עסקית ומקרי קצה + +#### 38. תגיות התראות חיפשו לפי מזהה אחד בזמן שהאחסון כתב לפי אחר +- **בעיה:** `fetch_alerts` חיפש `tags_map.get(alert["alert_uid"])`, בזמן ששכבת האחסון בנתה את המזהה בשרשרת fallback (`alert_uid` → `uid` → `id` → `_id`) והמירה ל־`str`. +- **תוצאה:** התראות שהמזהה שלהן הגיע מאחד השדות האחרים, או שהיה מספר ולא מחרוזת, קיבלו רשימת תגיות ריקה תמיד. +- **מקור:** #2429 (`7ac2aae2`, 2025-12-27) +- **דפוס:** עקביות DB וכתיבה חלקית + +#### 39. `setdefault` על מזהה שכבר קיים אבל falsy +- **בעיה:** `doc.setdefault("story_id", doc.get("id") or uuid.uuid4().hex)` — `setdefault` לא נוגע במפתח שקיים, גם אם ערכו `None` או `""`, והמזהה גם לא הומר ל־`str`. +- **תוצאה:** סיפורי תקלה נשמרו בלי `story_id` שמיש, או עם מזהה מטיפוס שלא מתאים לשאילתות שקוראות אותו. +- **מקור:** #1989 (`382dd558`, 2025-12-07) +- **דפוס:** לוגיקה עסקית ומקרי קצה + +#### 40. קאש פריטי אוסף לא בוטל בעדכון מצב +- **בעיה:** `update_workspace_item_state` כתב ל־DB ולא ניקה את מפתח הקאש `collections_items:*`. +- **תוצאה:** המשתמש שינה מצב של פריט וראה את המצב הקודם עד שה־TTL פג. +- **מקור:** #1978 (`2ba3d164`, 2025-12-07) +- **דפוס:** בליעת כשל בשקט (K11) + +#### 41. `first_ts` שנקבע בזמן הצינון חסם כל התראה חדשה +- **בעיה:** `LogEventAggregator` קבע `first_ts` גם על שורות שהגיעו בתוך חלון הצינון, ולכן אחרי שהצינון עבר החלון נראה כאילו התחיל מזמן. +- **תוצאה:** אחרי ההתראה הראשונה על קטגוריה, התראות נוספות עליה לא נשלחו — התקלה נמשכה והמוניטור שתק. +- **מקור:** #1193 (`9fa3fd8d`, 2025-10-30) +- **דפוס:** ניהול מצב, אתחול ו־concurrency + +#### 42. ה־`errorhandler(Exception)` הכולל בלע 404 ו־403 +- **בעיה:** `@app.errorhandler(Exception)` תפס גם `werkzeug.exceptions.HTTPException`. +- **תוצאה:** כל 404, 403 ו־405 הפכו ל־500 עם `traceback.print_exc()` — גם קודי מצב שהם חלק מהחוזה התקין של ה־API. +- **מקור:** #1330 (`c945c55c`, 2025-11-04) +- **דפוס:** async / control flow + +#### 43. פתקים דביקים חזרו למצב קודם — שמירות שלא אוחדו +- **בעיה:** `_flushPendingKeepalive` הסתכל רק על `_pending` והתעלם ממה שכבר בטיסה (`_inFlight`). +- **תוצאה:** כשהמשתמש סגר את הדף בזמן שבקשת שמירה בדרך, השמירה שבטיסה אבדה והפתק חזר למצב מוקדם יותר. +- **מקור:** #1292 (`b45f1dbe`, 2025-11-04) +- **דפוס:** ניהול מצב, אתחול ו־concurrency + +#### 44. פתקים נדרסו בין מכשירים, ואז נתקעו בלופ של 409 +- **בעיה:** לא הייתה בדיקת concurrency בין מכשירים. התיקון הוסיף `prev_updated_at` ו־409 — וזה יצר באג שני: הלקוח לא עדכן את ה־`updatedAt` המקומי בכשל 409. +- **תוצאה:** קודם דריסה חוצה־מכשירים, ואחר כך לופ קונפליקטים אינסופי. השרת גם החזיר `updated_at` מ־`find_one` נוסף אחרי העדכון, מה שיצר מרוץ עם כותבים מקבילים. +- **מקור:** #1056 (`bcb03c0e`, 2025-10-23) +- **דפוס:** ניהול מצב, אתחול ו־concurrency (U1) + +#### 45. `ensure_*_indexes()` נקרא מתוך נעילת החיבור +- **בעיה:** `get_db` קרא ל־`ensure_recent_opens_indexes()` ו־`ensure_code_snippets_indexes()` **בתוך** הנעילה שהוא מחזיק, והן קוראות ל־`get_db` בעצמן. +- **תוצאה:** קריאה re-entrant לנעילה בזמן האתחול — מרוץ preload שהופיע כתקיעה. +- **מקור:** #1003 (`c9e38f3f`, 2025-10-22) +- **דפוס:** ניהול מצב, אתחול ו־concurrency + +#### 46. חלון ה־rate-limit המקומי איבד את נקודת ההתחלה +- **בעיה:** כשהחלון התאפס והבקשה הנוכחית נחסמה על ידי המגביל הראשי, ה־`start_ts` החדש לא נשמר. +- **תוצאה:** הפולבק המקומי של ההגבלה נשאר לא־עקבי, כלומר משתמש יכול היה לקבל יותר או פחות בקשות ממה שהוגדר. +- **מקור:** #1173 (`8a0d35e0`, 2025-10-30) +- **דפוס:** ניהול מצב, אתחול ו־concurrency + +#### 47. גודל ה־ZIP נקרא מ־`r.content` אחרי הזרמה לבאפר +- **בעיה:** `total_bytes = len(r.content)` על תשובה שכבר הוזרמה למקטעים לתוך `tmp_buf`. +- **תוצאה:** הגודל שהוצג למשתמש לא תיאר את הארכיון שנשלח לו. +- **מקור:** #948 (`8ba5088a`, 2025-10-22) +- **דפוס:** לוגיקה עסקית ומקרי קצה + +#### 48. התראות GitHub שלחו את כל הבקלוג בהרצה הראשונה +- **בעיה:** לא נקבע baseline כשאין `notifications_last`, ולכן הלולאה עברה על 10 ה־PRים האחרונים וכולם נחשבו "חדשים". +- **תוצאה:** המשתמש שהפעיל התראות קיבל מיד מטח של עשר הודעות על דברים שקרו מזמן. +- **מקור:** #139 (`7a416e4d`, 2025-08-10) +- **דפוס:** לוגיקה עסקית ומקרי קצה + +#### 49. `None` ברשימת השפות הפיל את דף הקבצים +- **בעיה:** `db.code_snippets.distinct('programming_language', ...)` מחזיר גם `None` למסמכים בלי שפה, ואז `sorted(languages)` על רשימה שמערבת `None` ומחרוזות זורק `TypeError`. +- **תוצאה:** דף הקבצים קרס לכל משתמש שהיה לו קובץ אחד בלי שפה מזוהה. +- **מקור:** #447 (`11231b57`, 2025-09-06) +- **דפוס:** ולידציית קלט וגבולות חיצוניים (U3) + +#### 50. גיבוי Drive לא התאושש אחרי 401 +- **בעיה:** `ensure_folder` ושאר הפעולות קיבלו את השירות פעם אחת, ואם הטוקן פג באמצע לא היה שום מסלול ריענון. +- **תוצאה:** אחרי 401 אחד, כל גיבויי Drive של המשתמש נכשלו עד התחברות מחדש ידנית. +- **מקור:** #3035 (`e0aac899`, 2026-02-03) +- **דפוס:** בליעת כשל בשקט (K11) + +#### 51. `PersonalBackupService` קיבל אובייקט מהטיפוס הלא נכון +- **בעיה:** `webapp/backup_api.py::_get_db` החזיר את `webapp.app.get_db()` — אובייקט `Database` של pymongo — בזמן שהשירות מצפה ל־`DatabaseManager` מ־`database/manager.py`. +- **תוצאה:** שירות הגיבוי האישי קרא מתודות שלא קיימות על האובייקט שקיבל. +- **מקור:** #3093 (`eaf00664`, 2026-02-15) +- **דפוס:** API design — קוד מת, שכפול, חוזה שגוי + +#### 52. עימוד בחירת הריפו נחטף על ידי מסך הקבצים השמורים +- **בעיה:** הקוד השתמש ב־`update.callback_query` גם במסלולים שבהם קיים רק `update.message`, ומסך אחד גלש לתוך זרימת העימוד של האחר. +- **תוצאה:** לחיצה על "עמוד הבא" בבחירת ריפו הובילה למסך אחר, או קרסה על `AttributeError`. +- **מקור:** #326 (`72fa73e5`, 2025-08-31) +- **דפוס:** async / control flow + +#### 53. `create_task` בזמן עליית הבוט — "no running event loop" +- **בעיה:** `self.application.create_task(...)` נקרא בזמן ההרכבה, לפני שיש לולאה רצה. +- **תוצאה:** מצב התחזוקה לא התנקה אוטומטית אחרי ה־warmup, כלומר הבוט נשאר חסום עד Redeploy ידני. +- **מקור:** #323 (`cc273da3`, 2025-08-31) — הוחלף ב־`job_queue.run_once` +- **דפוס:** async / control flow + +#### 54. Webhook פעיל התנגש עם polling, ו־deleteWebhook סגר את הלולאה +- **בעיה:** הבוט התחיל polling בזמן שקיים webhook פעיל (409 Conflict). התיקון הראשון קרא ל־`deleteWebhook` אסינכרוני — מה שסגר את ה־event loop של PTB. +- **תוצאה:** קודם הבוט לא קיבל עדכונים בכלל, ואחר כך הוא קרס בעלייה. התיקון השני עבר ל־HTTP סינכרוני. +- **מקור:** #3150 → #3151 (2026-04) +- **דפוס:** async / control flow + +#### 55. בדיקת אמת בוליאנית על אובייקט של pymongo +- **בעיה:** `if collection:` / `if not default_db:` על `Collection` ו־`Database` — pymongo זורק `NotImplementedError` בכוונה, כדי למנוע את הבלבול בין "אובייקט קיים" ל"האוסף לא ריק". +- **תוצאה:** שמירת סקילים נכשלה, ובגרסה המוקדמת הבוט עשה `sys.exit(1)` על מסד תקין. +- **מקור:** #3199 (`0e150994`, 2026-07-26); ומופע מוקדם ב־`4e20f7b5` (2025-08-06) +- **דפוס:** ולידציית קלט וגבולות חיצוניים + +#### 56. `.format()` על תבנית שמכילה `{20,}` שיבש את רג'קס הניקוי +- **בעיה:** כדי לפתור אזהרות escape בתוך מחרוזת תבנית של ספריית הסניפטים, נוסף `.format(bs="\\")` — והוא פירש את `{20,}` שברג'קס כשדה פורמט. +- **תוצאה:** הרג'קס שמשחיר `ghp_...` ו־`github_pat_...` נשבר, כלומר **מסנן הסודות בלוגים הפסיק לעבוד** עד שתוקן ב־PR הבא. +- **מקור:** #1595 (`4a9551f8`) הכניס, #1597 (`024a74b5`) תיקן — שניהם 2025-11-12 +- **דפוס:** בטיחות נתונים ודליפת סודות + +#### 57. תווים בלתי נראים וסימני כיווניות נשארו בקוד שנשמר +- **בעיה:** הניקוי טיפל רק ב־`​‌‍`, ולא ב־`⁠` ולא בסימוני הכיווניות (`‎‏‪-‮⁦-⁩`). בנוסף, שחזור המארקדאון מ־entities של טלגרם בלע תווים. +- **תוצאה:** קוד שהודבק מהדפדפן נשמר עם תווים בלתי נראים שמשבשים הרצה והצגה, ובחלק מהמקרים תווים נעלמו מהתוכן. +- **מקור:** #638, #639 (`67d36bc6`, 2025-10-04), #643, #677 (`d4d48934`), #679 +- **דפוס:** ולידציית קלט וגבולות חיצוניים + +#### 58. גרסת־רפאים ריקה נוצרה אחרי "החלף" +- **בעיה:** אחרי "🔄 החלף" הזרימה נפלה לשמירה גנרית עם באפר ריק. +- **תוצאה:** גרסה נוספת, ריקה, נכתבה להיסטוריה מיד אחרי שמירה מוצלחת — כלומר הגרסה האחרונה של הקובץ הייתה ריקה. +- **מקור:** #1687 (`e6247d42`, 2025-11-19) +- **דפוס:** עקביות DB וכתיבה חלקית + +#### 59. `large_files` לא נכללו בייצוא ZIP +- **בעיה:** יצירת ה־ZIP שלפה רק מ־`code_snippets` ולא מאוסף `large_files`. +- **תוצאה:** משתמש שייצא את כל הקבצים שלו קיבל ארכיון חסר, בלי סימן שמשהו נשמט. +- **מקור:** #2948 (`6a82440b`, 2026-01-27) +- **דפוס:** עקביות DB וכתיבה חלקית + +#### 60. `no-store` גורף על כל HTML ביטל את ה־ETag +- **בעיה:** `_disable_html_cache` דרס כל תגובת HTML ב־`no-cache, no-store, must-revalidate`, כולל עמודים שמגדירים `ETag` בכוונה. +- **תוצאה:** מנגנון ה־revalidation שנבנה לעמודים כבדים לא עבד — הדפדפן הוריד הכול מחדש בכל פעם. +- **מקור:** #2932 (`cb678b31`, 2026-01-24) +- **דפוס:** API design — קוד מת, שכפול, חוזה שגוי + +#### 61. בדיקות הדופק על `/` נחסמו על ידי ה־rate limiter +- **בעיה:** `index` ו־`index_head` לא היו פטורים מהגבלת הקצב. +- **תוצאה:** מוניטור חיצוני שדוגם את `/` קיבל 429 והכריז על השירות כמנותק. +- **מקור:** #2789 (`89523a2d`, 2026-01-17) +- **דפוס:** לוגיקה עסקית ומקרי קצה + +#### 62. `ZipFile` שלא נסגר דטרמיניסטית +- **בעיה:** `zf = zipfile.ZipFile(buf, "r")` בלי `with`, בתוך handler אסינכרוני עם יציאות מוקדמות. +- **תוצאה:** "Unraisable Exception" בזמן GC — שגיאה שמופיעה בלוג במקום ובזמן שאין להם קשר לקוד שגרם לה. +- **מקור:** #2751 (`323486ce`, 2026-01-17) +- **דפוס:** async / control flow + +#### 63. `KeyError: 'PNG'` — Pillow 12 רושם פורמטים בעצלנות +- **בעיה:** `img.save(buf, format='PNG')` בלי `Image.init()` מוקדם. +- **תוצאה:** יצירת תמונות קוד נכשלה בסביבות מסוימות אחרי עדכון Pillow, עם שגיאה שלא מסבירה את עצמה. +- **מקור:** #2275 (`fb6d6a4a`, 2025-12-21) +- **דפוס:** ולידציית קלט וגבולות חיצוניים + +#### 64. `username` גם ב־`$setOnInsert` וגם ב־`$set` +- **בעיה:** `update_one` עם שני האופרטורים על אותו שדה — מונגו זורק "Updating the path 'username' would create a conflict". +- **תוצאה:** עדכון פרטי משתמש נכשל. +- **מקור:** #2182 (`319b979b`, 2025-12-17) +- **דפוס:** סכימת DB ואינדקסים + +#### 65. `skip`/`limit` לא הוחלו בשקט +- **בעיה:** `coll.find(...).skip(skip).limit(per_page)` בתוך `try/except` שמחזיר `rows = []` — כשה־collection החזיר רשימה (במקום cursor), או כש־`skip`/`limit` זרקו, העימוד פשוט לא הוחל. +- **תוצאה:** עמוד 2 החזיר את אותן שורות של עמוד 1, או רשימה ריקה, בלי שגיאה. +- **מקור:** #1448 (`fc3d4bd6`, 2025-11-06) +- **דפוס:** בליעת כשל בשקט (K11) + +#### 66. ה־`plugin` של `==מארקר==` הפיל את כל תצוגת המארקדאון +- **בעיה:** התוסף היה כתוב ביד וקרא ל־`state.md.inline.parse` מתוך כלל inline. זו נקודת כניסה עליונה שמריצה את שלבי ה־ruler2 על מערך הטוקנים המשותף, ו־`fragments_join` מקצר אותו. +- **תוצאה:** מסמך עם `**מודגש**` ואחריו `==מארקר==` באותה פסקה הפיל את כל התצוגה עם "שגיאה ברינדור Markdown" בלי סיבה, כי `emphasis.postProcess` זרק `TypeError`. ובמקרים כמו `*נטוי* ואז ==מארקר==` התוסף **השמיד תוכן בשקט**. אותו תוסף היה משוכפל תו־בתו גם ב־`live-preview.js`, כלומר ה־Live Preview היה שבור גם. +- **מקור:** #3365 (`6bfa40e6`, 2026-09-09) +- **דפוס:** DOM / UI / RTL + +#### 67. גלילה חלקה שביטלה כל ניווט עוגן באפליקציה +- **בעיה:** `smooth-scroll.js` נטען מ־`base.html` בכל עמוד, ו־`onAnchorClick` ביטל את ניווט הדפדפן לכל קישור עוגן וגלל בעצמו — בלי לכתוב את העוגן לכתובת. +- **תוצאה:** בכל האפליקציה, אף לחיצה על `` לא עדכנה את `location.hash`: אין Back, `:target` לא נדלק, כתובת שמעתיקים לא נוחתת על המקטע, ו־`hashchange` — ש־`base.html` נשען עליו לשמירת הכתובת האחרונה — לא ירה. וההעדפה השמורה החזיקה את המנגנון דלוק גם אחרי שהפך לכבוי כברירת מחדל. +- **מקור:** #3339 (`588f6719`, 2026-09-06) +- **דפוס:** DOM / UI / RTL + +#### 68. השורות האחרונות של העורך נבלעו מתחת לקצה התחתון +- **בעיה:** גובה ה־CodeMirror נקבע בשני מקומות עם שתי נוסחאות (`max-height: 60vh` מול `calc(100vh - 200px)`), ואף אחד מהם לא הכיר את 106 הפיקסלים שיושבים ביניהם. +- **תוצאה:** מתחת לגובה חלון של כ־800px, `overflow: hidden` בלע את העודף בשקט והשורות האחרונות **לא היו נגישות בשום גלילה**. נמדד: חלון 600px הסתיר 5.4 שורות, 680px הסתיר 2.19. +- **מקור:** #3362 (`81e22310`, 2026-09-08) +- **דפוס:** DOM / UI / RTL + +#### 69. המפריד ב־RTL זז הפוך מהאצבע +- **בעיה:** `startWidth + (clientX - startX)` היא נוסחת LTR — היא נכונה כשהסיידבר משמאל למפריד. בפריסת RTL הסיידבר מימין. +- **תוצאה:** נמדד: האצבע ב־1038 והמפריד ב־1078; האצבע ב־958 והמפריד ב־1138 — 180px פער. +- **מקור:** #3286 (`aac8d7d1`, 2026-08-26) — הסימן נגזר עכשיו מהפריסה בפועל +- **דפוס:** DOM / UI / RTL + +#### 70. פתק שנפתח במסך צר יצא מהמסך ולא היה דרך להחזירו +- **בעיה:** `_reflowWithinViewport` מצמידה **מיקום** ואינה מצמצמת **גודל**: `maxLeft` נגזר כ־`vpW - w`, ולכן כשהפתק רחב מהמסך הביטוי מתכווץ למינימום והפתק נדחף לקצה וממשיך לגלוש. +- **תוצאה:** זו תקיעה ולא אי־נוחות — ידית הגרירה בכותרת וידית שינוי הגודל בפינה הן הכלים היחידים לחלץ פתק, והן בדיוק מה שיוצא מהמסך. נמדד על מסך 380: הפתק הגיע ל־`x=621`. +- **מקור:** #3299 (`7b90030a`, 2026-08-27) +- **דפוס:** DOM / UI / RTL + +#### 71. נכסי הפתקים נטענו בלי cache-buster +- **בעיה:** `md_preview.html` טען את `sticky-notes.js` ו־`sticky-notes.css` בכתובת קבועה, בלי `?v={{ static_version }}` — שתי התגיות היחידות של הפתקים בכל הריפו בלי buster. +- **תוצאה:** מול `SEND_FILE_MAX_AGE_DEFAULT=31536000` הדפדפן הגיש את העותק שנשמר לפני הדיפלוי, **עד שנה**, בלי revalidation. רינדור המארקדאון בפתקי קבצים פשוט הפסיק לעבוד אחרי דיפלוי בזמן שאותו קוד עבד בלוחות. +- **מקור:** #3283 (`7e0ec4f6`, 2026-08-26) +- **דפוס:** DOM / UI / RTL + +--- + +### LOW + +#### 72. הערות DEBUG וקוד מת שנשארו בקוד הייצור +- **בעיה:** `webapp/rules_api.py:119` מדפיס `print(f"🕵️‍♂️ DEBUG INCOMING RULE: {data.get('rule')}")` ואת ה־`data` המלא, על נתונים שמגיעים מגוף בקשה. `github_upload_fix.py` הוא קובץ שלם עם חמש שורות `logger.info("DEBUG: ...")` — **ואין לו שום קורא** מלבד רשימת חריגים בטסט ארכיטקטורה. גם `handlers/documents.py:380-381` ו־`webapp/app.py:19226` נושאים לוגים בסגנון DEBUG. +- **תוצאה:** רעש בלוגים, קוד שאף אחד לא מריץ אבל כל אחד יכול להסיק ממנו על התנהגות המערכת, וסיכון שתוכן משתמש יודפס לפלט התהליך. זה **פתוח היום**. +- **מקור:** מצב נוכחי של `main`, לא קומיט תיקון +- **דפוס:** API design — קוד מת, שכפול, חוזה שגוי + +#### 73. שגיאות תחביר ו־API בסיסיות מהשבוע הראשון +- **בעיה:** `def init(self, ...)` במקום `__init__` ב־`activity_reporter.py` (`aed6d4c2`); `f"{len(content.split('\n'))}"` — קו נטוי הופכי בתוך f-string לפני Python 3.12 (`c70cdf62`); `db[LOCK_COLLECTION]` על `DatabaseManager` במקום `db.db[...]` (`812cfa48`). +- **תוצאה:** כל אחד מהם מנע מהבוט לעלות. כולם תוקנו באותו יום או יומיים. +- **מקור:** 2025-08-06 עד 2025-08-09 +- **דפוס:** לוגיקה עסקית ומקרי קצה + +#### 74. חיווי, ניגודיות ואייקונים — שכפול שהוליד אי־עקביות +- **בעיה:** שורת סטטוס "נשמר" שאף אחד לא מסתיר, ושמירה שנייה כותבת בה את אותה מחרוזת (אפס שינוי ב־DOM ולכן אפס חיווי) — #3293. שם הפתק ירש `color` מהערכה על גבי רקע צהוב קשיח, כלומר 1.03:1 ניגודיות בחמש מתוך שש הערכות — #3264. אייקוני השפה יוצרו בחמישה מקומות עם כיסוי שונה, ולכן אותו קובץ קיבל אייקון אחר בדף הקבצים ובחיפוש — #3217. שגיאת GitHub 403 גולמית ("Must have admin rights") הוצגה למשתמש במקום להסביר שחסר scope `delete_repo` — #3164. מנעול הוצג בקונפיג אינספקטור גם למשתני סביבה שאין להם ערך בכלל — #3231. +- **תוצאה:** פגיעות נגישות מדידות, חוסר עקביות בין מסכים, והודעות שגיאה שהמשתמש לא יכול לפעול לפיהן. +- **מקור:** #3164, #3217, #3231, #3264, #3293 (2026-06 עד 2026-08) +- **דפוס:** DOM / UI / RTL + +--- + +## דפוסים שקלאוד קוד פספס (ותוקנו בדרך אחרת) + +הפרויקט הזה נכתב כמעט כולו בעזרת סוכני קוד: מתוך 2,718 קומיטים, **1,751 חתומים על ידי Cursor Agent, 981 על ידי Claude ו־67 על ידי Copilot**. מתוך 423 קומיטי התיקון, **367 (87%) הם עצמם AI־מיוצרים**. כלומר אין כאן "באגים של AI" מול "באגים של אדם" — יש באגים, וכולם נכתבו ותוקנו באותו זרם עבודה. מה שכן אפשר לזהות הוא **מי תפס אותם**: + +| ערוץ הגילוי | קומיטי תיקון שמזכירים אותו | מה זה אומר | +|---|---|---| +| ריוויוור אוטומטי (cubic / qodo / CodeRabbit / Sourcery / claude[bot]) | 63 | הרשת הצפופה ביותר. תופסת דפוסים, לא רק שגיאות | +| CI וטסטים | 70 | תופס נפילות, כמעט לא תופס התנהגות שקטה שגויה | +| פרודקשן | 16 | מעט במספר, אבל אלה החמורים — דליפות סוד, קריסת boot, אובדן תוכן | + +ושלוש חתימות חוזרות שדווקא סוכן קוד נוטה להן: + +**1. "תיקון" של API שלא נבדק מול המקור.** #2939 שינה `cursor.explain(verbosity)` ל־`cursor.explain(verbosity=verbosity)` — תיקון סינטקטי סביר למראה, על פרמטר שמעולם לא היה קיים. הבאג האמיתי (הכול רץ ב־`allPlansExecution`) חי עוד שמונה חודשים עד #3333. חמש דוגמאות נוספות מאותו סוג: `explain` באגרגציה שלא מקבל `verbosity` בכלל, `$ne` ב־`partialFilterExpression` (ארבע פעמים), `sparse=True` במקום `partialFilterExpression`, `Image.init()` שהפסיק להיות אוטומטי ב־Pillow 12, ו־`get_user()` של PyGithub שמחזיר אובייקט עצל ולא מאמת כלום. **החתימה: הקוד נראה נכון, קורא נכון, ואינו מתאים לחוזה שבצד השני.** + +**2. אופטימיזציה ל־linter על חשבון סדר טעינה אמיתי.** #3260 הפיל את הפרודקשן בלופ מפני שייבוא הועבר לראש הקובץ כדי לספק E402 — במקום להוסיף `noqa` — בזמן שהשורה 159 מכינה את `sys.path` ושתי הערות בקובץ אומרות זאת במפורש. אותו כיוון מופיע ב־#1595, שהכניס `.format(bs="\\")` כדי לנקות אזהרות escape ובדרך שיבש את רג'קס הניקוי של הסודות. + +**3. כתיבת עותק שלישי במקום איחוד.** גודל קובץ בחמישה מקומות (#3345), אייקוני שפה בחמישה (#3217), שאילתת המחיקה בשלושה (#3358), תוסף ה־`==מארקר==` בשניים תו־בתו (#3365), ולוגיקת ה־`before_send` של Sentry בשני עותקים שהתחילו להתפצל (#3237). זה לא באג בפני עצמו — זה מכפיל־באגים: כל תיקון עתידי צריך להיזכר בכל העותקים, והוא לא נזכר. + +מה שהריפו עשה בתגובה, ועבד: **הקומיטים מ־2026-07 ואילך מכילים מדידה, לא טענה** — "נמדד מול הקלאסטר", "נמדד בכרומיום מול השרת האמיתי", "13 מתוך 15 הבדיקות נופלות על הקוד הקודם", "נמדד, 20 קריאות: 5.0 שניות / 111.0 / 5.1". וההשפעה נראית: 95 תיקונים ב־2025-10 מול 18 ב־2026-09, כשכל תיקון מהאחרונים מכסה יותר קרקע. + +--- + +## המלצות ל־CLAUDE.md + +חמשת הכללים הבאים היו מונעים את הבאגים החוזרים ביותר ברשימה. שניים מהם כבר קיימים ב־`CLAUDE.md` (K11 ו־`amir-bug-patterns`) — הם מופיעים כאן כי הניתוח מראה **איפה הטריגר שלהם לא נדלק**. + +**1. לפני שכותבים קריאה לספרייה או למסד — לאמת את החתימה מול המקור, לא מול הזיכרון.** +הדפוס הכי יקר בריפו הזה הוא קריאה שנראית נכונה ואינה תואמת את החוזה שבצד השני: `Cursor.explain(verbosity=...)` שאין לו פרמטר כזה, `$ne` בתוך `partialFilterExpression` (ארבע פעמים), `$substrBytes` שמקבל אינדקס תווים, `sparse=True` במקום partial, ו־`Image.init()` שהפסיק להיות אוטומטי. הכלל: *מזהה שההגדרה שלו אינה בריפו הזה — מריצים `source-driven-development` ומצטטים מקור.* וחשוב: **ציטוט מקור אינו הרצה.** ב־#3357 הרג'קס עבר את כל הבדיקות שנכתבו לו ופספס את הצורה האמיתית של השדה — רק הרצה מול המבנה האמיתי תפסה. + +**2. שורת טריגר ל־`partialFilterExpression`, שכרגע חסרה.** +זה הכלל הקונקרטי היחיד כאן שאינו מכוסה בשום מקום. `$ne` ב־`partialFilterExpression` נכשל **ארבע פעמים** (#895, #2121, #2627, וההערות בקוד), והלקח יושב היום כהערה ב־`database/manager.py:1994` — כלומר נקרא רק אחרי שכבר הגעת לשורה הנכונה. שורה מוצעת לטבלת דפוסי הבאגים: *"יצירת אינדקס עם `partialFilterExpression` / `sparse` ← `$ne`, `$not` ו־`$nin` אינם נתמכים; השתמש ב-`$exists` + `$type`, וּודא שהערך הריק לא נכתב מלכתחילה."* הטריגר ממופתח לפי מה שיהיה מול העיניים (`partialFilterExpression` על המסך), לא לפי הקטגוריה. + +**3. כשל בשקט הוא באג בפני עצמו — גם כשהמשתמש לא רואה אותו.** +K11 כבר בטבלה, אבל תשע מהבעיות ברשימה הזו הן בליעה, וארבע מהן לא היו על מסלול של "הודעת הצלחה למשתמש" — שהוא התנאי שהכלל מנוסח עליו. `delete_pattern` שיצא ב־`return 0` (#3229), שלושה `except` שקטים בחיפוש (#3349), `AttributeError` שנבלע והפך ל־`return False` במחיקה (#3358), ו־`try/except` שהפך 404 אמיתי לתשובה על קובץ קיים (#3357). ההרחבה המוצעת: *לכל `except` שאין בו `logger` — לכתוב בהערה למה הכשל הזה אינו מעניין. `except: pass` בלי הערה כזו הוא ממצא.* ומצד שני: **ההערה הזו היא גם מה שמונע את התיקון הגרוע** — #3232 הרחיב `except BadRequest` ל־`except Exception` כדי להעביר טסט, והשורש היה בסטאב החסר. + +**4. לפני שכותבים עותק שני של חוק — לחפש את הראשון.** +חמש מהבעיות ברשימה הן שכפול שנסחף. הכלל: *לפני שכותבים פונקציה שמחשבת, מפרמטת או מסננת משהו — `grep` על שם התופעה (`format_file_size`, `bulk-delete`, `lang_icon`). אם יש עותק — מאחדים, ולא מוסיפים שלישי.* ושלוש נקודות בדיקה שעלו כאן: הוובאפ מריץ **חיבור מונגו משלו** ואינו עובר דרך `database/repository.py`, ולכן שכפול בין הבוט לווב אינו נראה בקריאת קוד אחת; תוספי JS מועתקים בין `live-preview.js` ל־`md_preview.html`; וכל תג `