What’s New
- summary:
יומן השינויים של הבוט וה-WebApp לפי תאריך — מה נוסף, מה השתנה ומה תוקן בכל עדכון, עם קישורים ל-Issues הרלוונטיים.
2026-09-27
fix (scripts): שיא הפרסור נמדד מאיפוס של ``VmHWM``, על 40 סידורי זיכרון, ונשפט כחסם עליון — וכל מספר שנמדד בשיטה הישנה נמדד מחדש (#3391). מאז #3429
scripts/measure_md_parse_cost.pyהפחית מהשיא את הגבוה מבין ה-RSS שלפני הפרסור לבין השיא שהתהליך כבר הגיע אליו, ולכן זיכרון שהתהליך תפס ושחרר לפני הפרסור — בעיקר החוצץ של קריאת הקובץ — הסתיר את מה שהפרסור הקצה מתחתיו: כחצי MB על קלט של 512KB וכ-10MB על outline של 10MB, תמיד לכיוון ”נכנס“. עכשיו הילד המודד כותב5ל-/proc/self/clear_refsרגע לפני הפרסור, מוכיח בכל ריצה שהשיא אכן אופס, ועוצר בקול כשאי אפשר — בלי נפילה שקטה לשיטה הישנה. שתי הטיות נוספות, לאותו כיוון, נסגרו איתו: השיא של אותו פרסור תלוי בסידור הזיכרון שלפניו (עד כ-0.9MB), ולכן כל מדידת זיכרון רצה על 40 זרעיPYTHONHASHSEEDקבועים (LAYOUT_SEEDS); ומוני ה-RSS של הקרנל מפגרים אחרי השיא האמיתי, ולכן מה שנשפט הוא חסם עליון — המקסימום ועוד הפיגור שמוכח מקוד הקרנל (counter_lag_bound_bytes), כשהילד המודד מוצמד למעבד אחד. ההצמדה היא של המדידה בלבד, לא של השרת. מה יצא: הקלט הגרוע תחת התקרות — חסם עליון של 95.9% מהעלות שמאגר הקריאות מקצה לחוט, כלומר 69.02 בתים לכל בית של תקרת הקריאה מול הקבוע 72: מרווח של 4.1% בלבד.CLOUD.mdמשוכפל עד תקרת הקריאה, בלי התקרות, עולה 72.4 עד 72.8 — מעל 72, שנגזר ממנו ב-#3429 — ולכן הקבוע מחזיק בזכות התקרות של #3391 ולא מעבר להן; הוחלט להשאיר אותו ולכתוב את זה לידו. RST עוין — 44.3MiB בלי תקרה ו-20.2MiB איתה; outline של 10MB לאורך המסלול כולו — 80.8 עד 91.6MiB; קובץ של 12MB לפני #3433 — 22MiB. הפרטים, הפיזור ואיך החסם נבנה — ליד_PARSE_RSS_PER_INPUT_BYTEוב“מודל הריצה של הכלים“ ב-שרת ה-MCP — חיבור Claude ל-CodeKeeper; השיטה — סקריפטים שימושיים. המספרים שנכתבו בשיטה הישנה ברשומות של #3429 ושל #3433 למטה נשארים כפי שנכתבו אז.fix (MCP): שתי תקרות בפרסר ה-Markdown — שורות לפני הפרסור, וטוקנים בכל טוקן שנוצר (#3391). עד עכשיו התקרה היחידה בתוך
md_parserספרה כותרות, וקובץ עוין בלי אף כותרת עבר אותה בלי סירוב: 500KB של שורות-תבליט עלו כ-142.5MiB לפרסור אחד, וקובץ של 128KB עם הרבה טבלאות רחבות הגיע ל-3GB ונפל ב-MemoryError— התקרה של upstream לתאים משלימים (#364) היא לכל טבלה, והמונה מתאפס בכל טבלה חדשה. עכשיוparse_documentמסרב לקובץ ארוך מ-MAX_LINESלפני הפרסור —too_many_lines, עםmaxו-total_lines— ועוצר ב-MAX_TOKENSברגע שנוצר הטוקן החורג, גם באמצע טבלה ובאמצע רשימה —too_many_tokens, עםmaxו-line. הבדיקה יושבת ב-appendשל רשימת הטוקנים עצמה, שכלל core מתקין לפניblock, ולא בבנאי שלStateCore, ששם רשימה ריקה מוחלפת בשקט (tokens or []); ומעקה אחרי הפרסור מוודא שהרשימה שחזרה היא זו שהותקנה, אחרתRuntimeError. שני המספרים נבחרו יחד, משני תנאים: הקלט העוין הגרוע שנמצא — ציטוט בעומק 19 בכל שורה,\r\nותו אסטרלי בכל שורה, וטבלה שעוברת את התקרה בתוך הציטוט — נכנס בעלות שמאגר הקריאות מקצה לחוט — החסם העליון שלו 95.9% ממנה (המדידה ברשומה שמעל) — ואף קובץ אמיתי אינו נחסם: הארוך ביותר 6,151 שורות, והצפוף ביותר 38,153 טוקנים. מגבלת הקצב יורדת מ-60 ל-45 בדקה: הטענה שהיא שומרת עליה — זהות אחת אינה עוברת דקת מעבד בדקה — נבדקת עכשיו מול הקלט הגרוע תחת התקרות,WORST_CASE_CPU_SECONDSב-md_parser, ו-tests/test_md_parse_worst_case_claims.pyגוזר ממנו גם את החשבון של הדדליין שלcodekeeper_read_batch. המדידה הישנה שהחשבונות האלה נשענו עליה (2.3 שניות, #3431) הייתה מוקלדת בכמה מקומות והתיישנה; עכשיו יש לה בעלים אחד.MAX_SECTIONSאינה יכולה להיפגע יותר ב-Markdown עם ברירות המחדל — כל כותרת צריכה שורה, ו-MAX_LINESקטן ממנה — והיא נשארת בשביל RST.scripts/measure_md_parse_cost.pyמודד עכשיו גם את הקלט העוין כמו שהכלי מקבל אותו, עם זמן מעבד, ויוצא בקוד 1 כשהזיכרון או המעבד עוברים את הקבועים; ו---doublingבודק שכל צורה עוינת ליניארית — הבדיקה שהייתה תופסת מראש את הריבועיות של #367. האורקל מול cmark-gfm משווה בלי התקרות, בכוונה, ובשורה של הטבלה שעוברת את תקרת התאים כתוב שהפער אינו נגיש דרך הכלי: הוא מתחיל רק אחרי 196,608 טוקנים. #3466 נשאר פתוח, אבל החומרה שלו יורדת: עלות כל פרסור חסומה עכשיו, מכל מקור. ראוtoo_many_linesו-too_many_tokensב-איזה קובץ docs_get_section קורא, ובאיזה ריפו.fix: שמירה שומרת בדיוק את מה שנשלח — הנרמול יצא משכבת השמירה, ונשאר ניקוי מינימלי רק לקוד שמודבק בבוט.
normalize_codeרץ מאוקטובר 2025 (#637) בתוךRepository.save_code_snippet,save_fileו-save_large_file, ולכן שכתב בשקט כל קובץ מכל כניסה — גם מה-MCP, מהוובאפ ומהעלאת מסמך לבוט: הוא מחק LRM ו-RLM ממסמכים בעברית, מחק רצפי escape טקסטואליים מקוד מקור (#643) כך שקבוע קיבל ערך אחר ורגקס נשבר, מחק את כל ה-newlines בסוף הקובץ (#1662) ורווחים בסוף כל שורה, ופירק אמוג’י שמחוברים ב-ZWJ; ועריכה של מילה אחת ב-edit_fileנרמלה את כל הקובץ. עכשיו ה-Repository, הוובאפ וה-MCP לא משנים תוכן, ו-tests/test_content_cleaning_stays_in_the_bot.pyנופל אם פונקציית ניקוי נקראת מ-database/,webapp/אוmcp_server/. בבוט רץ ניקוי מינימלי אחד,clean_pasted_code, ממש לפני השמירה בכל זרימה — שמירה רגילה ואיסוף ארוך,/save, עריכת קובץ ועריכת קובץ גדול (/saveועריכת קובץ גדול נוקו עד היום רק בשכבת השמירה): סופי שורה של Windows, CR בודד ו-BOM, ובקוד שאינו Markdown גם רווחיZs, ZWSP ורווחים בסוף שורה — הרשימה המדויקת ב-docstring שלclean_pasted_code. הודעת ההצלחה אומרת מה נוקה, ותווי embedding, override ו-isolate נשארים, עם אזהרה על השורות שלהם. גם עמוד הקוד בוובאפ (view_file.html, בתצוגת הבעלים ובשיתוף הציבורי/share/<id>) מציג עליהם אזהרה מעל הקוד, עם מספרי השורות שבעמוד. בוובאפdecode_form_newlinesמבטל את ה-CRLF ששליחת טופס HTML מוסיפה (HTML Living Standard, סעיפים 4.10.22.6 ו-4.10.22.8) — פענוח ולא ניקוי — ב-/uploadוב-/edit/<id>בלבד; קובץ שהועלה לא עובר את הפענוח הזה, ו-upload_file_webרק מפענח את הבתים שלו לטקסט. הוסרו:utils.normalize_code,CodeNormalizer,strip_hidden_escapes, הכלל שמחק ”…“ מכל חלק באיסוף ארוך, והדגלNORMALIZE_CODE_ON_SAVE(ערך שנשאר מוגדר בסביבה פשוט לא נקרא). למה אף טסט לא תפס: תחת pytest ה-Repository קורא אתtests/config.py, שאין בו את הדגל, וה-AttributeErrorנבלע ב-except Exception: pass— כלומר בטסטים הנרמול בשכבת השמירה אף פעם לא רץ. הטסטים החדשים טוענים אתconfig.pyהאמיתי. נתונים שכבר נשמרו לא משתנים. ראו ניקוי קוד מודבק (Pasted-code cleanup).fix: סל המיחזור מתרוקן שוב — אינדקס ה-TTL חזר למסלול העלייה. כל מחיקה רכה כותבת
deleted_expires_at, והעמוד/trashמציג אותו כ“נמחק סופית ב-“, אבל אף קוד אפליקציה אינו מוחק בתאריך הזה: את העבודה עושה אינדקס TTL בצד השרת. ב-1 בינואר 2026 (#2525) רשימת האינדקסים שבעלייה נכתבה מחדש כרשימה ”מינימלית“ של אינדקסי ביצועים, והאינדקס הזה לא נכנס אליה; האשכול שהוקם אחר כך קיבל רק את מה שהעלייה יוצרת, ולכן מעולם לא היה בו TTL — לא ב-code_snippetsולא ב-large_files— ופריטים שתאריכם עבר נשארו בסל. עכשיוDatabaseManager._create_recycle_bin_ttl_indexesיוצר אותו בכל עלייה, והמפרט יושב ב-file_deletion.py: חלון 0 (מחיקה בדיוק בתאריך המוצג), ומסנן חלקי עלis_active: false— כך שגם מסלול שחזור ששוכח לנקות את התאריך לא יגרום למחיקת קובץ פעיל. כשל ביצירה יוצא כאירועdb_recycle_bin_ttl_index_missingברמת error ולא נבלע. ביצירה הראשונה מונגו מוחק בבת אחת את כל מה שכבר עבר — זו החלטה ולא תאונה, והיא כתובה ליד היצירה./recycle_backfillקורא לאותה פונקציה ומדווח את מצב האינדקס לכל קולקציה, במקום לבלוע את הכשל ב-except: pass; ובמצב שבו המסד לא עלה הוא מסרב, במקום לדווח ✅ על כלום. בעמוד/admin/verify-indexesנוסף הסעיףrecycle_bin_ttl: האם האינדקס קיים ותואם למפרט, וכמה פריטים שתאריכם עבר עדיין בסל. ראו אינדקסים שהם התנהגות, לא אופטימיזציה.chore (MCP/docs): הפארסר של ה-Markdown עולה ל-markdown-it-py 4.2.0 — והפערים הידועים מ-GitHub יושבים עכשיו בטבלה אחת. הנעיצה עלתה מ-3.0.0 ל-4.2.0, ואיתה
mdit-py-pluginsל-0.6.1 ו-myst-parserל-5.1.0 (הוא זה שחסם: 4.0.1 דרשmarkdown-it-py~=3.0). למה עכשיו: לפני ש-md_parserיפרסר גם תוכן שמשתמשים כותבים, שני תיקונים של upstream ב-4.0.0 סוגרים עלויות שהגרסה הקודמת נשאה — הגדרות קישור רצופות נסרקו בזמן ריבועי (#367), וטבלה עם כותרת רחבה הושלמה תא-תא בלי תקרה (#364). איך נבדק: סקריפט חדש,scripts/md_parser_upgrade_zero_diff.py, רץ על שתי הגרסאות — כל מפות ה-.mdוה-RST בריפו הזה וב-amir-bug-patterns, תשובות הסירוב, צורות עוינות, וכל משפחות הצורות של האורקל מול cmark-gfm — והפלט זהה, חוץ משתי מחלקות שהוכרזו מראש בהרצה ונבדקו עד הסוף. הראשונה: רשימת תגיות הבלוק של CommonMark 0.31.2, שממנהsourceיצאה ובהsearchנמצאת.markdown-it-pyעובד לפיה מאז 4.0.0 ו-cmark-gfm עוד לא, ולכן שורה שמתחילה באחת מהתגיות האלה יכולה להזיז כותרת — 28 מתוך 60 צורות שנמדדו חולקות היום על GitHub, ופער אחד נסגר (<search>שלמה אחרי פריט רשימה, שב-3.0.0 הייתה מופע של #434). השנייה, שנמצאה בסקירה: התקרה של #364 עצמה — טבלה שעוברת את מספר התאים המשלימים המותר נחתכת, והשורות שנשארו בחוץ יכולות להפוך לכותרת setext שאין ב-GitHub. הפערים מקובעים, לא מתוקנים: הם בטבלה_KNOWN_DIVERGENCESב-tests/test_md_parser_oracle.py, לצד הפער הוותיק של תגית מסוג 7 הצמודה לשורת מכל (#434) ולצד שני פערים שהסקירה מצאה ושקדמו לשדרוג (3.0.0 מחזיר בדיוק את אותן מפות): כותרת setext שצמודה להגדרת קישור, ותו רווח ש-\sשל פייתון מזהה ו-cmark-gfm לא — כמו NBSP — אחרי שם של תגית HTML. לכל שורה: הסיבה, צורה ומה כל צד מחזיר עליה, על מה ומתי נמדדה, ומשפחת צורות קבועה באורקל. לידן משפחה קבועה של כל תגית בלוק, בכל צורות הכתיבה וההקשרים, כדי ששינוי ברשימת התגיות של אחד הפארסרים יתגלה בשדרוג הבא בלי שמישהו יחשוד בו מראש. איפה מפת ה-Markdown חולקת על GitHub מעתיק מהטבלה, עם טסט שמשווה בכל שורה. והטענה שאף קובץ אמיתי אינו מושפע נבדקת: טסט ב-CI סורק את עמודי ה-.mdתחתdocs/לשורות שמפעילות שורה בטבלה שיש לה טריגר, מחוץ לבלוקי קוד ומחוץ לבלוק HTML שנפתח בשורה קודמת (כמו<source>בתוך<video>) — לפי הפארסר, ועם קבצים שתולים שמוכיחים שהוא תופס ולא תופס בהתאם.scripts/compare_md_parser_to_cmark.pyמריץ את אותה סריקה ואת השוואת המפות על כל קורפוס, ונופל על כל אחת מהן — אפס עלdocs/, עלamir-bug-patternsועל הריפו כולו. והקבצים השמורים נמדדו פעם אחת במסד, והתוצאה מתוארכת בתיעוד. בדרך תוקן גם הטוען של הסקריפט הזה, שלא רשם את מודול האורקל ב-sys.modulesולכן נפל על@dataclass— ועכשיו גם מסיר את הרישום כשהטעינה נכשלת. עלות הפרסור נמדדה מחדש ולא זזה (_PARSE_RSS_PER_INPUT_BYTEנשאר 72). ראו איפה מפת ה-Markdown חולקת על GitHub.
2026-09-23
feat (MCP): ``codekeeper_read_batch`` — כמה סעיפי תיעוד וקבצים מהמראות בקריאת כלי אחת (אדמין). סוכן ריוויו קורא לפני כל סבב את כל הדפוסים ב-
amir-bug-patternsשהטריגר שלהם נדלק, ובסבב שבו הוא ביקש את הכלי אלה היו 16 קריאות. PostHog הראה שהן מגיעות לשרת אחת אחרי השנייה ולא יחד: ארבעת הפרצים של הסבב נמשכו כ-11.5 שניות, והשרת עבד מתוכן כחצי שנייה. הכלי מקבל רשימת פריטים — סעיף (הארגומנטים שלcodekeeper_docs_get_section) או קובץ (שלcodekeeper_get_repo_file) — וכל פריט עובר דרך ה-handler של הכלי שהוא משקף, ולכן התשובה שלו זהה בית-בית לזו של הכלי הבודד, כולל סירובים, וכישלון של פריט אינו כישלון של הקריאה. commit אחד לכל ריפו (RepoBackend.snapshot,GitMirrorService.resolve_commit), ו-resolved_commitבכל פריט; קריאה ופרסור אחד לכל קובץ, עםlinesבתוך מפתח הקבוצה כדי שפריט סעיף לעולם לא יפרסר טקסט שנקרא תחת תקרת הטווח; תקציב בתים בצורה שה-SDK שולח, פריט שלם או כלום (item_too_largeעםread_with), והמשך ב-unreadעםunread_reason(byte_budgetאוtimeout); דדליין של 10 שניות מהכניסה ל-call_tool, שנבדק בין פריטים. כל פריט נספר כקריאה אחת במכסה, הכל או כלום, לפני שדבר נקרא:RateLimiterקיבלweight(ברירת המחדל 1 משאירה את הבוט כמו שהיה), והתקרה,MAX_BATCH_ITEMS, נבדקת לפני השקילה ויורדת עם המכסה כשהיא נמוכה ממנה._meta["anthropic/maxResultSizeChars"]מוצהר, כדי ש-Claude Code לא ישמור את התשובה לקובץ. בדרךdocs_get_sectionהתפרק לחלקים שהבאץ« משתמש בהם (resolve_docs_target,load_document,document_from_read,answer_section) — ו-scripts/docs_section_zero_diff.pyהראה אפס דיף לפני ואחרי, על קורפוס ה-RST ועל קורפוס ה-Markdown, גם דרךRepoBackendאמיתי ועם תשובות הסירוב. המדידות שהתיעוד מצטט מגיעות מסקריפט חדש,scripts/measure_read_batch.py. כלי הקריאה הקיימים עדיין נשמרים לקובץ מעל סף הפלט של הלקוח — אישו #3460. ראו קריאה קבוצתית — codekeeper_read_batch.fix (WebApp): רשימות הקבצים — שורה אחת לכל קובץ, ו“שאר קבצים“ לא נופל יותר על תקרת הזיכרון של המסד. שלושה תסמינים, שני שורשים. (1) ”מועדפים“ ותצוגת ריפו ספציפי הציגו שורה לכל גרסה — 30 קבצים מועדפים הוצגו כ-65 שורות — כי הענף שלהם לא קיבץ לפי שם קובץ. עכשיו כל קטגוריה שנשלפת מ-
code_snippetsעוברת באותו בנאי,_latest_version_per_file_stages, והספירה ב“מציג X מתוך Y“ סופרת קבצים ולא גרסאות. במועדפים הכלל הוא של הקובץ: קובץ נכנס אם איזושהי גרסה פעילה שלו מסומנת, ומוצגת הגרסה האחרונה שלו. (2) ”שאר קבצים“ החזיר 500 עם שגיאה 292 (QueryExceededMemoryLimitNoDiskUseAllowed): שלב$addFieldsשישב בין ה-$matchלהורדת השדות הכבדים גרם למונגו למיין מסמכים מלאים, כולל גוף הקובץ — 35.8MB מול תקרה של 32MB באשכול Flex, שמתעלם מ-allowDiskUse. עכשיו רק$matchבא לפני ההורדה, והקיבוץ בוחר את הגרסה האחרונה ב-$topבלי מיון מקדים; על נתוני הפרודקשן הצינור רץ ב-26ms. אותו שורש הפיל גם את ”כל הקבצים“ בכל טעינה במשך 17 יום, אלא שמסלולfindעוקף הסתיר את זה — והמסלול הוסר: כשל כזה יופיע מעכשיו כשגיאה, ולא כעמוד איטי שמציג בחירת גרסאות אחרת. ובאותו תיקון: הוספה והסרה ממועדפים בבחירה מרובה חלות על כל הגרסאות הפעילות של הקובץ, ו-updatedסופר קבצים; עריכה בדפדפן, העלאה, שמירת מדריך משותף, Incident Story ושמירה מאוסף משותף מעבירות את הסימון לגרסה החדשה, דרך פונקציה אחת ב-file_favorite.pyשכל שבעת מסלולי הכתיבה קוראים לה — והיא שואלת את המצב של הקובץ, כלומר את כל הגרסאות הפעילות ולא רק את האחרונה, כמו הרשימה; עריכה ששינתה שם שואלת גם את השם הקודם;Repository.save_file(עריכה בבוט, ייבוא מ-GitHub ומ-ZIP) קורא את הגרסה הקודמת מהמסד ולא דרך הקאש — עד היום סימון מועדף שהוסר יכול היה לחזור לחיים,favorited_atיכול היה להיכתב כמחרוזת, ותיאור או תגיות ששונו בוובאפ יכלו להתהפך בשמירה הבאה מהבוט; סימון והסרה בוובאפ, אחד-אחד ובבחירה מרובה, מבטלים את הקאש של המשתמש, כך שקובץ שהוסר לא חוזר ברענון מעמוד המועדפים ששמור בקאש (עלות הניקוי ופתרון השורש שלה — #3402); גרסת המינימום של MongoDB עלתה ל-5.2, בגלל$top— התקנה והגדרה (התיעוד אמר 4.4, אבל$setWindowFieldsב-search_engine.pyדרש 5.0 עוד לפני כן); תצוגת ריפו ספציפי ו“שאר קבצים“ מסננות לפי התגיות של הגרסה האחרונה של כל קובץ, כמו רשימת הריפואים — עד היום הן סיננו גרסאות, ולכן קובץ שעבר לריפו אחר, או קובץ ידני שיובא אחר כך מ-GitHub, הופיע גם בקטגוריה הישנה, בגרסה הישנה; עמוד הקובץ והכוכב שבו מציגים ומחליפים את המצב של הקובץ ולא של הגרסה שנפתחה — עד היום בקובץ שרק גרסה ישנה שלו מסומנת הכפתור אמר ”הוסף“ והלחיצה השאירה אותו במועדפים, ובעמוד של קובץ בינארי או גדול לתצוגה הכפתור אמר תמיד ”הוסף“; המצב נכנס גם ל-ETag של העמוד, כדי שהדפדפן לא יקבל 304 עם כוכב ישן; ”נפתחו לאחרונה“ רושם לוג כשהשליפה נכשלת, במקום להציג בשקט רשימה ריקה; והמיון האחרון ב-codekeeper_list_filesרץ על מטא-דאטה בלבד — עד היום בעמוד האחרון הוא מיין גם את גוף הקבצים, עד 19MB מתוך אותה תקרה.docs (MCP): ``codekeeper_docs_get_section`` אומר איך קוראים קובץ שלם. תיאור הכלי, שורת הטבלה ב“הכלים“ וה-README אומרים עכשיו את מה שהקוד כבר ידע:
sectionעם הכותרת הראשונה בעץ מחזיר את תת-העץ שלה (תת-הסקשנים כלולים כברירת מחדל; כל הקובץ כשהוא כולו תחת כותרת עליונה אחת, כמו קובציbugbot-rules), מדופדף כמו כל סעיף — כל עודtruncatedהואtrueממשיכים מ-next_offset— ולאדמין יש גםcodekeeper_get_repo_file. עד היום התיאור הזכיר אתcodekeeper_get_repo_fileרק כדבר שעדיף לא להשתמש בו, וקורא שאינו אדמין לא ידע שיש לו דרך לקרוא קובץbugbot-rulesשלם בלי סיבוב של עץ כותרות. ה-README גם אומר שהכלי קורא Markdown ולא רק RST. ובאותו PR: הבלוק ”דפוסי באגים“ ב-CLAUDE.mdמונה אתcodekeeper_get_repo_fileלקריאת קובץ שלם מ-amir-bug-patterns, לצדcodekeeper_docs_get_sectionו-codekeeper_search_repo.
2026-09-22
feat (MCP): ``codekeeper_get_note`` — פתק בודד לפי מזהה, ו-``include_content`` ברשימות הפתקים. עד היום לא הייתה דרך לקרוא פתק אחד:
codekeeper_search_notesמחזיר מזהה בלי תוכן,codekeeper_get_note_versionמחזיר תוכן רק לגרסאות קודמות, וכלי הרשימה מחזירים את המשטח כולו — בלוח ”Ck MCP“ עם 18 פתקים זה 64,691 עד 66,416 תווים, מעל מגבלת הפלט של הלקוח, כדי להגיע לפתק של 4,997 תווים; סוכן בלי shell לא יכול היה לקרוא את הפתק בכלל, וכל עריכה ב-codekeeper_note_str_replace, שדורש את הגוף הנוכחי המדויק, חייבה משיכה של הלוח כולו. הכלי החדש מחזיר את הגוף הנוכחי בדיוק כפי שהוא מאוחסן — פתק ישן עם"חוזר עם הישות, כמו ב-codekeeper_get_note_version, בעוד כלי הרשימה ממשיכים לפענח — את הכותרת והצבע, אתversion(המספר שבוcodekeeper_get_note_versionמחזיר את הגוף הזה — עכשיו, או אחרי הדריסה הבאה דרך השרת הזה; עריכה מהוובאפ אינה נכנסת להיסטוריה), ואיפה הפתק יושב בדיוק בארגומנטים שכלי הרשימה המתאים דורש, עםorphanedעל פתק ריפו כמו ב-codekeeper_list_repo_notes. זו אותה קריאה-לפי-מזהה ש-codekeeper_note_str_replaceכבר עושה, ולא מסלול שני, והיא סוגרת את הלולאה שלו:conflict←codekeeper_get_note← ניסיון חוזר; נבדק שהשער האופטימי משווה תוכן בלבד, ולכן הגוף הנוכחי הוא כל מה שהניסיון החוזר צריך. ההרשאה נבדקת לפי סוג הפתק לפני שתוכן חוזר: פתק של משתמש אחר, ופתק על קובץ בריפו למי שאינו אדמין, מחזיריםnot_found— לא סירוב שמגלה שהמזהה קיים — ולכל אחד מהם טסט שנופל כשהבדיקה מוסרת.codekeeper_list_notesו-codekeeper_list_board_notesמקבליםinclude_content(ברירת מחדלtrue, בלי שינוי לקוראים קיימים);falseמחזיר לכל פתק רק מזהה, כותרת, צבע (colorו-color_id),updated_atו-content_bytes— בבתים של UTF-8, ביחידה נקובה, כי בפתק עברי זה פי שניים מספירת תווים. תיאוריcodekeeper_search_notesו-codekeeper_note_str_replaceמפנים ל-codekeeper_get_note, ושני כלי הגרסאות מצביעים זה על זה כדי שלא ייווצר ספק מי מחזיר את ההווה; המשפט בתיעוד שאמר שאת הפתק קוראים ”בכלי הרשימה“ תוקן, וההחלטה שפגיעת חיפוש אינה נושאת תוכן נשארת. הכלי אינו נושאck_read_modeבדשבורד, בכוונה — התווית היא לכלי קריאת קובץ בלבד, וכלי שאינו במפה אינו נספר כקריאה מלאה. ממצא בדרך:OUTPUT_BYTE_BUDGETאינו נאכף על רשימות פתקים כלל — לוח שלם עבר מתחתיו, ומה שחסם היה הלקוח; אכיפה שם היא שינוי נפרד, רשום ב-#3457 יחד עם שאר הפריטים שנדחו מה-PR. ומה שסקירת ה-PR תיקנה בו לפני המיזוג: הגוף והגרסה נקראים תחומים — הפתק נקרא, ההיסטוריה נקראת, והפתק נקרא שוב, והזוג חוזר רק אם שום כתיבה לא נגעה בו בין השתיים (אותה הוכחה משולשת של תוכן,updated_atו-write_idשמסלול הכשל שלupdate_noteמשתמש בה), אחרת קוראים שוב ולבסוףconflict; המסלול הרזה שלinclude_content=falseהוא צינור אגרגציה שבו$strLenBytesמודד את הגוף במסד ומוריד אותו לפני המיון, ולאfindשמושך גופים כדי לזרוק אותם; גוף ריק, שההיסטוריה מדלגת עליו, נושאversion: nullולא מספר שלעולם לא יחזיר אותו; ומזהה פתק שהוקלד באותיות גדולות מגיע לכל כלי בצורתו הקנונית — קודם הוא עבר את השער, מצא את הפתק, וחיפש היסטוריה שלעולם לא תימצא. ומה שסקירה שנייה, בלתי תלויה, תיקנה לפני המיזוג: ההבטחה עלversionנוקבת בתנאי שלה בכל מקום שבו היא כתובה — תיאורי שני הכלים, עמוד ה-MCP וה-README — (דריסה דרך השרת הזה; עריכה מהוובאפ אינה נכנסת להיסטוריה), והפסקה שתיארה את החישוב תואמת את הקוד; רשימת שדות השורה הרזה חיה פעם אחת (LEAN_NOTE_FIELDS) ותיאורי הכלים נגזרים ממנה;lengthברשימת הגרסאות נוקב ביחידה שלו (תווים) מולcontent_bytes(בתים); המשפט עלOUTPUT_BYTE_BUDGETמדויק — עץ, אאוטליין וחיפוש, ו-queryבקבוע משלו;_next_note_versionנמחק לטובת קריאה ישירה אצל הקורא היחיד; ושני טסטים חדשים — פתק שנמחק בין שתי הקריאות שלcodekeeper_get_noteעונהnot_foundועוצר, וצורת השאילתה של רשימת פתקי קובץ ($orעם$in) רצה במסלול הרזה מול מונגו אמיתי (אחרי מיזוג, ב-deploy.yml). ראו קריאת פתק בודד — codekeeper_get_note.
2026-09-21
fix (MCP): סגירת ממצאי סקירת שבעת ה-PRים #3434–#3441 — תקרת הגוף מפסיקה לקרוא גוף אנונימי לפני האימות, ועוד ארבע-עשרה הצעות. הרגרסיה (SEC-001): תקרת הגוף מ-#3431 קראה כל גוף עד 1MiB לפני שהשכבה הבאה ראתה את הבקשה — במצב OAuth גם גוף אנונימי שהאימות של ה-SDK דוחה מהכותרות בלבד, ובלי דדליין (ל-uvicorn אין timeout לגוף בקשה): נמדדו 5 קריאות
receive()לפני ה-401 על גוף של חמישה נתחים, גם עםContent-Length. שלושה כללים, כל אחד עם טסט שנפל לפני שנכתב:Content-Lengthתקין (ספרות בלבד, בליTransfer-Encoding) עובר בלי קריאה, כי השרת תוחם את הגוף לאורך המוצהר — 0 קריאות לפני ה-401; גוף בלי אורך מוצהר נקרא עד התקרה תחת דדליין של 30 שניות על הלולאה כולה, ואז408 body_read_timeoutשאומר כמה נקרא; ורקPOST/PUT/PATCHנבדקות, ולכןGET /healthzעםContent-Lengthמזויף מחזיר200ולא413(WARN-002). שני הסירובים נושאיםConnection: close, וטסט סדר-התקנה למצב OAuth עומד ליד זה של מצב PAT. התקרה נגזרת:DEFAULT_MAX_REQUEST_BYTESהיה מוקלד לצד הערה שגוזרת אותו ביד; עכשיוrequest_bytes_for(MAX_CODE_SIZE)— פי שישה ועוד מעטפת, מעוגל ל-MiB — גם בעליית השירות מהתצורה בפועל, וטסט בונה את בקשת השמירה הגדולה ביותר (קובץ עברי בגודל המקסימלי) ומודד שהיא נכנסת (SUGG-022). המגביל: הסירוב הואCallToolResultשלם ולא ערך שעובר ב-convert_resultשל הכלי, ולכן כלי עם טיפוס החזרה מוצהר או שם כלי לא מוכר מקבליםrate_limitedולאValidationErrorאו שגיאת SDK (SUGG-009);is Noneבמקוםorבבחירת המגביל, כדי ש-ToolRateLimiter(0)לא יוחלף בשקט ב-60 בדקה ברגע שיתווסף__len__(SUGG-023); טסט מקבילי — 25 קריאות בו-זמנית, תקציב 2, בדיוק 2 עוברות (SUGG-024);RateLimiterאינו יוצר עוד רשומה לזהות ששאלו עליה בלבד, ופנקס האזהרות מנוקה (SUGG-011);ToolRateLimiter.enabledנמחק (YAGNI-001) ו-MCP_MAX_REQUEST_BYTESנשאר כהחלטה סגורה — לא kill switch אלא דרך להעלות את התקרה בלי דיפלוי (YAGNI-002). החיווט נבדק: טסט בתת-תהליך מייבא אתmcp_server.appכמו uvicorn, בשני מצבי האימות ועם ריצת בקרה, ומוודא ששני משתני הסביבה ו-MAX_CODE_SIZEמגיעים לאפליקציה הבנויה (WARN-001). ומה שלא נספר: סירובי מכסה אינם נספרים ב-PostHog — ההכרעה קורית לפני התפר שהמדידה עוטפת, וגם סירוב מגוף כלי נרשם שם כהצלחה — התיעוד אומר זאת עכשיו במפורש, והעיצוב פתוח באישו #3442 (WARN-003). קטנות באותו PR:token_countו-front_matter_endעוברים באותו שער כניסה כמוparse_document(BOM,\rבודד, טיפוס), ולכןgenerate_ai_mapאינו יכול עוד לקבל גבול front matter שחולק עלsplit("\n")(SUGG-019, SUGG-013); הצורה העוינת בסקריפט מדידת הפרסור רצה עםmax_sections=Noneבמפורש, כי עם התקרה היא מדדה עצירה (41.0 בתים לבית) ולא את הפרסר (90.1) (WARN-004);_CANDIDATES_MAXמיובא מ-doc_sectionsבמקום להיות מוקלד (SUGG-002); תנאי מת ב-utils.normalize_codeומספר מוקלד בטסט של שורת הקיבולת הוסרו (SUGG-021, SUGG-020); והערות עם מספרים שהתיישנו אחרי המעבר לפרנקפורט אומרות זאת בתאריך (SUGG-005, SUGG-006, SUGG-010). עשר ההצעות שנדחו — באישו #3442. ראו גבולות הבקשה — גודל הגוף והקצב.chore (MCP): יתרת ממצאי הסקירה על #3428 (#3432). שני סירובים חדשים שנוקבים בסיבתם:
path_outside_rootב-codekeeper_docs_get_section(עד היום נתיב שיצא מהשורש חזר כ-missing_path, ו-missing_pathנשאר לנתיב ריק בלבד) ו-repo_not_mirroredמ-RepoBackend.get_file(עד היום מארח בלי עותק של הריפו חזר כ-not_found, וסוכן ניסה שם אחר בזמן שהבעיה אצל המפעיל). טבלאות המדיניות של הכלי (DOCS_PATH_POLICY,_PARSERS) מיוצאות כ-MappingProxyType, כי הוולידציה בייבוא היא הבטחה רק אם הטבלה אינה משתנה אחריה; זנב עיצוב התשובה שלdocs_get_sectionנפרד ל-_answer_from_document(אפס-דיף מדוד על 208 קובצי ה-RST); שתי בדיקות הנרמול של הוולידטור ושורת הלוג עלrepo_not_configuredקיבלו טסטים; טסט ההסכמה בין חיפוש לקריאה כבר אינו דורש שוויון מדויק (המדיניות מצהירה ש-is_deniedנשאר שכבה אחרונה), ושני הטסטים תלויי-git מדלגים בלי git באותה צורה. החלטה מפורשת על לוגים לסירובים: סירוב הוא תשובה של הפרוטוקול ולא תקרית, ואף מסלול סירוב בשכבת המטפלים אינו רושם שורה — מתועד ב-שרת ה-MCP — חיבור Claude ל-CodeKeeper. שני ממצאים נדחו בנימוק: תצוגת התצורה אינה יכולה לייבא אתdocs_handlers(הכיווןservices←mcp_serverחד-סטרי), ושני חצאי מדיניות הסודות נשארים בשתי שפות כי אין מנוע אחד לשניהם — מה שמחזיק אותם יחד הוא רשימת תבניות אחת ושני טסטים.
2026-09-20
fix: ``codekeeper_docs_get_section`` חוסם את רשימת המועמדים של ``ambiguous_section`` ב-50 (#3426). זה היה השדה היחיד בתשובה בלי תקרה:
tocחסום ב-400 ו-suggestionsב-50, ורק המועמדים חזרו כולם — נמדד 13,030 מועמדים ו-1.4MB על שאילתה בת שלושה תווים בעמוד סינתטי. הענף נדלק מכותרות שנושאות מזהה, ומאז #3428 הקורפוס שנושא אותם מוגש. אותו מספר כמו ההצעות ומאותה סיבה (מלאי בסדר הופעה, לא דירוג), והדגלcandidates_truncatedמופיע רק כשנחתך — כמוsuggestions_truncated— ולכן תצלום אפס-הדיף של הכלי על 208 קובצי ה-RST זהה לפני ואחרי. ובאותו PR: טסט שמקבע שגם במסלול ה-Markdownsuggestionsהוא רשימת מחרוזות ולא ה-NamedTuple(הצרכן השני מ-#3426 הוא אותו אתר קריאה, והוא כותב.titles).fix (MCP): תקרת גודל לגוף בקשה והגבלת קצב לפי זהות, עם סירוב שנוקב בסיבתו (#3431). עד עכשיו המידלוור היחיד היה האימות, וכל משתמש מאומת יכול היה לשלוח בקשות בכל גודל ובכל תדירות. שני גבולות חדשים ב-
mcp_server/limits.py: גוף בקשה מעל 1MiB (MCP_MAX_REQUEST_BYTES) נדחה ב-413 body_too_largeבמידלוור ASGI, לפני שהטרנספורט מפענח JSON — לפיContent-Lengthואם היא חסרה או מכזבת, לפי מה שבאמת מגיע; וקריאות כלים מוגבלות ל-60 בדקה לזהות (MCP_RATE_LIMIT_PER_MINUTE) בנקודת השיגור של הכלים — המקום היחיד שרואה את הזהות גם במצב OAuth — ולפני שנתפס חוט, עםrate_limitedו-retry_after_secondsבתשובה. המגביל הואrate_limiter.RateLimiterהקיים של הבוט, לא עותק. ``/healthz`` פטור מבנית משניהם (blanket-policy-silent-block§7), וזה מקובע בטסט. המספרים נגזרו מהמדידות ב-#3431: 0.24 שניות מעבד לעמוד RST עוין של 500KB, 0.47 למסמך ה-Markdown הצפוף, 2.3 לצורה העוינת, מול מכסה של 0.5 מעבד. ראו ”גבולות הבקשה“ ב-שרת ה-MCP — חיבור Claude ל-CodeKeeper.fix (mirror): ``get_file_at_commit`` בודק את גודל הקובץ במאגר האובייקטים לפני ``git show``, ולכן קובץ מעל התקרה אינו נטען לזיכרון כלל (#3433, פריט 9). עד היום
git showנטען כולו לזיכרון ורק אזmax_sizeנבדק — כלומר התקרה הייתה בדיקה בדיעבד: קובץ של 12MB שנדחה עלה 20MiB שיא בחוט הקריאה לפני שהתשובה הייתהfile_too_large. מעכשיוgit cat-file -s <sha>:<path>מחזיר את גודל האובייקט בלי לקרוא אותו, שתי הפקודות פונות לאותו קומיט שנפתר פעם אחת (אין חלון לסנכרון להיכנס בו), וקובץ מעל התקרה נדחה לפני שנקרא בית אחד — אותה תשובה, אותוsize, אפס זיכרון. כשל בבדיקת הגודל אינו נופל לקריאה בלי תקרה, ומה שמוחזר עדיין נבדק מול התקרה בנפרד, לכל סוג אובייקט. נמדד: 12MB מעל תקרה של 500KB ושל 10MiB — 20.2 ו-20.4MiB לפני, אפס אחרי; 7MB מתחת לתקרה — 14MiB לפני ואחרי, כי אותו כן קוראים. ראו ”מודל הריצה של הכלים“ ב-שרת ה-MCP — חיבור Claude ל-CodeKeeper.chore (MCP): ארבעה פריטים קטנים מסקירת #3429 (#3433). שורת הקיבולת של מאגר הקריאות קוראת את
ThreadPoolExecutor._max_workersהפרטי דרך_installed_width, וכשהמאפיין ייעלם היא אומרת ”unreadable“ לצד הרוחב שהתבקש עם WARNING, במקום שהשירות לא יעלה בגלל שורת לוג (SUGG-004); ``scripts/measure_md_parse_cost.py`` מודד צפיפות דרךmd_parser.token_countהציבורית — על המופע שהכלי מריץ — במקום לקרוא ל-_build_parserהפרטית ולבנות פרסר לכל קובץ (SUGG-010); שני טסטים חדשים מקבעים שקובץmemory.maxשקיים אבל ריק או לא-מספרי נופל ל-cgroup v1 (SUGG-014); ואסרשן שלא יכול היה ליפול הוחלף בטסט שמצמידos.cpu_countל-16 כדי שה-fallback ייבדק מול 20 ולא מול עצמו (SUGG-001).fix (parsers): ``rst_parser.parse_document`` בודק את הקלט בכניסה, וברירת המחדל של ``max_sections`` מיושרת לזו של ``md_parser`` (#3421, #3420). עד היום
rst_parser.parse_document(None)החזיר מסמך ריק בשקט — מפה ריקה שמתחזה למפה של קובץ בלי כותרות — ו-17נפל ב-AttributeErrorגולמי, בעודmd_parserזרקTypeErrorשאומר מה התקבל. מעכשיו שני הפארסרים עוברים באותה בדיקה,doc_sections.require_str, ומרימים את אותה חריגה עם אותן מילים; אף קורא לא נשען על הצורה הישנה (המטפל, הסורק והסקריפטים מעבירים תמיד מחרוזת). ובאותו PR ההכרעה על ברירת המחדל:max_sectionsב-rst_parserהיהNone(בלי תקרה) כי הפרמטר נוסף לפארסר שכבר רץ בייצור, ואילו ב-md_parserהואMAX_SECTIONS— כלומר קורא ששכח להעביר תקרה קיבל הגנה מהפארסר האחד ולא מהשני. עכשיו שניהם על אותה ברירת מחדל,MAX_SECTIONSשעבר ל-services.doc_sectionsומיוצא משני הפארסרים, ו-codekeeper_docs_get_sectionאינו מעביר תקרה לאף אחד מהם — וזה מחליף את מה שכתוב ברשומת הקריאה של Markdown שלמטה (”RST דרך התקרה שהכלי מעביר מאז #3429“): מאז השינוי הזה הכלי אינו מעביר תקרה, וברירת המחדל של הפרסר היא שעוצרת. נמדד, לא הונח: על כל 208 קובצי ה-RST ב-docs/הקובץ העשיר ביותר נושא 50 סקשנים מול תקרה של 50,000 (אחד לאלף), ופלט הכלי על כולם — 5,931 רשומות — זהה בית-בית לפני ואחרי. סורק האאוטליין ממשיך להעביר את התקרה שלו במפורש, וטסט מקבע זאת. ראו ”שני סירובים שמגיעים מהפרסור עצמו“ ב-שרת ה-MCP — חיבור Claude ל-CodeKeeper.refactor: שלושה כללים שנכתבו פעמיים חזרו למקום אחד (#3419, #3427). (1) כלל ה-
\rהבודד — סיומת שורה שאינה חד-משמעית — ישב גם במנתב האאוטליין וגם בפארסר ה-Markdown, ושני העותקים היו צריכים להסכים לנצח; עכשיו הוא במודול עלה חדש,services/line_endings.py, ושני הצרכנים קוראים לו. (2)scripts/generate_ai_map.pyכתב ביד את גבול ה-front matter ונמדד שהוא חולק על התוסף ש-MyST מריץ בחמש מתוך שתים-עשרה צורות; עכשיו הוא קורא את הגבול מהפארסר של הכלי (md_parser.front_matter_end, דרךmdit_py_plugins.front_matter), ו-AI-MAP.mdשנוצר זהה בית-בית לזה שבריפו — הטסט שקיבע את הפער בכוונה נערך יחד עם הסגירה. (3) שתי רשימות זהות של 16 קודי תווים נסתרים — ב-utils.normalize_codeוב-CodeNormalizer— לא הוסיפו דבר על בדיקתunicodedata.category == "Cf"שישבה מתחתן (כל 16 הערכים הםCf); הרשימות נמחקו, שתי הפונקציות אוחדו ל-strip_hidden_escapesאחת בשכבת הדומיין, וענף ה-Variation Selectors (Mn, לאCf) נשאר נפרד ונדלק רק לפי בקשה. לשני הנרמולים אפס-דיף מדוד על קורפוס של רצפי בריחה, בכל צירוף אפשרויות.feat: ``codekeeper_docs_get_section`` קורא גם קובצי Markdown, ולכל ריפו מדיניות נתיבים משלו. עד היום הכלי ידע לקרוא
.rstבלבד, ורק מתחת ל-docs/. הפארסר של Markdown נכתב ונמדד כבר קודם — ואף אחד לא קרא לו; זה השינוי שמחווט אותו. מעכשיו הסיומת בוחרת את הפארסר, ולכל ריפו מוצהר איפה התיעוד שלו יושב ובאיזה פורמט:CodeBotנשארdocs/עם.rstללא שינוי, ו-amir-bug-patternsמגיש.mdמשורש הריפו. וזו הרחבה מכוונת של גבול הקריאה — הכלי ציבורי, כלומר כל משתמש מאומת מפעיל אותו בלי אדמין, והריפו שנוסף ציבורי ב-GitHub. מה ש**לא** השתנה: מדיניות סינון הסודות חלה על המסלול הזה במלואה, ולכןpath="secrets"מחזירpath_deniedולאnot_found, וגםsecrets.mdו-credentials.mdחסומים; ותקרת 500KB של שירות המראה נשארת ההגנה על הפרסור, כי הכלי קורא את הקובץ כולו בליlines. שני שערים ולא אחד:MCP_DOCS_REPOמתיר בזמן ריצה, טבלת המדיניות בקוד יודעת לקרוא, וריפו שנמצא במשתנה הסביבה ואין לו מדיניות נדחה ב-repo_not_configuredבמקום ליפול לברירת מחדל מתירנית. ובקשה לפורמט שהריפו אינו מגיש נדחית בשמה —suffix_not_allowedעם רשימת הסיומות שכן מוגשות, במקום להשלים סיומת בשקט מעל הקיימת ולחזור כ“לא נמצא“ על קובץ שקיים. שני סירובים חדשים מגיעים מהפרסור עצמו:inconsistent_line_endingsעל\rבודד (CRLF עובד במלואו), ו-too_many_sectionsעל קובץ שחוצה את תקרת 50,000 הכותרות — Markdown דרך ברירת המחדל של הפרסר, ו-RST דרך התקרה שהכלי מעביר מאז #3429; סירוב שהפארסר מספק לו מספר שורה נושא גםline— תמיד ב-Markdown, ולא ב-too_many_sectionsשל RST, כיrst_parserמרים אותה בלי מספר.includesהוא שדה של RST ולכן בעמוד.mdהוא תמיד ריק. ומסלול ה-RST לא זז: אפס דיף מדוד על 5,920 רשומות מ-208 קובצי RST, לפני ואחרי. ראו איזה קובץ docs_get_section קורא, ובאיזה ריפו.fix: מאגר הקריאות של ה-MCP נגזר ממכסת הזיכרון של הקונטיינר, ולא ממספר המעבדים של המארח (#3391). כלי הקריאה רצים ב-
asyncio.to_thread, כלומר ב-executor ברירת המחדל של הלולאה, שגודלוmin(32, os.cpu_count() + 4)— ו-os.cpu_count()בקונטיינר הוא מספר הליבות של המארח, כפי ש-CPython מתעד במפורש. שורת הקיבולת מהייצור אמרה את זה במספרים:read pool 12 threads (os.cpu_count=8, usable=8)על מכסה של0.50 cpu. לקריאת מונגו זה לא מזיק; לפרסור בפייתון טהור אין מקביליות ב-CPU בגלל ה-GIL, ולכן 12 חוטים היו 12 עצי פרסור בזיכרון בו-זמנית — יותר ממה שנכנס ב-512MiB — ורגע לפני שהפרסור הופך לכלי ציבורי. מעכשיוattach_read_poolמתקין בעליית ה-lifespan של אפליקציית ה-ASGI מאגר משלו, בגודל(מגבלת הזיכרון − קו בסיס − שוליים) ÷ עלות פרסור אחד: המגבלה נקראת מ-cgroup בזמן ריצה (memory.maxב-v2, ``memory.limit_in_bytes`` ב-v1) ולא מקובעת בקוד, קו הבסיס והשוליים הם מדידות מהשירות בייצור עם תאריך, ועלות הפרסור נגזרת מ-MAX_FILE_SIZE_FOR_DISPLAYכי הכלי דוחהtoo_largeלפני שהוא מפרסר. על תוכנית הייצור זה 10 חוטים במקום 12 — עלות הפרסור נמדדה מחדש על הפרסר הנעוץ (scripts/measure_md_parse_cost.py; הקבוע נמדד עלservices.md_parserכהכנה לכלי ה-Markdown של PR 5, בעוד הכלי הציבורי היום מפרסר RST ב-services.rst_parser, שעולה כ-6 בתים לבית גם על העמוד הצפוף ביותר — והסקריפט מודד את שניהם), והיא תלויה בצפיפות הטוקנים של המסמך ולא בגודלו: המסמך הצפוף ביותר בקורפוס הוא הקבוע, וצורה עוינת של שורות-תבליט בודדות עולה פי ארבעה ממנו — מה ששום רוחב מאגר אינו סופג, ומחכה לתקרת טוקנים בפרסר עצמו. ובעקבות הסקירה:codekeeper_docs_get_sectionמעביר לפרסר את תקרת הסקשנים של האאוטליין (50,000) ומחזירtoo_many_sectionsבמקום לפרסר את הקובץ כולו — צורה עוינת של RST (כותרת בת תו אחד בכל שורה) עלתה 44.0MiB לפרסור, יותר מ-35.2MiB שהמאגר מקצה לחוט, ועכשיו נעצרת ב-20.1MiB; אף עמוד אמיתי אינו מתקרב לתקרה. רצפה של 2 (קריאה תקועה אחת לא משביתה את השאר), תקרה של 12, הרוחב שהייצור כבר הריץ (מעבר לתוכנית גדולה לא מרחיב את המאגר בלי החלטה; הגרסה הראשונה נקבה ב-32, והסקירה של #3429 הורידה אותה כי המחלק מתאר את הפרסור ולא את קריאות הטווח של האדמין; חסימת הקריאה במקור — #3433), וכשאין מגבלה קריאה — הרצפה, לאcpu_count + 4, עם WARNING שאומר שזה קרה. ושורת הקיבולת מפסיקה לחשב בעצמה: היא נרשמת מתוך ה-lifespan, קוראת את_max_workersשל המאגר שהותקן בפועל, ומדווחת גם את מגבלת הזיכרון לצד מכסת ה-CPU — כי רשומה שמתארת מצב ומתעדכנת בנפרד ממנו היא בדיוק מה שמשקר בשקט. ה-seam הוא עטיפתrouter.lifespan_context, אותו seam שניקוז ה-PostHog כבר משתמש בו, כיFastMCP.streamable_http_app()בונה את Starlette עםlifespan=משלה ו-on_startupאינו רץ. הטסט המרכזי נכנס ל-lifespan של האפליקציה האמיתית ומוכיח שקריאתto_threadנוחתת על חוט של המאגר החדש — לא שפונקציה נקראה; שורת הקיבולת נבדקת בתת-תהליך נקי דרך הפלט האמיתי ולא דרךcaplog. ראו ”מודל הריצה של הכלים“ ב-שרת ה-MCP — חיבור Claude ל-CodeKeeper.feat: ``codekeeper_docs_get_section`` מוצא סעיף גם לפי המזהה שלו, ולא רק לפי השם המלא. הסוכנים מפנים זה לזה לפי מזהה — ”קרא K11“, ”ראו U3“ — ועד היום
section="K11"החזירsection_not_foundעם רשימת הצעות ריקה, כי ההתאמה הייתה שוויון מלא ו-difflibעםcutoff=0.5אינו מוצא קרבה בין שלושה תווים לכותרת עברית ארוכה. כלומר הזרימה המרכזית של הכלי לא עבדה. מעכשיו, ורק אחרי שהשוויון המלא לא מצא כלום, שאילתה שבנויה כמזהה (עד שלוש אותיות ואחריהן עד שלוש ספרות, עם נקודה אופציונלית) מותאמת לכותרת שנפתחת באותו מזהה. והמזהה נקרא כיחידה ולא כקידומת של מחרוזת, ולכןK1מחזיר אתK1בלבד ולעולם לא אתK10עדK15— הצורה השבורה של הבדיקה הזאת היא אותה מחלקה בדיוק שבה גבול נתיב נבדק כרצף תווים. שאילתה שאינה בנויה כמזהה אינה עוברת במסלול הזה כלל, ולכןsection="איך"ממשיך להחזיר אפס התאמות גם בעמוד שיש בו חמש-עשרה כותרות שמתחילות במילה הזאת. מזהה שחוזר פעמיים הואambiguous_sectionעם מועמדים, כמו כל כותרת כפולה, ושאילתת מזהה שלא נמצאה מקבלת ב-suggestionsקודם כול כותרת קרובה, אם יש כזו בעמוד, ורק כשאין אף כותרת קרובה את המזהים שכן קיימים — עד 50, ועםsuggestions_truncatedכשהיו עוד. והתקרה גבוהה בכוונה, כי רשימת מזהים אינה מדורגת: לשאלה ”אלה שכן קיימים“ אין תשובה ”החמש הטובות“, וחיתוך שלה מסתיר פריטים בלי קריטריון ובלי דרך לבקש את השאר. והדגל הזה אומר את אותו דבר בשני המסלולים: גם רשימת מזהים שנחתכה וגם רשימת כותרות קרובות שנחתכה מדליקות אותו. שדה בשםtruncatedשמשמעותו ”נחתך“ במסלול אחד ו“שקט“ בשני הוא בדיוק ההפתעה שאנחנו מנסים למנוע. ונקודה עוד יותר קלה לפספס: הנקודה שפותחת תת-מספור אינה גבול. בכותרתK11. טקסטהנקודה מסיימת את המזהה, ובכותרתK11.1 טקסטהיא מפרידה בתוך מזהה ארוך יותר — ולכןK11לעולם אינו מחזיר אתK11.1, וכותרת בתת-מספור נגישה בשמה המלא בלבד. זו אותה מחלקה בדיוק כמוK1שתופס אתK10: גבול שנבדק כתו בודד, בלי לשאול מה בא אחריו. ההתנהגות על התיעוד כאן לא זזה, וזה נמדד ולא הונח: הפלט של הכלי על כל קובצי ה-RST — כל כותרת, בכל צורותיה — זהה בית-בית לפני ואחרי. וההנחה שהכול נשען עליה, שאף כותרת בעמודים כאן אינה נפתחת במזהה, אינה ספירה שנרשמה פעם אחת אלא טסט שרץ בכל CI:tests/test_docs_headings_carry_no_identifier.pyסורק את כל העמודים ונופל על כותרת שנפתחת במזהה, עם דוגמת autodoc אמיתית שמוכיחה שהוא מסוגל ליפול. ומה שלא נבדק בקורפוס הזה נבדק בבקרה סינתטית, כי סוללה שכל תשובותיה ”לא נמצא“ אינה מבדילה בין ענף כבוי לבין פרובים עיוורים. שני הדברים שמפתיעים קורא — קיצור המזהה, וכותרת שנושאת בקטיקים ולכן דורשת אותם בשאילתה — עברו לתיאור הפרמטרsection, שאינו נחתך אצל הלקוח כמו תיאור הכלי. ראו איך docs_get_section מוצא כותרת.feat: גרסה קודמת היא עמוד שאפשר לפתוח, לא רק לשחזר. בבוט אפשר היה לפתוח כל גרסה ולראות אותה; בווב-אפ גרסה ישנה הייתה משהו שאפשר רק לשחזר, או לראות בדיף מול הנוכחית. מעכשיו כל שורה בהיסטוריית הגרסאות מקשרת אל הגרסה עצמה, והיא נפתחת ב**אותו עמוד קובץ** ובאותה תצוגת Markdown — אותה הדגשת תחביר, אותו מסלול הברחה ואותה בדיקת בעלות, כי לא נבנה מסלול תצוגה שני. בראש העמוד באנר שאומר באיזו גרסה מדובר ומתי נשמרה, עם מעבר לגרסה הנוכחית, השוואה אליה ושחזור; פעולות שמשנות מצב מוסתרות שם, כי כולן פועלות על המסמך שבכתובת. ברשימת ההיסטוריה אפשר גם לסמן שתי גרסאות ולפתוח דיף בין שתיהן — דף ההשוואה תמיד ידע לקבל
?left=ו-?right=, ומה שהיה חסר הוא הדרך להגיע לשם. בדרך תוקנו שלושה דברים שהיו שקטים:?left=abcהציג דיף של גרסה אחרת בלי לומר דבר, כיtype=intשל Flask מחזיר את ברירת המחדל בשקט; שלושה מקומות חישבו בנפרד אילו שתי גרסאות מוצגות, כך שהרשימה הנפתחת והדיף יכלו לומר דברים שונים; ולשאילתה ”מהי הגרסה האחרונה של הקובץ“ לא היה אינדקס תומך, אף שהיא רצה גם בכל שמירה.compare.jsו-compare.cssהוגשו בלי מזהה גרסה מולmax-ageשל שנה, כלומר שינוי בהם לא היה מגיע למי שכבר ביקר בדף; זה תוקן, ויש טסט שמונע תגית חדשה כזו. ראו גרסאות: צפייה והשוואה.
2026-09-17
feat: ``description_age_versions`` — כמה גרסאות עברו מאז שהתיאור נקבע. תיאור של קובץ התיישן בשקט:
codekeeper_edit_fileו-codekeeper_append_fileמעבירים את התיאור הקיים לגרסה החדשה — ברירת מחדל נכונה — ולכן תיאור שנכתב בגרסה 1 נראה בגרסה 11 בדיוק כמו תיאור שנכתב אתמול, ולא הייתה שום דרך לדעת שכדאי לבדוק אותו בלי לקרוא את כל הקובץ. השדה מוחזר ב-codekeeper_list_files, ב-codekeeper_search_codeוב-codekeeper_get_file, והוא רמז לבדוק ולא קביעה שהתיאור שגוי: עשר עריכות קטנות אינן הופכות תיאור לשגוי, ועריכה אחת גדולה כן יכולה.nullאומר שהגיל לא ידוע ולעולם לא 0, ולקובץ בלי תיאור השדה נעדר לגמרי. שתי ההיטלות של הרשימה והחיפוש לא החזירוversionכלל, ולכן הורחבו — בלעדיהן הגיל היהnullבכל שורה, דווקא במסלול שהוא נועד לו.codekeeper_update_file_descriptionמסמן מעכשיו את התיאור כנבדק ומאפס את הגיל גם כששולחים את אותו טקסט, ומחזירunchanged: trueכדי שיהיה גלוי ששום דבר לא השתנה מלבד החותמת. לקבצים שנשמרו לפני השדה יש מיגרציה חד-פעמית,scripts/migrate_description_set_at_version.py, שמשחזרת את החותמת מהיסטוריית הגרסאות ומשאירהnullכששרשרת הגרסאות קטועה. ראו גיל התיאור — description_age_versions.fix: stderr של ``git grep`` מנוקז תוך כדי הקריאה, בשני המעברים (#3398).
stderr=PIPEשאיש לא קורא ממנו הוא צינור בן 64KB; ריפו עם אובייקטים חסרים מדפיס שתי שורותerror:לכל קובץ, וגיט נחסם ב-writeעד תום התקציב — כשל צינור שדווח כ-timeoutובלי לוג על הסיבה. האישו נסגר בלי שהקוד השתנה; זה המימוש, במחלקה אחת שמשרתת גם את איסוף התוצאות וגם את הספירה. בדרך נתפס גם מה ש-stderr מגלה: בלוב חסר גורם ל-git grepלדלג ולצאת ב-0, וקוד היציאה לבדו היה מכריז עלtotalמדויק שחסר בו קובץ — היום זהtotal_at_least. וכשל של המנוע אחרי שכבר אסף תוצאות חזר עד היום כתשובה תקינה עםtruncated: falseובלי אף שדה ספירה; היום מה שנאסף מוגש עםsearch_failed, אוsync_in_progressאם sync רץ.feat: ``case_sensitive`` ב-``codekeeper_search_repo``. הפרמטר היה קיים לכל אורך המנוע, אבל שכבת ה-MCP לא העבירה אותו — ולכן
git grepרץ תמיד עם-iולא הייתה דרך להבדיל ביןConfigל-config. הדגל הוא תוספתי (ברירת מחדלfalse, בדיוק ההתנהגות שהייתה), חל גם עםregex=true, ו**גם על הספירה**: מופע שהוא מוציא מהתוצאות אינו נספר ב-total. בדרך תוקן גם תיאור הכלי, שאמר ”matched literally — every character is itself“ בלי לומר שהרישיות נבלעת. ראו כמה מופעים יש באמת (total מול total_at_least).fix: ``total`` ב-``codekeeper_search_repo`` הפסיק לשקר. השדה היה שווה ל-
countתמיד —max_results=3החזירtotal: 3בין אם יש בריפו שלוש התאמות ובין אם אלפיים — כי אחרי שנאספו מספיק תוצאות התהליך נהרג ואיש לא ספר את השאר. זו הייתה מלכודת ולא רק חוסר: באותו שרת,query=שלcodekeeper_get_fileמחזיר בשדה בעל אותו שם את הסך האמיתי. היוםtotalהוא מספר המופעים בריפו, ו**קיים רק כשהוא מדויק**; כשהספירה נקטעת חוזריםtotal_at_leastו-truncation_reasonבמקומו, ולעולם לא שניהם יחד. ראו כמה מופעים יש באמת (total מול total_at_least).feat: קוד חיצוני וקבצים מקומפלים מוחרגים מהחיפוש בריפו, וניתן להחזיר אותם.
node_modulesבכל עומק, וגם*.bundle.js,*.min.js,*.min.cssו-*.map, אינם נסרקים ואינם נספרים כברירת מחדל — נמדד: באנדל אחד החזיק 64% מהמופעים שלfunctionבריפו. הם — הוא מילא חיפושים רחבים ודחק את הקוד שבאמת נכתב בפרויקט.include_vendored=trueמחזיר אותו. ההחרגה יושבת במנוע, ולכן היא חלה גם על החיפוש בדפדפן הריפו בוובאפ, וקבצים כאלה ממשיכים להיקרא כרגיל ב-codekeeper_get_repo_file. אפס תוצאות אינו אומר שהמחרוזת לא קיימת בריפו, וזה כתוב גם בתיאור הכלי.fix: חיפוש רחב בריפו הפסיק להיכשל על בייט שאינו UTF-8. הפלט של
git grepפוענח ב-strict, ולכן קובץ טקסט אחד עם בייט חריג (docs/images/onboarding-flow.svgכאן) הפיל את כל החיפוש ל-search_error— תשובה ריקה על חיפוש תקין. עד היום זה הוסתר בכך שהזרם נהרג מוקדם, ומעבר הספירה החדש היה חושף את זה בכל חיפוש רחב.fix: קובץ חסום במדיניות הסודות אינו נסרק ואינו נספר. הסינון קרה על התוצאות אחרי החיתוך, ולכן קובץ חסום בין ההתאמות הראשונות הקטין את התשובה בלי לומר דבר. ההחרגה נכנסת עכשיו ל-
git grepעצמו, ונגזרת מאותה רשימת תבניות שחוסמת קריאה — לא מרשימה שנייה; הסינון על התוצאות נשאר כשכבה אחרונה.
2026-09-16
feat: ``codekeeper_update_file_description`` — סוכן יכול לרענן תיאור של קובץ קיים. עד היום התיאור נקבע רק ב-
codekeeper_save_file, שמסרב לקובץ שכבר קיים, ול-codekeeper_edit_fileול-codekeeper_append_fileאין פרמטרdescription— הם מעתיקים את הקיים לגרסה החדשה. כלומר תיאור שהתיישן ביחס לתוכן היה ניתן לתיקון רק מהדפדפן. הכלי החדש מקבלfile_nameו-descriptionבלבד, ומחזיר את התיאור הקודם, את החדש ואת מספר הגרסה שלא השתנה. הוא אינו יוצר גרסה, ולכן התיאור הקודם אינו נשמר בהיסטוריה ואינו ניתן לשחזור — התשובה מחזירה אותו כי זה המקום היחיד שבו הוא עוד קיים; ורק הגרסה האחרונה מתעדכנת, כך שקריאה של גרסה קודמת מחזירה את התיאור שהיה בה. שלוש ההצהרות האלה כתובות בתיאור הכלי עצמו, כי דוקסטרינג שקרי גרוע מחסר. תיאור ארוך מהמותר נדחה ואינו נחתך (description_too_longעם ה-max): הראוט בוובאפ חותך כי אדם רואה את התיבה, וסוכן אינו רואה את התוצאה. ראו עדכון תיאור בלי גרסה חדשה.refactor: ”עדכון תיאור מהיר“ בוובאפ והכלי החדש חולקים מסלול כתיבה אחד.
POST /api/file/<id>/quick-updateהחזיקupdate_oneמשלו ישירות מולcode_snippets, ולא עבר דרך שכבת ה-repository. כלי שני לאותה כתיבה היה הופך את זה לשני עותקים שרק אחד מהם יקבל את התיקון הבא, ולכן הלוגיקה חולצה ל-database.repository.update_file_metadata_inושני הערוצים קוראים לה. ההתנהגות כלפי הדפדפן לא זזה — אותו404, אותה חתימתupdated_at(רק כשהתיאור נכלל, לא על תגיות), ואותם שדות בתשובה. בדרך נסגר גם חלון TOCTOU: הבעלות נבדקה ב-find_oneנפרד לפני ה-update_one, והיא עברה לתוך הפילטר שלfind_one_and_updateעצמו.fix: עדכון תיאור השאיר את החיפוש מגיש את התיאור הישן עד חמש דקות. הראוט של העדכון המהיר קורא רק ל-
cache.invalidate_file_related, ורשימת הדפוסים שלה כיסתה אתuser_files,latest_versionו-web:files— אבל לא אתsearch_code, שה-cache שלו הוא 300 שניות. ``invalidate_user_cache`` תפסה אותו ממילא דרך fallback רחב שיש שם ואין כאן, ולכן הפער היה גלוי רק בהשוואה בין שתי הרשימות. נוספוsearch_code,regular_filesו-files_by_repo— שלוש הרשימות שמחזירותdescriptionשל הגרסה האחרונה, כלומר אותה מחלקה ולא מופע בודד.
2026-09-15
fix: כלי ה-MCP רצו כולם על לולאת האירועים, ובקשה אחת איטית השתיקה את כל הסשנים. ה-docstring של
mcp_server/server.pyהצהיר שנים ש“FastMCP מריץ כלים סינכרוניים ב-worker thread“, וזה פשוט לא נכון — האמונה השגויה הזאת היא הסיבה שאיש לא בדק. נמדד מול ה-SDK המותקן (mcp 1.28.1,func_metadata.call_fn_with_arg_validation): פונקציית כלי אסינכרונית נקראתawait fn(...), וסינכרונית נקראתreturn fn(**arguments_parsed_dict)— ישר, בליto_threadבשום מקום במסלול. כלומר גוף כלי שרץ שנייה אחת עצר את כל התהליך ולא רק את הקורא שלו, וזו מחלקת השבתה משותפת ולא בקשה איטית. התיקון יושב במקום אחד:AdminAwareFastMCP.add_toolעוטף כל גוף סינכרוני בזמן הרישום, ושני מסלולי הרישום (@mcp.toolו-add_toolישיר) עוברים שם — אומת ב-SDK. ולא בגוף של כל כלי בנפרד, כי כל אתר קריאה הוא הזדמנות לשכוח, והכלי הבא היה נולד שוב על הלולאה; במקום זה יש טסט שדורש שכל כלי רשום יהיהcoroutine function, כך שרגרסיה נופלת בקול. ושני יעדים ולא אחד: קריאה הולכת ל-asyncio.to_threadולמאגר המשותף, וכתיבה למאגר ייעודי בעל worker יחיד. כך גוף כתיבה אחד רץ בכל רגע — הסדר שהיה לכלים כשהכול ישב על הלולאה — והתור מוסר אותם לפי סדר ההגעה. גרסה ביניים ניסתה מנעול ברמת התהליך במקום מאגר, והמדידה פסלה אותה: כותב שחסום על מנעול עדיין תופס חוט מהמאגר המשותף, ולכן ב-40 כתיבות מקבילות קורא חיכה 16.51 שניות (20.02 לפני התיקון כולו) — ועם מאגר ייעודי הוא חוזר ב-0.00. גם הסדר: 16 כותבים על פני 30 סבבים יצאו מחוץ לסדר ב-25 סבבים תחת המנעול, ובאפס עם המאגר. שלושה דברים אומתו שהם שורדים את הקפיצה ולא הונחו: הסכימה המפורסמת (functools.wrapsשומר על__wrapped__, כולל הזרקתContext),ctx.request_context(מאפיין של האובייקט, לא contextvar), ו-get_access_token()— שכן contextvar.asyncio.to_threadמעתיק את ההקשר בעצמו,loop.run_in_executorאינו מעתיק, ולכן מסלול הכתיבה מעתיק אותו במפורש; בלי זהrequire_admin/require_writeמפסיקים לראות מי שואל. ומה שהשתנה בסמנטיקה, במפורש: קריאות יכולות להשתלב עכשיו זו בזו, וזו התוצאה המכוונת. כתיבות אינן, אבל כתיבות של משתמשים שונים ממתינות בתור אחד — המחיר, והוא כתוב בקוד ובתיעוד. כתיבה שבוטלה בעודה בתור אינה רצה כלל; כתיבה שבוטלה אחרי שהתחילה רצה עד הסוף. פער ה-read-then-write בבחירת מספר הגרסה (U1וריאציה b) אינו נעשה שגוי בגלל השינוי, אבל הוא מפסיק להיות מוסתר מ-MCP, והוא ראוי לתיקון משלו. ומיפוי בטיחות החוטים נעשה לפני המיזוג, כפי שהאישו דורש:pymongoבטוח לחוטים ואיןmotorבכלmcp_server, המצב ברמת המודול הוא קבועים לקריאה בלבד, איןthreading.local, והשירות רץ תחת uvicorn בלי monkey-patching של gevent. סוגר את #3379 — חסם השורות בפרסור נשאר פתוח, ומתועד בסורקים.fix: תיאור
codekeeper_get_repo_fileהיה ארוך מכדי שהלקוח יגיש אותו, והסוף — שפות האאוטליין ו-symbol=— נחתך. התיאור הגיע ל-2,482 תווים, פי 3.8 מהכלי השני בגודלו (652) ופי שמונה מהחציון (303), ונמדד שהוא מגיע לסוכן חתוך באמצע המשפט על RST ועלsymbol=. כלומר שני פיצ’רים עבדו ואף לקוח לא קרא עליהם — בדיוק מחלקת הכשל שהמשפטים האלה נכתבו כדי למנוע. הפתרון הוא העברה ולא מחיקה: כל משפט שם נולד מבאג אמיתי ויש עליו טסט, ולכן הפירוט על השפות עבר לתיאור הפרמטרoutlineוהפירוט על הסינון לתיאור הפרמטרsymbol, ששניהם יושבים ב-inputSchemaואינם מתחרים על תקציב התיאור. סך שלושת השדות גדל (2,711 מול 2,482) — אף לקח לא נמחק — ותיאור הכלי ירד ל-1,125. ונוסף טסט תקרה על כל 29 הכלים, כי זו מחלקת בעיה ולא מופע; הוא רץ על_tool_manager.list_tools()ולא עלmcp.list_tools(), שמסנן את כלי האדמין ומחזיר 22 — ושבעת החסרים כוללים אתcodekeeper_get_repo_fileעצמו, כלומר טסט על התצוגה המסוננת היה ירוק בלי לכסות את המקרה היחיד שהפיל אותנו. ומה שנמדד, כדי שלא ייקרא כאן יותר ממה שכתוב: שהשרת שולח את תיאורי הפרמטרים במלואם ב-mcp==1.28.1(עם ריצת בקרה שבליFieldהשדה חוזרNone), ושהזרקתcontextשל PostHog אינה דורסת אותם — לא מה שלקוח מציג מהם.fix: טסט התקרה תפס כלי שני יומיים אחרי שנכתב —
codekeeper_get_fileעבר את הסף. הפרמטרqueryנוסף עם תיאור בן 1,376 תווים שצורף לתיאור הכלי, והביא אותו מ-349 ל-1,726 — כלומר סופו נחתך אצל הלקוח, בדיוק כמו ב-codekeeper_get_repo_file. זה מה שהופך את התקרה ממופע בודד ל**מחלקת בעיה**, ולכן התרופה זהה:_QUERY_DOCעבר ל-Field(description=...)של הפרמטרquery, ותיאור הכלי ירד ל-464 תווים עם משפט שמפנה לפרמטר._RANGE_DOCלא נגע — הוא משותף לשני הכלים ו-קריאת טווח שורות מחייב שיתארו אתlines=באותן מילים. ומה שההשוואה משפט-משפט מול הנוסח הישן העלתה: הפסוקית ”symbol= filters on that full name“ נשמטה בסבב הקודם, והיא נושאת מידע — הסינון הוא על השם המנוקד השלם ולא על החלק האחרון שלו, ולכןsymbol="method"מוצא גםClass.method. היא הוחזרה. ומבחן מוטציה מצא assertion שלא הייתה מסוגלת ליפול:"query" in descriptionעברה גם אחרי מחיקת ההפניה, כי התיאור נושא ממילא את הדוגמהquery="..."— היא הוחלפה בבדיקה על ההפניה לשם הפרמטר.fix:
page/per_pageהיה הפיצ’ר היחיד בתיאור בלי אף טסט, ונשמט בטיוטה הראשונה של הפיצול בלי שאף בדיקה שמה לב. הוא הוחזר לתיאור הפרמטרoutlineעם טסט שישמור עליו. עמוד ראשון שנראה כמו כל המפה הוא כשל שקט:OUTLINE_PER_PAGE_DEFAULTהוא 100, ולכן 486 הסימבולים שנמדדו על הקובץ הצפוף בקורפוס מתפרסים על חמישה עמודים. באותה הזדמנות נוספה בדיקה על ה-escape עצמו ב-services.backup\_service, שעד כה נבדק רק דרךsymbol="backup_service"— טענה שעוברת גם על טקסט שאיבד את הלוכסן, כלומר בדיוק מה שקורה בהעתקה דרך שכבת escaping.fix: שלוש שתיקות בקריאת קובץ — שאילתה שלא יכלה להתאים, פרמטר שנזרק, וקריאה שנספרה בעמודה הלא נכונה. שלושתן חלקו צורה אחת: התשובה נראתה תקינה, לא הייתה שום שגיאה, ומי שקרא לא היה יכול לדעת שמה שביקש לא קרה. (א) שאילתה רב-שורתית. ההתאמה ב-
queryנעשית שורה אחר שורה, ולכן מחרוזת שיש בה\nאינה יכולה להתאים לעולם — ואפס מופעים מוצהר כ**הצלחה**. נמדד:query in textהואTrueבעוד התשובה מדווחתtotal: 0עםok. זה לא קצה, אלא בדיוק מה שסוכן מייצר כשהוא מדביק קטע מקובץ שקרא לפני רגע. נדחית עכשיו כ-query_multiline; דחייה ולא תמיכה, כי כל רשומה עוגנת ל-lineאחד ולהתאמה שחוצה שורות איןlineמוגדר. (ב) ``context_lines`` ו-``max_results`` בלי ``query``. נמדד שקריאה עםcontext_lines=5בליqueryמצליחה ומחזירה את הקובץ המלא, ומפתחות התשובה הם בדיוק['file', 'found']— כלומר הפרמטר נזרק בלי סימן. זו אותה שתיקה שהשינוי הזה עצמו אוסר עלquery``+``lines. והשורש היה בברירות המחדל:0ו-50לא אפשרו להבדיל בין ”לא נשלח“ לבין ”נשלח בדיוק הערך הזה“, ולכן אי אפשר היה לדחות. שתיהןnullעכשיו, ההצמדה מותנית, והסירוב יושב בראש הקריאה ב-backend לפני הגישה למסד —context_lines_without_queryאוmax_results_without_query, שני קודים כדי שהקורא ידע איזה. הסכימה שהלקוח רואה משתנה בהתאם, וזה הפרש מכוון מ-codekeeper_search_repo. (ג) עמודת ``ck_read_mode``. הקולבק של האנליטיקס שאל ”מה נשלח“ במקום ”מה הכלי הזה בכלל מקבל“, והוא מקבל את מילון הארגומנטים הגולמי — לפני ש-pydantic מסלק ממנו מפתחות שאינם בסכימה. נמדד בארבעה צירופים:codekeeper_get_repo_fileעםqueryתועה נספר כ-query, ו-codekeeper_get_fileעםoutlineתועה נספר כ-outline— והחצי של ``outline`` קדם לפיצ’ר ``query`` לגמרי. לא רק שהעמודה השגויה מתנפחת: שתי האחרות יוצאות חסרות באותה מידה, וזו בדיוק העמודה שנבנתה כדי למדוד עלות ניווט. השיוך מפורש עכשיו לפי כלי,FILE_READ_TOOLSנגזר ממנו במקום להיכתב פעמיים, וטסט שואל את הכלים הרשומים עצמם אילו פרמטרי קריאה הם מצהירים עליהם ומשווה — כלי שיקבל מחרqueryבלי שהמפה תעודכן מפיל את הבדיקה במקום להיספר בשקט במקום הלא נכון.המספרים בתיאור הכלי נשתלים מהקבועים, ולא נכתבים כטקסט. התיאור הבטיח ”50 כברירת מחדל, עד 100“, ושום בדיקה לא קשרה את המספרים ל-
QUERY_RESULTS_DEFAULTול-QUERY_RESULTS_MAX. העלאת תקרה הייתה משאירה כל סוכן מגביל את עצמו למספר שכבר אינו התקרה, עם סוויטה ירוקה. בניית התיאור עברה לפונקציה_build_query_docדווקא כדי שהטענה תהיה בת-הפרכה: הבדיקה מריצה את הבנייה עם קבועים אחרים ורואה את הטקסט זז, במקום להשוות לערכים הנוכחיים — השוואה כזו עוברת גם על טקסט קשיח שבמקרה נכון היום.ושתי בדיקות שלא היו מסוגלות להיכשל, ושלושה מסלולים בלי כיסוי. שתי הצהרות בקובץ הבדיקות תפסו
Exceptionגנרי, כך שגם שגיאת ייבוא או נפילת דמה היו נקראות כהצלחה; הן צרות עכשיו ל-ToolErrorעם התוכןvalidation error, שנמדד מולmcp 1.28.1. ההצהרה על שם כלי שאינו מחרוזת בודקת עכשיו את הלוג ולא את ערך ההחזרה, כי ה-exceptשעוטף את הקולבק מחזירNoneבכל מקרה — בלי ההגנה על הטיפוס כל אירוע כזה כותב אזהרה עם traceback, ועם ההגנה אין רעש כלל. ובנוסף נוספו הבדיקות לשלושה מסלולים שהתנהגו נכון ולא נבדקו: קובץ חסר,file_id, ו-version—queryרץ אחרי בחירת המסמך, ולכן הסריקה עלversion=1היא על תוכן הגרסה הישנה ולא על האחרונה.ומה שלא שונה, במפורש. הסף של תו אחד נשאר — הוא הפרש מכוון מ-
codekeeper_search_repo, כי קובץ בודד אינו ריפו שלם — ומה שחסר היה תיעוד ולא יישור.max_resultsנשאר פרמטר שהקורא קובע, מיושר ל-codekeeper_search_repo.
2026-09-14
``query`` ב-``codekeeper_get_file`` — חיפוש בתוך קובץ פרטי. סוכן שרצה שדה אחד מתוך JSON גדול, או כלל אחד מתוך מסמך, משך עד היום את הקובץ כולו — או ניחש טווח.
codekeeper_search_repoכבר עושה בדיוק את זה לקבצי ריפו, והקבצים השמורים פשוט לא קיבלו את הכלי.query="..."מחזיר עכשיו את המופעים במקום את התוכן, בדיוק בצורת התשובה שלcodekeeper_search_repo— אותםcount,total,truncatedו-results, ולכל פגיעהlineו-snippet, ואותוcontext_lines. ההמשך הואlines=[line, line], ומה שמבטיח שזה עובד הוא שהסריקה מפצלת שורות באותה צורה כמו קריאת הטווח. שלוש החלטות:queryיחד עםlinesנדחה כ-query_and_linesולא מתעלם בשקט מאחד מהם; אפס מופעים הוא הצלחה עם רשימה ריקה ולאerror; ותקרת המופעים ותקציב הבתים מוצהרים ב-truncatedבמקום לחתוך בשקט. התקרה על טקסט הרשומה נמדדת בבתים ולא בתווים, והחיתוך נוחת על גבול תו. הפרמטר תוספתי לחלוטין — קריאה בליqueryמחזירה בדיוק את מה שהחזירה קודם. עוגן חדש: חיפוש בתוך קובץ שמור (query).fix: אאוטליין של ``.rst`` פרסר את הקובץ כולו לפני שהתקרה נגעה בו. התקרה על מספר הסימבולים יושבת בסורקים בכוונה — סינון של פלט אינו חסימה של עבודה — אבל בסורק ה-RST היא נגעה רק ברשימה שנבנתה אחרי ש-
parse_documentסיים. כלומר קובץ שנכנס בנוחות לתקרת ה-10MB של קריאת הטווח יכול לקנות עבודה גדולה: נמדד על 10MB של כותרת בת תו אחד בכל שתי שורות — 2.6 מיליון סקשנים — 18.5 שניות ו-1,132MB, מול 0.30 שניות ו-83MB אחרי התיקון. הזיכרון הוא מה שמצדיק את זה: בקשה איטית היא בקשה איטית, אבל ג’יגה-בייט בתהליך משותף מפיל שירות.parse_documentמקבל עכשיוmax_sectionsשברירת המחדל שלו ללא תקרה, ולכןdocs_get_section— הצרכן השני של הפארסר — זהה בית-בית, והתשובה על קובץ שחוצה את התקרה נשארה בדיוק אותה תשובה:no_outlineעםtoo_many_symbols, בלי רשימה חלקית. ומה שלא נחסם, במפורש: התקרה מגבילה סקשנים ולא שורות, ולכן 10MB של שורות ריקות הוא אפס סקשנים ו-6.3 שניות — זהה עם התקרה ובלעדיה.docs: הקבועים והתקרות של שרת ה-MCP במקום אחד, ואוצר המילים של התשובה. נוסף סעיף הקבועים, התקרות ואוצר המילים של התשובה עם כל תקרה שנאכפת בקוד — כולל תקרות העימוד, שלא היו מתועדות כלל — ועם הכלל שערך מעל התקרה נצמד בשקט ואינו נדחה, כלומר
per_page=1000באאוטליין מקבל 500 ואין שדה שמצהיר על זה. ובנוסף: הרשימה הסגורה של ערכיstatusבתשובת כלי (תשובת HTTP של השרת אינה תשובת כלי —401נושאerrorבליok), ההסבר למה ל-errorאין רשימה כזאת, המחלקה היחידה שסורק ה-RST אינו ממודל (בלוק literal מצוטט), ושורה בפתרון תקלות על כך שתהליך ה-MCP אינו מגדיר לוגים — ולכן היעדר לוג אינו סימן שהכלי לא נקרא. ובנוסף הוסבר מה ``file.encoding`` באמת אומר:utf-8ו-utf-8-sigהם זיהוי, ו-latin-1הוא נפילה-לאחור שאינה נכשלת לעולם — נמדד שקובץ עברי ב-cp1255חוזר עם התוויתlatin-1ועם תוכן משובש. וש-BOM מוסר מ-contentבזמן ש-sizeסופר אותו, כלומר זה החריג היחיד לכלל שהטקסט חוזר בית-בית.fix: שעון חוזר של קורא חיצוני הפך לחד-פעמי בשקט אחרי הערכת regex.
ConditionOperators.regexדורס אתSIGALRMלשנייה אחת כדי לעצור ביטוי שמתפוצץ, ומשחזר אחר כך את השעון של מי שקרא לו.signal.getitimerמחזיר זוג — מה שנשאר עד הירייה הבאה, ומרווח החזרה — והקוד שמר את האיבר הראשון בלבד והשחזור העביר ארגומנט אחד. כלומר שעון חוזר שוחזר כחד-פעמי: הירייה הבאה כן מגיעה, וכל אלה שאחריה אינן, בלי שום שגיאה. נמדד: על שעון של 30 שניות עם חזרה כל 5, הקוד שלפני התיקון החזיר מרווח0.0אחרי הקריאה והמתוקן מחזיר5.0. בריפו הזה אין מסלול הגעה ידוע —pytest-timeoutמצמיד שעון חד-פעמי — ולכן זה פגם נכונות בקוד ייצור ולא תקלה שנצפתה. שלושת הטסטים שכיסו את השחזור הדליקו שעון חד-פעמי בלבד, ולכן אף אחד מהם לא היה יכול לראות את זה, והעזר שבטסטים נשא בעצמו את אותו פגם — גם הוא שמר איבר אחד.fix: שתי בדיקות שלא היו מסוגלות לתפוס את מה שהן שומרות עליו. העד של סורק ה-escape-ים שחזר את הסריקה בתוך עצמו במקום לקרוא למימוש, ולכן מי שישבור את המימוש היה מקבל אפס ממצאים ושתי הבדיקות היו ממשיכות לעבור; הוא עובר עכשיו דרך
_invalid_escapesעל קובץ שנשתל ב-tmp_path. ושורת הסיכום שאומרת ”כל בדיקות הדפדפן דולגו והכיסוי אפס“ לא נכתבה דווקא בתצורה שה-CI מריץ. הסיבה נרשמה במשתנה ברמת המודול שהפיקסצ’ר ממלא, וה-CI מריץpytest -n auto --dist=loadscope— כלומר הפיקסצ’ר רץ ב-worker ו-pytest_terminal_summaryרץ ב-controller, ושם המשתנה ריק לעולם. נמדד על קובץ דפדפן אחד בלי דפדפן: בלי-nהשורה נכתבת, ועם-n 2 --dist=loadscopeהריצה מסתיימת ב-4 skippedבלי שום אזכור של הסוויטה. הפתרון הוא מקור אחד במקום שניים: הסיבה נקראת מדיווחי הדילוג של pytest, שכן עוברים ל-controller בשתי התצורות —when == "collect"כשהחבילה חסרה, ו-when == "setup"כשרק הדפדפן חסר, וזו הצורה שה-CI מגיע אליה כיrequirements/base.txtמצמיד אתplaywrightואין באף workflow שלב שמתקין דפדפן. אומת בהרצה עלpytest 8.4.2עםpytest-xdist 3.8.0— הגרסאות שה-CI מתקין, ולא אלה שמותקנות בקונטיינר הפיתוח. ושתי הבדיקות ההתנהגותיות מריצות עכשיו את תת-התהליך בשתי התצורות, כי שתיהן הריצו אותו בלי-nולכן אף אחת מהן לא הייתה מסוגלת לראות את זה.
2026-09-13
fix: כותרת שנגמרת ב-``::`` נעלמה מהמפה, ושורה בתוך פסקה הפכה לסעיף מומצא. סבב סקירה נוסף מצא שני באגים באותו ענף, ושניהם נבעו מאותה סיבה: הענף שזיהה שורה שנגמרת ב-
::ודילג על הבלוק המוזח שאחריה רץ לפני בדיקת הכותרות, וקידם שורה אחת ולא את בלוק הפסקה. הראשון: במצבTextשב-docutils הסדר הוא blank ← indent ← underline ← text, כלומר כשהשורה שאחרי הטקסט היא קו פיסוק זה סעיף וה-::אינו מתפקד כסמן — ולכןConfiguration::עם קו מתחתיה היא סעיף ש-Sphinx בונה, והמפה החסירה אותו לגמרי;codekeeper_docs_get_sectionענה עליו ”סעיף לא נמצא“. והשני: ב-docutils ה-literal block נכנס רק אחרי שהפסקה נצרכה, ולכן שורות שהיו בתוך אותה פסקה נבחנו כאן שוב ככותרות והמפה המציאה סעיף — ובצורה אחת זו הייתה רגרסיה, כלומר מפה ריקה ב-main והמצאה אחריה. הפתרון הוא הסרת הענף כולו: בלוק literal מוזח, ולכן שומר ההזחה מדלג עליו ממילא, וכך גם תוכן של דירקטיבה. נמדד על אורקל דיפרנציאלי מול docutils עם::באלפבית, 6,000 קלטים: 245 אי-הסכמות עם הענף, ארבע בלעדיו, ו-706 בקוד שלפני ה-PR. ארבע הנותרות הן מחלקה מוצהרת אחת — quoted literal block, בלוק שאינו מוזח ששורותיו נפתחות בתו פיסוק — שאין לה אף מופע בקורפוס, והאורקל הבלתי-תלוי סוטה על אותן ארבע בדיוק. הפלט על כל קובצי ה-RST כאן לא זז: אפס מתוך 208, והסכמה מלאה עם docutils. ובנוסף שדה הלוג בנתב שונה מ-bytesל-chars, כיlenעל מחרוזת מודד תווים — נמדד שעברית היא 1.83 בתים לתו, ולכן השדה היה מדווח עמוד עברי כחצי מגודלו בדיוק ברשומה שנכתבה כדי לזהות קלט פתולוגי.fix: כותרת שכתובה מיד אחרי שורה מוזחת נעלמה מהמפה גם במסלול השני, וזו הייתה רגרסיה. הכלל של
Text.text— שהבלוק נגמר בשורה ריקה או בשורה מוזחת — היה כתוב בפארסר בשני מקומות, כי docutils מגיע לאותו מסלול משני כיוונים: מעבר ה-textמ-Body, והחזרה אליו מ-short_overlineדרךstate_correction. התיקון הקודם נתן את העצירה על ההזחה לאחד מהם, והשני נשאר כפי שהיה — ולכן קו פיסוק קצר, ואחריו שורה מוזחת, ואחריה כותרת, החזיר מפה בלי הכותרת ועםstatus: "ok", כלומרcodekeeper_docs_get_sectionענה ”סעיף לא נמצא“ על סעיף שכתוב בקובץ ו-Sphinx בונה אותו. וזו הייתה רגרסיה ולא פער חדש: הפארסר שב-main מחזיר את הכותרת בהסכמה עם docutils, והשינוי שקדם לזה לא. הכלל יושב עכשיו בהגדרה אחת,_skip_paragraph_block, ששני האתרים קוראים לה — כי שני נוסחים של אותו כלל הם בדיוק מה שנסחף כאן. נמדד על מדגם של 37,376 צורות: 280 מהן חלקו על docutils, וכולן חזרו להסכמה, ואפס צורות שהיו נכונות נשברו. ולולאת ה-doctest נשארה בכוונה מחוץ לאיחוד, כיBody.doctestקוראget_text_block()בליflush_leftושם רק שורה ריקה מסיימת — אומת במקור של docutils 0.23, ויש על זה טסט בקרה שיתפוס ”איחוד“ שגוי. ובנוסף יושר האורקל הבלתי-תלוי שבטסטים, שנשאר מאחור בשני כללים והפסיק להיות עוגן בדיוק להם: בכלל הרוחב הוא המציא כותרת ש-docutils דוחה, ובבלוק הפסקה הוא איבד כותרת ש-docutils מחזיר. שני הכללים נגזרו מחדש מהמקור של docutils ולא מהמימוש, והפלט על כל קובצי ה-RST כאן לא זז — אפס מתוך 208.fix: כותרת שבאה אחרי טבלה פשוטה עם מפריד כותרת נעלמה מהמפה, ויעד חיצוני נכנס אליה. סבב סקירה נוסף על הענף העלה שני ממצאים, ושניהם אומתו במדידה מול הפארסר של docutils. הראשון: היקף הטבלה נקרא עד הגבול הראשון, וטבלה עם מפריד כותרת יש בה שני גבולות — כלומר הסריקה נעצרה על המפריד, גוף הטבלה נקרא כפסקה, והפסקה בלעה את הכותרת שבאה מיד אחרי הגבול הסוגר. ההיקף נגזר עכשיו משלושת תנאי הסגירה של המקור: הגבול שהוא השני שנמצא, או האחרון בקלט, או אחד שאחריו שורה ריקה — ועד בכלל; שורה ריקה בתוך גוף הטבלה אינה מסיימת אותה; וטבלה בלי גבול סוגר בולעת את שאר הקלט, כפי שהמקור מתנהג. נמדד על מדגם צורות טבלה: שבע חולקות לפני, ואפס אחרי — ואחת מהן הייתה בכיוון ההפוך, כלומר כותרת שהמפה המציאה ו-Sphinx אינו בונה. והשני:
.. _label:שאחריו שורה מוזחת הוא יעד חיצוני ואינו יעד של:ref:— במקור, כל תוכן בבלוק של היעד הופך ל-URI, בלי שום אימות שזה נראה כמו URI, ולכן גם פרוזה מוזחת. יעדים כאלה אינם במפה יותר, והצורה שבה ה-URI באותה שורה הייתה חסומה ממילא. התנהגות התיעוד בריפו הזה לא זזה: הפלט על כל קובצי ה-RST כאן זהה בית-בית לפני ואחרי, ומספר התוויות לא השתנה.fix: קובץ
.rstשנוצר ב-Windows נעל את שרת ה-MCP עד restart. סקירת קוד על השינוי שלמעלה מצאה שהלולאה שבולעת פסקה אינה מובטחת להתקדם:_is_title_textבדק ”שורה ריקה“ לפי המחרוזת הריקה בלבד, ולכן שורה שכולה\r— כלומר שורה ריקה בקובץ עם סיומות שורה של Windows — אושרה כטקסט כותרת, ואז הלולאה, שבודקתstrip, רצה אפס איטרציות וחזרה לאותה שורה. בלי מוצא: אין תקרת זמן במסלול,get_repo_fileסינכרונית וה-SDK קורא לה על לולאת האירועים, ולכן כל השרת אילם — ולא רק הבקשה הזאת. הכשל הגיע גם ל-codekeeper_docs_get_section, שרץ על אותו פארסר. הפתרון בגבול הקלט:\r\n←\nמנורמל פעם אחת ב-parse_document, וזה הנרמול היחיד שמותר שם כי הוא היחיד שאינו משנה כמה שורות יש — כל שדות ה-linesבתשובות ה-MCP נספרים באותה חלוקה, ומפה שנספרת אחרת מהחיתוך שיבוא אחריה מצביעה לשורה שאי אפשר להגיע אליה.splitlines()נשקל ונדחה: הוא מפצל על עשרה תווים במקום אחד, מוריד שורה ריקה סופית, ושינה את הפלט כאן על כמעט כל עמוד. ותו whitespace שאין לו נרמול — tab אנכי, form feed, רווח קשיח — נחסם בהאחדת ההגדרה של ”שורה ריקה“, שהיא השורש. ובנוסף תוקן שיעדי.. _label:נעלמו בשקט מקובץ כזה בזמן שהכותרות כן חזרו, כי זיהוי הכותרת מקצץ רווחים בעצמו והתווית דרשה סוף שורה; הנרמול סוגר את שני הסימפטומים במקום אחד.fix: כותרת שכתובה מיד אחרי שורה מוזחת נעלמה מהמפה, והסעיף שמעליה בלע אותה.
Text.textב-docutils קורא את הפסקה ב-get_text_block(flush_left=True), שעוצר על הזחה, והלולאה כאן המשיכה עד השורה הריקה — כלומרlines=שבא אחרי המפה החזיר את הקטע הלא נכון בלי שום סימן שמשהו אבד. זו הייתה רגרסיה: הגרסה שלפני השינוי ו-docutils שהורץ שניהם מחזירים את הכותרת.fix: כלל אורך הקו של כותרת עם overline לא ספר את ההזחה שלה. במקור, הרוחב נמדד על הכותרת אחרי קיצוץ מהסוף בלבד, וקיצוץ ההזחה קורה אחר כך ורק לטקסט שנשמר — כלומר ההזחה כן נספרת. בלי זה המפה הציגה סעיף ש-Sphinx אינו בונה. נמדד על מדגם צורות בממד ההזחה, והמימוש מסכים עם docutils בכולן. והתיאור של הכלי תוקן בשני מקומות:
symbol="_"מסנן בהכלה ולכן מחזיר גם כותרות שיש בהן קו תחתון, וכותרת autodoc נושאת את ה-escape של RST — ולכן שאילתה בלי הלוכסן אינה מוצאת את העמוד ששמו כך.feat: ``outline`` עובד גם על קובצי
.rst.codekeeper_get_repo_fileעםoutline=trueמחזיר עכשיו תוכן עניינים עם טווחי שורות לקובצי תיעוד, ולא רק לפייתון, לתבניות ול-CSS. השמות מנוקדים לפי ההיררכיה כמו בפייתון, וההיררכיה עצמה נקבעת לפי סדר הופעת תווי הפיסוק בכל קובץ בנפרד —===אינו ”רמה 1“, ולכן קובץ שמשתמש ב-^לרמה שלישית ואחר ב-~יוצאים שניהם נכון. ובנוסף, יעדי.. _label:הם סימבולים נפרדים — אלה היעדים שאליהם:ref:מצביע, וכשקישור פנימי נשבר זה בדיוק מה שמחפשים;symbol="_"מגיע אל כולם — וגם אל כל כותרת שיש בה קו תחתון, כי הסינון הוא בהכלה בכל השפות. יעד חיצוני ויעד אנונימי אינם במפה, כי:ref:אינו יכול להצביע אליהם. וקובץ בלי אף כותרת אינו כשל אלא מפה ריקה, כמו קובץ CSS בלי כללים. שתי התנהגויות שכתובות מראש כדי שלא ייראו כבאג: טקסט הכותרת הוא המקור ולא הרינדור (כותרת שנכתבה ``code`` מוחזרת כך), והנקודה בשם אינה מפריד בלעדי — כותרת RST יכולה להכיל נקודה בעצמה, כמו בעמודי ה-API כאן, ולכן השם הוא תווית ל-symbol=וההשתייכות נקראת מטווח השורות. הסורק עצמו דק: הוא נשען על פארסר ה-RST של הפרויקט, שיושר לפארסר של docutils בשינוי שקדם לזה.fix: כותרת שכתובה בקובץ ו-Sphinx מקבל אותה נעלמה מהמפה של
codekeeper_docs_get_section, וכותרת ש-Sphinx דוחה הופיעה בה. הפארסר ב-services/rst_parser.pyמזהה כותרות לפי כלל שנכתב ביד, והושווה עכשיו מול הפארסר של docutils עצמו — שהורץ, ולא רק נקרא. מה שהיה נשמט: קו פיסוק קצר מהכותרת אך באורך ארבעה תווים ומעלה (docutils מקבל אותו עם אזהרה, והפארסר הפיל אותו); כותרת שהקו שמתחתיה מיושר לרוחב התצוגה ולא לספירת התווים — וזה בדיוק המקרה של עברית מנוקדת ושל אמוג’י, כיcolumn_widthשל docutils מחסר תווים משולבים ומכפיל תווים רחבים, ובעמודtestingבריפו הזה יש תקדים חי; וכותרת שהקו שלה נכתב בפסיקים או בנקודה-פסיק, שנעדרו מרשימת תווי ה-adornment שהוקלדה ביד. מה שהופיע בלי שצריך: כותרת שתו הפיסוק שלה מדלג רמה אחרי חזרה לרמה קודמת (check_subsectionמסמןInconsistent title styleואינו יוצר את הסעיף), כותרת שה-overline שלה אינו זהה ל-underline גם באורך, וכותרת מדומה שנוצרה משורה שהיא בעצם פריט רשימה. וההיררכיה עצמה תוקנה: כותרת עם overline היא סגנון נפרד מכותרת עם underline באותו תו, ולכן רמה אחרת. התנהגות התיעוד בריפו הזה לא זזה: הפלט של הכלי על כל קובצי ה-RST כאן זהה בית-בית לפני ואחרי, כולל ה-TOC וטווחי השורות — כל אחת מהצורות האלה מייצרת אזהרה או שגיאה ב-docutils, ו-fail_on_warning: trueב-Read the Docs היה מפיל את הבנייה מזמן. הבדיקות נשענות על שני אורקלים שנכתבו בנפרד מהמימוש: אחד מזהה כותרות מצורת הטקסט בלבד, והשני משחזר את חישוב הרמות שלcheck_subsection.fix: פסקה בת שתי שורות ואחריה קו פיסוק ייצרה כותרת מדומה, ושורה מבנית הפכה לכותרת בשם עצמה. סבב ריוויו על התיקון שלמעלה חשף פינה שהטבלה שבה אימתתי אותו לא מגיעה אליה: כל הצורות שנבדקו שם היו כותרת אחת בת שורה אחת. מצב
Textב-docutils הוא ”השורה השנייה של בלוק טקסט“, וכשהיא אינה קו פיסוק הוא קורא את כל הבלוק עד השורה הריקה כפסקה אחת — כלומר קו פיסוק שיושב בתוך פסקה אינו נבדק בכלל. והכלל אינו ”אחרי שורה ריקה“ אלא ”פסקה שכבר נפתחה“, וזו הבחנה שנמדדה: בולט, שורת שדה, דירקטיבה,line_block, ראש טבלת grid ויעד אנונימי — כולם מעל כותרת ובלי שורה ריקה ביניהם — וה**כותרת כן נוצרת**. בכיוון ההפוך, שורה מבנית אינה יכולה להיות הכותרת עצמה, ולפני התיקון כל אחת מהן הפכה לכותרת בשם עצמה. שתי התבניות שיוצאות מהכלל הןenumeratorו-option_marker, שנופלות חזרה למעבר ה-textולכן מתנהגות כמו פסקה לשני הכיוונים. בנוסף: היקף טבלה פשוטה נקרא עד הגבול הסוגר ולא עד השורה הריקה, וכותרת מוזחת בין שני קווי overline זהים היא כותרת (Line.indentמנותב לאותו מסלול). טבלת האימות הורחבה בהתאם — שורה קודמת מכל סוג כפול שורת כותרת מכל סוג — והמימוש מסכים עם docutils בכל הצורות בשתי הטבלאות. הפלט על קובצי ה-RST בריפו הזה לא זז.
2026-09-10
fix: בדיקה שחורגת מתקרת הזמן הרגה את התהליך שמריץ אותה במקום להיכשל, ולקחה איתה את שמה.
pytest.iniקבעtimeout_method = thread, ובשיטה הזאתpytest-timeoutקוראos._exit. תחתpytest-xdistה-controller רק רואה תהליך שנעלם ומדפיסnode down: Not properly terminated— בלי שם הבדיקה, בלי שורה ובלי מחסנית, כי ה-worker כותב אותן ל-stderr שלו ויוצא לפני שהפלט נשטף. וזה קרה בפועל, פעמיים: ריצה אחת נתקעה על 99% ונשרפו בה עשרים דקות עד ביטול ידני, והזיהוי דרש הורדת הלוג המלא והצלבה בין שתי גרסאות פייתון. השורה הוסרה, ו-pytest-timeoutבוחר עכשיו את השיטה לפי הפלטפורמה כפי שהוא מיועד. נמדד, עם תקרה של שלוש שניות במקום שישים: בשיטה הקודמת התהליך מת ואפילו הבדיקה שאחרי החורגת לא רצה; עכשיו הבדיקה האיטית נכשלת בשמה והשאר ממשיכות. ונמדד גם שברירת המחדל תופסת את שלוש הצורות שהיה סביר לחשוד בהן: תקיעה ב-thread שאינו הראשי, קריאת C חוסמת, ובדיקות אסינכרוניות בשתי הצורות.fix: ההגנה מ-ReDoS מחקה את שעון הזמן של מי שקרא לה.
ConditionOperators.regexדורס אתSIGALRMלשנייה אחת כדי לעצור ביטוי שמתפוצץ, וסיים ב-signal.alarm(0). אבל ``alarm`` ו-``setitimer(ITIMER_REAL)`` הם אותו שעון אחד בתהליך — נמדד: אחריsetitimer(60)הקריאהalarm(1)מחזירה60, כלומר היא רואה אותו, ואחריalarm(0)נשאר0.0. הקוד שמר ושחזר את ה-handler ומחק את ה-שעון, ושמירת אחד מהשניים בלי השני היא כל הפער. זה באג בקוד ייצור — הוא מוחק שעון של כל צרכן, מי שזה לא יהיה — והוא נעשה קריטי מהרגע שתקרת הבדיקות עברה לשיטה שמשתמשת באותו שעון: נמדד דרך pytest אמיתי שבדיקה שמחקה את הרצף ואז ישנה עשר שניות תחת תקרה של שלוש עברה. עכשיו נשמר גם השעון, ומה שנשאר בו מוחזר פחות מה שנצרך;getitimer/setitimerולאalarm, כיalarmעובד בשלמים ומעגל למעלה.fix: בדיקות הדפדפן הכריעו בנפרד בכל בדיקה אם יש דפדפן, כל אחת דרך הרמת תהליך דרייבר ותפיסת חריגה. ”יש דפדפן או אין“ היא עובדה אחת על הסביבה, קבועה לאורך כל הריצה, וההכרעה נעשית עכשיו פעם אחת. נמדד על סוויטת הדפדפן כשאין דפדפן: הזמן ירד פי 33, ומהרמה לכל בדיקה להרמה אחת בסך הכול. והצורה ההיא לא רק בזבזה זמן: מסלול שנשען על חריגה מטופל היטב כשהחריגה נזרקת ואינו מטופל בכלל כשהמסלול פשוט אינו חוזר — ובריצת CI על פייתון 3.12 בדיקות שנשענות על פיקסצ’ר אחד דולגו כרגיל ובדיקה שנשענת על אחר לא חזרה, חצתה את התקרה והרגה עובד
xdistשלם, בזמן שאותה בדיקה בדיוק דולגה על 3.11 באותה ריצה ועל אותו קוד. והדילוג נאמר בקול. ל-CI אין שלב שמתקין דפדפן, ולכן זה המצב הרגיל שם — והריצה מצהירה במפורש שהכיסוי של בדיקות הדפדפן בה הוא אפס, במקום להיבלע בתוך שאר הדילוגים.fix: סדרות escape לא חוקיות ב-docstrings, שפייתון 3.12 מזהיר עליהן ובגרסה עתידית לא ייטענו בכלל. לוכסן-רווח הוא ”רווח מוברח“ של RST ונחוץ כדי לחבר סימון inline לטקסט שצמוד לו, אבל במחרוזת שאינה raw הוא סדרה שאינה מוכרת לפייתון. ריצת CI על 3.12 הדפיסה את האזהרה, והתיקון הוא ה-
rשלפני שלושת הגרשיים ולא הסרת הלוכסן — שמשנה את מה שהתיעוד מציג. ובאחד המקומות זה שיבש את הטקסט עצמו ולא רק הזהיר: אותו docstring הכיל גם\n, שהיא סדרה חוקית, ולכן היא הפכה לירידת שורה אמיתית וחתכה משפט באמצע. והסריקה שמצאה אותם עברה שני ניסיונות שנכשלו, ושניהם מתועדים בבדיקה שנוספה: הראשון סינן לפיSyntaxWarningבלבד, וב-3.11 הקטגוריה היאDeprecationWarning— הוא החזיר אפס על ריפו שהיו בו מופעים; השני חיפש לוכסן צמוד למרכאות הפוכות ופספס את\s. מה שקבוע בין הגרסאות ובין הצורות הוא נוסח ההודעה, וזה מה שהבדיקה בודקת.docs: ``symbol=`` עובד על כל שם שהמפה מחזירה, ולא רק על המנוקדים — וזה לא היה כתוב. בתיאור הכלי הפילטר ישב בתוך המשפט של פייתון, ומיד אחריו בא המשפט שאומר ש-HTML נותן שמות שטוחים; סוכן שקורא את זה קושר אותו לשמות מנוקדים ולא ינסה אותו על
@media. וזה לא היפותטי — נמדד שהוא מצמצם 486 סימבולים בחמישה עמודים לשישה בעמוד אחד, ובכל זאת נשכח. נוספה פסוקית עם שתי דוגמאות קונקרטיות, וגם דוגמה שאינה פייתון בפסקת ה-symbol=בתיעוד:symbol="block "מחזיר את תגיות ג’ינג’ה בלבד, ו-symbol="#"את כל מה שנושאid— גם עוגני HTML בצורתtag#idוגם סלקטורי id מתוך<style>, כי הסינון הוא בהכלה ולא בתחילית. אפס שינוי התנהגות: אף שורת סורק לא נגעה.וההבטחה הזאת קיבלה מי שאוכף אותה. כל טסטי ה-
symbol=היו עד כה על שמות פייתון, בזמן שארבעה docstrings כבר מזכירים אותו בהקשר CSS ו-HTML — כלומר פסוקית בתיאור בלי טסט הייתה הבטחה לצרכן שאיש אינו אוכף. נוספו שלושה טסטים על קבצים שנגזרים מהקורפוס ולא נקובים בשם, והטענה בהם היא שהסינון מצמצם ולא שמוחזרות שורות — טסט אי-ריקנות עובר גם על סינון שאינו מסנן כלום. וגם חוזה ההכלה עצמו קובע עכשיו: נמדד שהחלפתו בתחילית מפילה טסט אחד בדיוק, כלומר עד כאן הוא לא היה מכוסה בכלל.fix: הערת Jinja בתוך בלוק ``<style>`` שברה את הסלקטור, ואף ייצרה סימבול מתוך טקסט ההערה.
{#הוא שני התווים היחידים שבהם CSS ו-Jinja מתנגשות באמת: בקובץ.cssזה סוגר-בלוק ואחריו סלקטור id, ובתבנית זו פתיחת הערה. הוצאת{#מרשימת הפותחים תיקנה את הצד של ה-.cssופתחה שלושה כשלים בצד של התבנית —.a,ואז הערה ואז.b {החזיר שני סימבולים במקום סלקטור אחד, ו-{# TODO .x { #}ייצר סימבול בשם# TODO .x, כלומר שם שנוצר מטקסט של הערה. המפריד אינו צורת הטקסט אלא מי הקורא, וזה מידע שהסורק כבר מחזיק: קובץ.cssמגיע דרך נקודת כניסה אחת וגוף<style>דרך אחרת. ואומת ב-Jinja 3.1.6 שבהקשר תבנית אין בכלל אי-בהירות —{# note #},{# don't do this #}ו-{# TODO .x { #}כולם הערות שנמחקות, ואילו@media print{#a{color:red}}בתבנית מחזירTemplateSyntaxError: Missing end of comment tag, כלומר תבנית שבה{#אינו הערה אינה מתרנדרת בכלל.fix: מחרוזת CSS שלא נסגרה מחקה מהמפה את כל מה שאחריה. אומת ב-Chromium 141:
.x{content:'oopsואז}בשורה הבאה ואז.b{…}נותן שם שני כללים — המחרוזת נקטעת בסוף השורה, ה-}סוגר את.x, ו-.bהוא כלל בפני עצמו. הסורק בלע עד סוף הקובץ, ולכן.bנעלם: תשובת הצלחה עם סימבול חסר. גרש בודד הוא טעות הקלדה נפוצה לגמרי, ולכן זה אינו קלט תיאורטי. ו-\בסוף שורה כן ממשיך את המחרוזת, וגם זה נמדד.fix: תווית הקידוד הצהירה על חתימת BOM שאינה קיימת. התיקון הקודם הקדים את
utf-8-sigברשימת הקודקים, וזה אכן הוריד את ה-BOM — אבל אז כל קובץ UTF-8 דיווחencoding: "utf-8-sig", גם קובץ בלי BOM, כלומר כמעט כל קובץ בכל ריפו.utf-8-sigפירושו ”UTF-8 עם חתימה“, והתווית נוסעת ל-fileבתשובת הכלי. הזיהוי הוא עכשיו ענף מפורש על הבייטים, ולכן התווית אומרת את מה שקרה; וחתימה תקינה מעל גוף פגום נופלת חזרה לרשימה במקום להחזיר כלום.fix: גוף חץ שהמשכיותו מסומנת בתחילת השורה הבאה נסגר מוקדם.
const h = () => listואז.map(…)בשורה נפרדת — שרשור מתודות, כתיב נפוץ לגמרי — קיבל טווח של שורה אחת מתוך שלוש, כיlistהוא מזהה ותקין כסוף ביטוי וההסתכלות הייתה אחורה בלבד. הקבוצה נמדדה ב-Node 22 ברמת הפרסינג, דרך טקסט המקור של החץ כפי שנפרס:.+-*/%^&|&&||??<>=??.([ו-backtick ממשיכים את הביטוי. וארבעת החריגים הם העיקר:.5הוא ליטרל מספרי ולכן נקודה שאחריה ספרה מסיימת,++ו---מסיימים אף ש-+ו--ממשיכים, ו-,מסיים כאן כי כל חץ ממתין הגיע מהצהרתconst/let/varושם הפסיק מפריד בין מוצהרים.והקורא-קדימה הזה חייב זיכרון, וזה נמדד ולא הונח. הוא נקרא בכל ירידת שורה שיש בה חץ ממתין, ורצף ארוך של הערות אחרי ה-
=>פירושו שכל ירידת שורה סורקת מחדש את שאר הרצף — פי ארבע לכל הכפלה, ו-8.9 שניות על 8,000 שורות מול 0.011 שניות עם הזיכרון, פי 809. זו בדיוק הריבועיות שנמדדה כאן קודם, רק מהצד השני: אז הסריקה החוזרת הייתה אחורה, וכאן קדימה.fix: קובץ CSS ממוזער החזיר מפה ריקה, עם ``status: ok``.
{#נקרא כפותח הערת Jinja, אבל ב-CSS ממוזער הוא שני דברים צמודים — סוגר-בלוק-פותח ואחריו סלקטור id.@media print{#a{color:red}}ואחריו.b{color:blue}החזיר רשימה ריקה: הדילוג בלע את כל מה שעד ה-#}הבא, ובהיעדרו את שאר הקובץ. אומת ב-Chromium 141 שהקלט הזה נותן שני כללים, ולכן מפה ריקה עליו היא תשובת הצלחה עם תוכן שגוי.{#הוצא מרשימת פותחי ה-Jinja, וזה אינו מפסיד דבר:{#…#}מאוזן בסוגריים מעצמו, ולכן הערת Jinja ממשיכה להיעלם מהמפה בלי טיפול מיוחד. ומבנה Jinja שבאמת לא נסגר מדלג עכשיו על שני תווי הפותח בלבד ואינו מוחק את הכללים שמתחתיו — אותה החלטה שכבר נלקחה בסורק ה-HTML.fix: ``url()`` שמכיל ביטוי Jinja סגר את הבלוק בשורה הלא נכונה.
url({{ url_for('static', filename='a.png') }})הוא הצורה הרגילה בתבנית, ובה ה-)הראשון סוגר אתurl_forולא אתurl— משם הלקסר חזר לקוד בתוך הביטוי, ראה שם}וסגר בלוק שעוד לא נגמר. הדילוג הוא עכשיו לולאה שמכבדת מבני Jinja כיחידה.fix: קלט של עשרות קילובייטים תפס את השרת לשניות ארוכות, בשני מסלולים. בגוף חץ ללא סוגריים מסולסלים, השאלה ”האם הגוף בכלל התחיל“ נשאלה מחדש בכל ירידת שורה בסריקה חוזרת מה-
=>— גדילה של פי ארבע לכל הכפלה, 80KB ב-1.7 שניות, ובהסקה לתקרת ה-10MB שעות. עכשיו התשובה נגזרת מהיסט של הטוקן האחרון, כלומר השוואת שני מספרים: אותו קלט ב-0.003 שניות, פי 554, וגדילה לינארית. במקביל, שלוש קריאות חיפוש בסורק ה-CSS לא קיבלו גבול — לא נראה בקובץ.cssשלם, אבלhtml.pyקורא לסורק פעם אחת לכל בלוק<style>, ולכן חיפוש שמתעלם מהגבול סורק את כל שאר הקובץ בכל קריאה. תבנית של 1MB ירדה מ-5.7 שניות ל-0.27. זה חשוב במיוחד כאן כי הכלי סינכרוני וה-SDK של MCP קורא לו ישירות — קלט אחד כזה נועל את הלולאה לכל השאר.feat: תקרה על מספר הסימבולים בקובץ, משותפת לשלושת הסורקים. קלט קטן יכול לקנות עבודה גדולה: 3MB של
<a>שאין לוidאינו מייצר אף שורה במפה ובכל זאת נמדד ב-122.5MB, כי כל תגית ממתינה במחסנית לסוגר שלא יבוא, ו-3MB שלa{נמדדו ב-363MB. שניהם נכנסים בנוחות לתקרת ה-10MB שלפני הסורק. מעל 50,000 סימבולים התשובה היאno_outlineעםreason: too_many_symbols— בליsymbols, בליtotalובלי חיתוך, כי רשימה חלקית שנראית שלמה היא בדיוק הכשל שהמפה קיימת כדי למנוע. התקרה חוסמת גם את המחסניות ולא רק את רשימת השורות, והתקציב הוא לכל הקובץ ולא לכל בלוק<style>/<script>. המספר נמדד ולא נבחר, והנימוק כולל שתי המדידות כתוב ליד הקבוע.fix: BOM בתחילת קובץ נסע לתוך הטקסט, ופגע בשני מסלולים. רשימת הקודקים בשירות המראה ניסתה
utf-8לפניutf-8-sig, וקובץ עם BOM נפרס בהצלחה כ-utf-8— כלומרutf-8-sigהיה בלתי ניתן להגעה בדיוק במקרה שהוא קיים בשבילו. הנזק נמדד: בקובץ פייתוןast.parseנכשל ב“invalid non-printable character U+FEFF“ והאאוטליין חזר ריק, ובקובץ CSS ה-BOM נכנס לשם הסימבול הראשון. אומת ב-Chromium שגיליון חיצוני עם BOM נותן ``.hero`` בלבד, כלומר ה-BOM הוא מטא-דאטה של קידוד ולא תוכן — ובתוך<style>, שם הוא מגיע כתו, הדפדפן כן משאיר אותו בסלקטור. החלפת הסדר שקילה לחלוטין: על 60,000 קלטים אקראיים שני הקודקים החזירו פלט זהה, פרט לקלט שמתחיל ב-BOM.fix: ``<style type=“ text/css ”>`` נסרק כ-CSS בזמן שהדפדפן אינו מחיל אותו. גטר התכונות המשותף עשה
strip()בשביל צרכן אחד, ושלושת הצרכנים דורשים שלושה דברים שונים — נמדד ב-Chromium 141:<script type=" text/javascript ">כן רץ,<style type=" text/css ">אינו יוצר גיליון (התאמה מדויקת, וכל רווח בכל מקום מבטל אותה), ו-<span id=" x ">שומר את הרווחים —getElementById(" x ")מוצא אותו. הערך חוזר עכשיו גולמי וכל צרכן מנרמל בעצמו, ושלושתם מסכימים עם הדפדפן.fix: גוף חץ שהוא מערך רב-שורתי נסגר לפני ה-``]``.
const f = () => [ואז שלוש שורות נסגר בשורה השלישית, כי2הוא סוף ביטוי תקין ואין מי שיידע שהמערך עוד פתוח. עומק הסוגריים המרובעים נמדד עכשיו, בדיוק כמו העגולים. ומה שנשאר פתוח מוצהר במפורש: המשכיות שמסומנת בתחילת השורה הבאה — שרשור מתודות — עדיין נסגרת מוקדם, כי ההסתכלות היא אחורה בלבד, ויש לזה טסט שמקבע את המצב הקיים.וההגנה על התפר קיבלה מיקום ואיזון, ולא רק ספירה. לבלוקי ה-
<style>הייתה עד כה בדיקת ספירה בלבד, וספירה אינה אומרת דבר על מקום: מוטציה שמזיזה כל שורה באחת עברה את כל החבילה. שני אורקלים חדשים נופלים עליה ב-1,995 הפרות מיקום וב-1,378 הפרות איזון. במקביל יושר האורקל הבלתי-תלוי של ה-CSS לשני הכללים החדשים, ונוסף בו במפורש שאסור לייבא ממנו מהמימוש — כי השכפול שם הוא ההגנה, וברגע ששני צידי ההשוואה יוצאים ממקור אחד קריסה חלקית עוברת בשקט.fix: גוף חץ שמתחיל בשורה שאחרי ה-``=>`` קיבל טווח באורך שורה אחת.
const f = (a) =>ואזa + 1;בשורה הבאה החזירend == startעל פונקציה שלמה, וגם גוף ביטוי שנפרס על כמה שורות נסגר מוקדם. זו אותה מחלקה בדיוק שסגנון Allman היה בה: הענף שסוגר חץ בגבול שורה לא הבחין בין ”אין גוף מסולסל“ לבין ”הגוף מתחיל בשורה הבאה“. שתי עובדות דקדוק אומתו ב-Node 22 ולא נכתבו מהזיכרון: גוף חץ אינו יכול להיות ריק —const f = (a) =>ואז;הוא שגיאת תחביר, ולכן ירידת השורה שצמודה ל-=>לעולם אינה מסיימת את הגוף; ושורה שנגמרת באופרטור משאירה את הביטוי חסר, בשאלה דקדוקית טהורה שאין בה תלות ב-ASI —(a +)הוא שגיאת תחביר בזמן ש-(a + b)תקין. אותה מדידה דחתה את! ~ { ; ) ]ואת מזהים וספרות, ואישרה את:בהקשר של תלתן. הפלט על כל התבניות, על 41 קובצי ה-JS ועל ה-bundle של 7MB זהה לחלוטין לפני ואחרי — הצורה אינה קיימת בקורפוס בשם, ושני המופעים שכן קיימים הם חצים אנונימיים ב-.map().feat: מפת הסימבולים (``outline``) תומכת ב-CSS, וגם יורדת לתוך בלוקי ``<style>`` בתבניות.
.cssמקבל סימבול לכל בלוק סלקטור, ובעיקר ל-at-rules —@media (max-width: 768px)ו-@supports (display: grid)מקבלים טווח שורות משלהם, ושם קבורים המובייל וה-RTL. סלקטורים מרובים שפרוסים על כמה שורות הופכים לשם אחד מנורמל, בלוקים שבתוך@mediaהם סימבולים שטוחים בפני עצמם, וצעדי@keyframes(0%,to) אינם סימבולים כי הם רעש ולא ניווט. at-rule שנגמרת ב-;אינה סימבול — אין לה טווח.וזה מה שפתח את בלוקי ה-``<style>`` שהיו גבול אטום. בתבניות של הפרויקט יושב בבלוקים האלה נפח CSS שדומה לזה של קובצי ה-
.cssעצמם, והם הוחזרו כסימבול אחד לכל בלוק — ב-dashboard.htmlבלוק אחד נמשך יותר מאלף שורות. זה בדיוק הכשל ש-<script>תוקן ממנו, ולא היה אפשר לתקן אותו עד שיהיה סורק CSS. עכשיו גם מקור האמת של טוקני הערכה נגיש: ה-:rootהגלובלי ובלוקי:root[data-theme="..."]יושבים ב-base.htmlולא בקובץ CSS.הסורק לקסיקלי, ו-Jinja מדולג ללא תנאי. בקובצי
.cssבריפו אין מבני Jinja בכלל, אבל בבלוקי<style>יש —:root[data-theme="custom"]בונה את הטוקנים שלו בלולאת{% for %}. לקסר שאינו מדלג סופר שם בלוק לכל שורה במקום בלוק אחד. שני הכיוונים נמדדו, ולכן הדילוג אינו דגל אלא התנהגות.ההערה שמעל הסלקטור אינה חלק מהשם, וזה הדבר המרכזי בסורק. אב-טיפוס שלא הסיר אותה החזיר שם באורך 1,699 בתים שהוא הערה בעברית ולא סלקטור, וגם עיוות את ספירת ה-
@media. אחרי ההסרה כל המספרים התיישבו בדיוק על מונה{בלתי-תלוי שנכתב בנפרד. הערה בתוך סלקטור אינה שוברת אותו:.a /* x */ .bהוא סלקטור אחד.שני כללים חיצוניים אומתו מול Chromium ולא נכתבו מהזיכרון. ב-CSSOM,
@keyframesהוא היחיד שהילדים שלו אינם כללים עם סלקטור —@media,@supports,@layer,@containerו-@scopeכולם מחזירים כלל עם סלקטור, ו-@-webkit-keyframesממופה לאותו כלל כמו הצורה בלי תחילית. ובבדיקה השנייה:<style type="text/plain">אינו יוצר גיליון סגנון, וגםtype="text/css; charset=utf-8"אינו — פרמטרים נדחים, בדיוק כמו כלל ה-essence של<script>.מונה התפר תפס ``<style>`` שאינו אלמנט בכלל.
admin_observability.htmlבונה מסמך להדפסה בתוך מחרוזת JavaScript, ובה<style>עם כללים. הסורק צדק שהוא אינו מדווח אותו, והמונה — רג’קס על הטקסט הגולמי — היה זה שספר יותר. אומת בדפדפן ש-<style>בתוך גוף<script>,<textarea>,<title>או הערת HTML אינו אלמנט סגנון, והמונה מנטרל את ארבעת האזורים לפני שהוא סופר.בקובץ ממוזער כל הסימבולים מצביעים לאותה שורה, וזו התנהגות מוצהרת ולא באג. קובץ שכל תוכנו בשורה אחת מחזיר מפה שכל רשומה בה אומרת אותה שורה, כי שם הבלוקים באמת יושבים; ה-
startוה-endנכונים והחוזה נשמר. ההצהרה נכתבה בתיעוד ולא רק בהערת קוד, כי מפה כזאת נראית כמו סורק שבור למי שלא יודע.וקבוצת הסימבולים שהייתה לפני נשמרה בדיוק. הפלט על כל התבניות הושווה לפני מול אחרי כ**השוואת קבוצות** ולא כספירה — סימבול שזז בשורה וסימבול חדש שנוסף באותו מקום נותנים אותה ספירה ומסתירים זה את זה. כל סימבול קיים נשאר עם אותו שם ואותן שורות, וכל סימבול חדש נבדק שהוא יושב בתוך גבולות בלוק
<style>ושהטווח שלו מאוזן בסוגריים.fix: סוגר מסולסל בשורה נפרדת סגר את הפונקציה בשורת החתימה. בסגנון Allman —
function foo()ואז{בשורה הבאה — המפה החזירה טווח באורך שורה אחת על פונקציה שלמה, בשלוש הצורות: הצהרה, ביטוי מוצב וחץ. הענף שסוגר בסוף שורה קיים בשביל חץ בלי גוף מסולסל, ולא הבחין בין ”אין גוף“ לבין ”הגוף מתחיל בשורה הבאה“. ההחלטה נלקחת עכשיו ברגע ההתאמה: אומת ב-Node 22 ש-``function`` בכל צורותיו הוא שגיאת תחביר בלי גוף בסוגריים מסולסלים, ולכן צורה כזאת ממתינה תמיד; אצל חץ נבדק מה בא אחרי ה-=>, תוך דילוג על הערות — כיfunction foo()ואחריו שורת הערה ואז{הוא קוד תקין.fix: הטסטים חיפשו את קבצי הריפו לפי ספריית העבודה, ודילגו בשקט כשלא מצאו. הרצה מספרייה אחרת לא נכשלה אלא דילגה — ובין המדולגים היו המונים שרצים על כל התבניות, כלומר ההגנה על באגי הסורק נעלמה והחבילה דיווחה ירוק. הנתיבים נגזרים עכשיו מהקובץ עצמו, ותיקיית תבניות שקיימת אך ריקה נכשלת במקום להידלג.
2026-09-09
fix: מספר השורה במפת ה-HTML נגזר מהאינדקס במקום להיות מתוחזק תוך כדי סריקה. קפיצה שמזיזה את מקום הקריאה ביותר מתו אחד עקפה את ספירת השורות, ומשם כל סימבול נוסף באותו בלוק סקריפט קיבל שורה שגויה — בשקט, עם
status: "ok". שני מסלולים היו כאלה:\ואחריו שורה חדשה אמיתית בתוך מחרוזת (המשך שורה חוקי ב-JavaScript), וחתימת פונקציה שנפרסת על כמה שורות. עכשיו הטבלה נבנית פעם אחת מכל\nבקובץ, ואין ספירה שאפשר לדלג עליה.fix: ``const f = function (a, opts = {})`` קיבל טווח באורך שורה אחת. התיקון הקודם ל“ברירת מחדל סוגרת את הפונקציה בשורת החתימה“ נשען על צורת הטקסט שהותאם, ולכן לא חל על צורת הביטוי המוצב. ”האם אנחנו עדיין ברשימת הפרמטרים“ נגזר עכשיו מעומק הסוגריים העגולים שהלולאה סופרת בעצמה.
fix: סוגר עגול שלא נסגר בחתימה השתיק את שאר בלוק הסקריפט. המצב ”בתוך חתימה“ חסם כל זיהוי הגדרה חדשה, ולכן טעות הקלדה אחת גרמה לכל פונקציה שאחריה באותו בלוק לא להופיע במפה בכלל. עכשיו פונקציה אחת מתקלקלת ולא הבלוק כולו.
fix: תגית Jinja שלא נסגרה גנבה את הסוגר של התגית התקינה שמתחתיה. חיפוש ה-
%}החזיר את המופע הבא בקובץ ולא את הסוגר של התגית עצמה, ולכן{% block %}תקין שיושב אחרי תגית שבורה נעלם מהמפה לגמרי. תגית שלא נסגרה אינה תגית: מדלגים עליה וממשיכים לסרוק, וגם אינה מחזירה עוד שם קטום.fix: ``id`` או ``type`` שיושבים בתוך ערך של תכונה אחרת ניצחו את האמיתיים. התכונות נקראו ברג’קס על המחרוזת הגולמית ונפרסות עכשיו פעם אחת. שתי תוצאות: עוגן שגוי שאינו קיים בקובץ, ובלוק
<script>שסווג כלא-JavaScript וכל הפונקציות שבתוכו נעלמו. אומת מול Chromium 141:<div title="x id=decoy" id="real">נותןreal, והבלוק עםdata-note="see type=text/plain"כן רץ.fix: לוכסן בסוף ערך תכונה לא מצוטט נקרא כסגירה עצמית.
<a id="k" href=/>סומן כתגית שסוגרת את עצמה, ולכן קיבל טווח באורך שורה וה-</a>שאחריו לא מצא התאמה. אומת מול Chromium: הלוכסן שייך לערך, והטקסט שאחרי התגית יושב בתוכה./>על אלמנט ב-SVG ממשיך לסגור את עצמו.fix: הרג’קס שמזהה ``function`` היה ריבועי, ורקורסיה בסורק המחרוזות הייתה בלי חסם. בצורה הקודמת שני
\s*היו צמודים ומופרדים באטום אופציונלי, ולכן התאמה כושלת אחת מול רצף רווחים עלתה ריבועית: על אותו קלט סינתטי, 9.8 שניות ל-40KB לפני ולעומת אלפית שנייה אחרי, וסדר גודל של שבוע בתקרת ה-10MB שהכלי מתיר. ובמקביל,${…}מקונן נכנס למסגרת רקורסיה חדשה לכל רמה, ולכן קובץ של כשלושה קילובייטים הפילRecursionErrorשאיש לא תפס לאורך המסלול עד לקוח ה-MCP — סתירה למה שהמודול מצהיר על עצמו, שערוץ הכשל הוא ערך ההחזרה ולא חריגה. מחסנית מפורשת החליפה את הרקורסיה, וחמישים אלף רמות קינון עוברות עכשיו.הפלט על התבניות האמיתיות זהה, וזה נמדד ולא הונח. הסורק שלפני התיקון ושאחריו הורצו על כל התבניות בפרויקט והחזירו את אותם סימבולים בדיוק, עם אותם מספרי שורה. כל חמשת הבאגים הם צורות שאינן קיימות בתבניות של הפרויקט היום — הם נכנסו כי הכלי משרת כל ריפו ממורר, לא רק את זה.
feat: מפת הסימבולים (``outline``) תומכת בתבניות HTML/Jinja.
{% block %},{% macro %},{% extends %}/{% include %}, אלמנטים עםid, בלוקי<script>/<style>— ובעיקר הגדרות הפונקציות בתוך בלוק סקריפט. ב-base.htmlהבלוק הגדול ביותר הוא מעל שבע-מאות שורות, ובלי ירידה לתוכו המפה נותנת גבול ולא ניווט. השמות שטוחים ולא מנוקדים, כי ב-HTML אין מרחבי שמות; התחילית נוספת רק כשלבלוק ישid.הסורק הוא לקסיקלי ולא פרסר עץ, ובכוונה. תבנית Jinja אינה HTML תקין — תגית שנפתחת בענף אחד של
{% if %}ונסגרת באחר היא הכתיב הרגיל. פרסר DOM מאזן את זה בשקט ומחזיר שורות שאינן במקום שבו הטקסט יושב.שלושה כללים חיצוניים אומתו מול Chromium ולא נכתבו מהזיכרון. רשימת ה-void elements (
paramהוצא מהמפרט אבל הדפדפן עדיין מתייחס אליו כך — נלקח האיחוד); ההבחנה בין<script>שהוא JavaScript לבין data block, לפי ”MIME type essence match“ —text/javascript; charset=utf-8אינו רץ; והעובדה ש-</script>בתוך מחרוזת JavaScript כן סוגר את האלמנט, כך שהיציאה מהבלוק אינה מכבדת מחרוזות.refactor: ``extract_outline`` הפכה למנתב לפי סיומת, והלוגיקה של כל שפה יושבת ב-
mcp_server/outline_scanners/. המיון והסינון נשארו במנתב, כדי שסורק חדש לא יוכל לשכוח אחד מהם.fix: קובץ עם ``r`` בודד נדחה כ-``no_outline`` במקום להפיל ``IndexError``.
astסופר עם universal newlines וקריאת הטווח מפצלת ב-split("\n"); בקובץ כזה השתיים נפרדות, ומפה שהייתה חוזרת מצביעה לשורה ש-lines=לא מגיע אליה. CRLF אינו מושפע.feat: בורר גודל טקסט לפתקים בעמוד ההגדרות. רגיל (14px), בינוני (15px) וגדול (16px), בסעיף ”גופן הפתקים“. הבחירה חלה על שני המשטחים יחד — דפדפן הריפו וקבצי Markdown — ואי אפשר להפריד ביניהם. הלוחות אינם מושפעים: להם נשאר הבורר שבמודאל, לכל לוח בנפרד.
הבחירה נשמרת בשרת ולא ב-``localStorage``, ולא מטעמי סגנון. ה-cookie של ההעדפה הוא
httponly, ולכן JS אינו יכול לקרוא אותו כלל; והיא צריכה לעבור בין מכשירים, מה ש-localStorageאינו עושה. אותו מסלול בדיוק שבו עוברת בחירת כתב היד שלצידה.בורר תחולה נפרד לגודל. ”לכל המכשירים“ / ”רק במכשיר הזה“ משלו, ולא זה של כתב היד — כדי שאפשר יהיה להחזיק טקסט גדול בטאבלט וכתב יד בכל המכשירים. שני cookies נפרדים, ובדיקה שמוודאת שאחד אינו גורר את השני.
ההעדפה נכנסה לוולידטור של ה-ETag, ולא רק ל-DB.
/md/<id>מרנדר את הגודל לתוך ה-HTML; בלי זה שינוי ההגדרה היה מחזיר 304 והדפדפן היה מציג את הגודל הישן. אותה מלכודת שבגללה נוצר_note_fonts_etag_keyעבור כתב היד — והמפתח נושא היום את שתי ההעדפות. באותה נשימה נוספו השדה לפרויקציה של שליפת מסמך המשתמש ולתנאי שמדלג על השליפה: שדה שחסר באחת מהן אינו קורס, הוא פשוט חוזר לברירת המחדל.הקוקי נבדק בתגובה עצמה, ולא רק הסטטוס. במצב ”רק במכשיר הזה“ שום דבר אינו נכתב ל-DB, ולכן ערך שנשכח ברשימת הקוקיז מחזיר 200 בלי לשלוח את הקוקי — כשל שכבר קרה כאן פעם. בדיקה שהסתפקה ב-200 הייתה עוברת עליו במלואו.
refactor: ”אילו גדלים קיימים“ ו“מה כל תפריט מציע“ הופרדו לשני מקורות.
STICKY_NOTE_FONT_MENUSהצטרפה למפה, כי שני הבוררים אינם מציעים את אותם גדלים — הלוח 14/16/18 ועמוד ההגדרות 14/15/16. מקור אחד היה מכריח כל תפריט להציע את מה ששייך לשני.fix: ערך ששייך לעמוד ההגדרות היה מרוקן את הבורר שבלוח. הלוח הציב את הערך השמור על ה-
<select>אחרי אימות מול המפה; ערך שאין לו<option>הופך אתselectedIndexל--1במקום ליפול ל“רגיל“. הלוח מאמת עכשיו מול התפריט שלו, גם בהצבת הבורר וגם בסקריפט הראש — אחרת התצוגה והבורר מציגים שני דברים שונים.fix: מנשא בדיקות ה-JS החליף רק את המופע הראשון של ביטוי Jinja. ביטוי שני היה נשאר בקוד כטקסט ומפיל את ההרצה בשגיאת תחביר שאינה מרמזת על הסיבה. ריצת בקרה: 25 מתוך 26 בדיקות נפלו.
replaceAllמסיר את המלכודת, וההערה בתבנית כבר אינה מנמקת את מבנה הקוד במגבלת הבדיקה.fix: בדיקות עמוד הלוח טענו על מחרוזת ולא על האלמנט.
'noteFontSizeSelect' in htmlהתקיים גם אחרי שהבורר נמחק מהמודאל, כי אותו מזהה חוזר גם ב-getElementByIdשבגוף העמוד. שתי הבדיקות — הבורר ומתג כתב היד — טוענות עכשיו על התגית עצמה.feat: בורר גודל טקסט בהגדרות הלוח. שלוש אפשרויות — רגיל (14px, ברירת המחדל), גדול (16px) וגדול מאוד (18px) — לכל לוח בנפרד, נשמר מקומית בדפדפן כמו שאר הגדרות הלוח. הפתק כולו גדל: נמדד בכרומיום, ותשעה רכיבים — תצוגה, תיבת עריכה, כותרת, קוד בשורה, בלוק קוד, תווית שפה, טבלה, גוף אלרט והזחת קינון — יצאו ביחס זהה בדיוק בשלושת המצבים. לוח שנשאר על ”רגיל“ נראה ביט-זהה למצב הקודם. הבורר הזה הוא לכל לוח בנפרד; לשאר המשטחים יש בורר משלהם בעמוד ההגדרות, ראו למעלה.
refactor: ``font-size: 14px`` היה מוצהר פעמיים ואוחד לטוקן אחד. הוא ישב על תיבת העריכה ועל התצוגה שמחליפה אותה, עם הערה שהן ”אותו ארגז בדיוק“ — שני מספרים שחייבים להישאר שווים ואיש לא אכף. שניהם קוראים עכשיו מ-
--sticky-note-font.הערכים בפיקסלים ולא ב-``em``, וריצת בקרה הראתה למה — חמור ממה שתועד. ההערה הקיימת בקובץ תיעדה ניסיון קודם שבו
1.08emהחליף 14px ב-17.28px, כיemב-font-sizeנפתר מול ההורה. במדידה הפעם, בסיס ב-emנתן לשני הצרכנים ערכים שונים — 18.24px לתצוגה ו-14.82px לתיבת העריכה — כלומר הוא שובר גם את ההבטחה ששני הארגזים זהים.הערך השמור מאומת מול מפה ואינו מורכב לשם מחלקה.
localStorageהוא קלט חיצוני;'sticky-font-' + sizeהיה מכניס כל מחרוזת ל-classList. הבדיקה מריצה שבעה ערכים פגומים, ובהם__proto__ו-constructor.feat: בלוקי אלרט (``::: note``) מרונדרים בתוך פתק דביק. כל סוגי האלרט שקיימים בתצוגת המסמך —
note,tip,warning,danger,important,info,success,question,example,quote,experimental,deprecated,todoו-abstract— עובדים עכשיו גם בפתק, עם אותו אייקון, אותו צבע ואותה כותרת בעברית. אין כאן פלטה שנייה: המחלקות.admonition/.admonition-<type>מגיעות מ-markdown-enhanced.css, שנטען ב-base.htmlולכן זמין בכל משטח שמציג פתקים;sticky-notes.cssמחזיק רק כיול לפתק הצר. בתוך האלרט עובד כל מה שעובד בפתק — מודגש, קוד, רשימות, טבלאות, גדרות וצ’קבוקסים — כי במקום ענף רינדור שני משתנה בלולאה דבר אחד: לאן שורה נכתבת.החוזה של המנוע נשמר: שורת מקור אחת, אלמנט תצוגה אחד עם ההיסט שלו. שורת
::: noteהיא שורת הכותרת של האלרט ונושאת את ה-charOffsetשלה, ולכן לחיצה עליה מחזירה לעריכה בשורה הנכונה; שורת:::הסוגרת נצרכת בלי אלמנט אבל כן מקדמת את ההיסט, בדיוק כמו שורת המפריד של טבלה. בלי הקידום כל מה שאחרי הבלוק היה מוסט באורך שורה שלמה.התחביר נמדד מול ``markdown-it-container@4.0.0`` ולא נזכר. הושוו רצפי פתיחה-וסגירה מול הספרייה שמרנדרת את תצוגת המסמך, על עשרות קלטים. מכאן:
:::noteבלי רווח תקף;::: NOTEתקף;::: note כותרתנותן כותרת מותאמת;::: note2ו-::: note_xאינם אלרט ואילו::: note.xכן; אלרט בלי שורת סגירה נמשך עד סוף הפתק; ובקינון — שורת סגירה סוגרת את החיצוני ביותר שאורך המרקר שלו קטן או שווה לה, ואיתו כל מה שבתוכו. הכלל האחרון הוא ההפך מהאינטואיציה (”סוגר את הפנימי“), והוא מכוסה בבדיקה על שלוש רמות בשני סדרי סגירה.שתי סטיות מכוונות, ושתיהן מתועדות. בתוך גדר קוד
:::נשאר טקסט ליטרלי — ב-markdown-itהוא כן סוגר, שובר את הגדר לשניים ומוציא את שאר התוכן החוצה, וזו תוצאה גרועה למשתמש פתק וגם סותרת את מה שהפתק כבר עושה עם טבלה בתוך גדר. והזחה אינה פוסלת מרקר: לפתק אין מושג של בלוק קוד מוזח, ו-MD_FENCE_REכבר מזהה גדר בכל הזחה.feat: בלוק קוד בפתק מוצג ככרטיס — בלי תווי הגדר, עם שם השפה וכפתור העתקה. שתי שורות ה-
```נצרכות ואינן מופיעות בתצוגה, בדיוק כמו שורת המפריד של טבלה. שם השפה שאחרי הגדר הפותחת מוצג כתווית קטנה — הסתרת הגדר היא הסתרה ולא איבוד — ולצידו כפתור שמעתיק ללוח את הקוד בלבד, עם חיווי הצלחה וגם חיווי כשל.שורת הפתיחה הפכה לכותרת הכרטיס, ונושאת את ה-``charOffset`` שלה. זה מה שמונע מבוי סתום: בלי שורת מקור בראש הבלוק, בלוק ריק (גדר ומיד גדר) היה יוצא בלי שום שורה לחיצה, ולא הייתה שום דרך לחזור ממנו לעריכה. שורת הסגירה נצרכת בלי אלמנט — החריג היחיד, וזהה לזה של הטבלה ושל האלרט.
refactor: הצריכה כיחידה מחקה שלושה סייגים.
inFenceו-isFenceLineחייבו חישוב מוקדם שלisFenceLineלפני בדיקת הטבלה, הבחנה בין!inFenceל-!inCodeבכלל המחסנית, והחרגה של שורת הסגירה. עם_appendCodeBlockהלולאה לעולם אינה נמצאת בתוך גדר, ושלושתם נמחקו יחד עם הדגלים. ההתנהגות המתועדת ”רק הזחת שורת הפתיחה קובעת“ נשמרה ביט-זהה, והבדיקה שמכסה אותה עברה בלי שינוי.fix: ההערה שנכתבה על הסדר בלולאה הייתה חצי נכונה, ותוקנה לפי מדידה. נטען בה שסדר הבדיקות מגן גם על הטבלה וגם על האלרט. בהיפוך סדר בפועל: מול הטבלה הסדר אכן ההגנה (
```sh | xהיא גם פותחת גדר וגם כותרת טבלה סבירה, והזזת הטבלה קדימה מפילה בדיקה), ומול האלרט הוא אינו משנה דבר — השורות זרות זו לזו, ומה שמשאיר:::בתוך גדר כקוד ליטרלי הוא הצריכה ולא הסדר. סייג שנכתב בלי לבדוק אם הוא נדרש הוא חוב, לא הגנה.ההעתקה קוראת את המקור ולא את ה-DOM. שורה ריקה בבלוק מרונדרת כרווח בודד, אחרת ה-
divבגובה אפס והשורה נעלמת; קריאה מה-DOM הייתה מחזירה רווח במקום שורה ריקה, כלומר קוד שמודבק עם רווח מיותר בכל שורה ריקה. אומת בשני הכיוונים: נמדד ההפרש, ואז לחיצה אמיתית בכרומיום עם קריאה חוזרת מהקליפבורד שהחזירה את המקור ביט-זהה.refactor: מנגנון קליפבורד אחד לשני הכפתורים.
_copyTextהוא התשובה היחידה ל“איך מעתיקים“, ושני הכפתורים עוברים דרכו;_flashCopyResultמקבל יעד מפורש, אחרת לחיצה על כפתור של בלוק קוד הייתה מהבהבת את כפתור הפתק. וה-titleהמקורי נשמר ומשוחזר במקום להיות מוקלד מחדש — כשל בהעתקת קוד היה משנה את ההסבר של הכפתור ל“תוכן הפתק“.fix: רשימה שנפתחה בתוך אלרט דלפה החוצה. שורת הסגירה לא נגעה במחסנית הרשימות, בנימוק שזה מה שעושה שורת גדר סוגרת — נימוק שהעתיק מסקנה במקום את הראיה שמתחתיה. אצל הגדר זו הוכחה ולא בחירה: בתוך גדר שום דבר אינו נדחף למחסנית כי התוכן ליטרלי, ולכן לסגירה אין מה לסגור. בתוך אלרט התוכן עובר את המנוע המלא ו**כן** דוחף. התסמין:
- אואז␣␣- בבתוך האלרט, ו-␣␣- גאחריו יצא בעומק 1, כבן של פריט שכבר אינו קיים. שורת הסגירה עוברת עכשיו בכלל האחיד —_closeListsAboveלפי ההזחה של עצמה — ולכן היא גם סוגרת את מה שנפתח בפנים וגם משמרת רשימה חיצונית כשהאלרט מוזח לתוכה. נמדד מולmarkdown-itעל חמישה-עשר צירופים של רשימה × אלרט: חמישה סטו לפני, ואחד נשאר אחרי — והנותר הוא סטיית בלוק הקוד המוזח שכבר מתועדת. החלופה של שמירת עומק המחסנית בפתיחה ושחזורה בסגירה נמדדה גם היא ותיקנה פחות (שלושה סוטים מול אחד), כי היא מתעלמת מההזחה.refactor: התוויות בעברית של האלרטים אוחדו למקור אחד.
admonition-icons.jsמחזיק עכשיו גםADMONITION_TITLESלצד האייקונים, ושלושת הצרכנים קוראים משם —md_preview.html,live-preview.jsוהפתקים. קודם התוויות הוחזקו פעמיים, וכל עותק יכול היה לקבל סוג חדש בלי שהשני ידע. אומת בהרצה: כל הסוגים שבמפה ממשיכים להתרנדר בתצוגת המסמך עם התווית הנכונה, דרך הקוד שחולץ מהתבנית עצמה.fix: ``note_board.html`` לא טען את מפת האלרטים כלל. הוא טוען את
sticky-notes.js, ולכן בלוח כל אלרט היה מוצג כטקסט רגיל — בלי שגיאה ובלי שום סימן. נוספה שם הטעינה, ולצידה בדיקה שסורקת את כל התבניות ומאמתת שכל אחת שטוענת את המודול טוענת גם את המפה, ולפניו.fix: שורת CSS שנראתה כמו הגנה ולא עשתה דבר. הכיול לפתק כתב
margin: .35em 0ואחריוmargin-inline: 0כדי לבטל את המרווח השלילי שכלל המובייל מחיל על.admonition. מדידה בכרומיום הראתה שהשורה השנייה חסרת השפעה — הקיצור כבר איפס את הצדדים — כלומר גם השומר הטקסטואלי שנכתב עליה שמר על כלום. הפיצול ל-margin-blockול-margin-inlineהופך אותה לנושאת משקל, כי המפל פועל לכל תכונה בנפרד: נמדד בבקרה, פתק ברוחב 260 באזור תצוגה של 390 מקבל0pxעם השורה ו--8pxבלעדיה, והאלרט חוצה את גבול הפתק. באזור תצוגה של 1024 אין הבדל כלל.
2026-09-08
feat: ערכה מיובאת יכולה לקבוע לעצמה את צבע שמות הקבצים בכרטיס אוסף. שם הקובץ בדף
/collectionsהוא<a>, ולכןdark-mode.cssצובע אותו ב-var(--primary)דרך[data-theme="custom"] a— ספציפיות שגוברת על כלל המחלקה שב-collections.css. כרטיס האוסף נשאר לבן גם בערכה כהה, ולכן ערכה שה---primaryשלה לבן הפיקה טקסט לבן על רקע לבן: נמדד בכרומיום מול השרת האמיתי, ניגודיות1.00. הפתרון אינו דריסה גורפת אלא וו בהצטרפות מרצון:--collections-link-overrideנכנס לרשימה הלבנה, ובכוונה נשאר ללא ערך ברירת מחדל בשום מקום — כך שני הכללים ב-collections.cssנופלים ל-var(--primary)במצב רגיל ול-var(--primary-dark)ב-:hover, בדיוק הערכים שנצבעו קודם. נמדד עם שתי ערכות במקביל: המצהירה7.00בשני המצבים, שאינה מצהירה1.00ו-4.81— זהה לחלוטין למצב שלפני השינוי. הווו חל על שני המסכים שמציגים שמות קבצים: כרטיס אוסף (.collection-card__link) ושולחן העבודה (.workspace-card__link). שולחן העבודה הוא מסך נפרד עם מחלקות משלו, ולכן נשאר שבור בסבב הראשון — אותו<a>עםcolor:inherit, אותה דריסה, ואותם מספרים:1.00←7.00.fix: ניסיון ראשון של הווו שינה גם ערכות שלא הצהירו, ותוקן. ברירת מחדל שנקבעה ב-
:root[data-theme-type="custom"]גרמה ל-:hoverלהיפתר ל-var(--primary)בכל ערכה מיובאת, במקום ל-var(--primary-dark). בערכה שה---primaryשלה לבן זה הוריד את ה-:hoverמניגודיות4.81ל-1.00— כלומר שם הקובץ נעלם גם במצב שבו קודם היה קריא. הלקח: טוקן שאמור לשמש כווו דריסה חייב להישאר בלי ערך ברירת מחדל, אחרתvar(--token, <ברירת מחדל>)לא נכנס לפעולה והשינוי הופך לגורף.feat: ערכת VS Code יכולה לשאת בלוק
variablesאופציונלי לצדcolors.parse_vscode_themeקורא רקcolorsו-tokenColors, ול-VSCODE_TO_CSS_MAPאין מפתח שמוביל לטוקנים שאין להם מקבילה ב-VS Code. בלי זה לא הייתה דרך להצהיר על הטוקן בלי לוותר על הדגשת התחביר, כי הפורמט המקומי אינו מעבדtokenColors.fix: ערך פגום בבלוק
variablesנדחה במפורש ולא נזרק בשקט. ולידציית הערכים ב-validate_theme_jsonישבה בתוך הענף שרץ רק כשאיןcolors, ולכן בערכת VS Code ערך כמו"teal"עבר את הוולידציה, נזרק אחר כך עם אזהרת לוג בלבד, והמשתמש קיבל200ו“הערכה יובאה בהצלחה!“ על ערכה שלא תוקנה. הבדיקה חולצה ל-_validate_variables_blockורצה עכשיו בשני המסלולים, עם הודעה שמציינת את המפתח.docs: תוקנה סתירה בין התיעוד לקוד ב-
POST /api/themes/import. העמוד תיעד גוף בקשה עםsourceו-content; הקוד קוראjson_content, ומזהה את הפורמט מתוך התוכן עצמו לפי נוכחותcolors.fix: מחיקה מהוובאפ הורידה לסל גרסה אחת, והקובץ חזר ברענון. קובץ שהמשתמש רואה הוא
(user_id, file_name), ובמסד כל גרסה היא מסמך נפרד. עמוד הקבצים מקבץ את הגרסאות לפי שם ומוסר לממשק את ה-_idשל הגרסה האחרונה בלבד, ו-POST /api/files/bulk-deleteסינן בדיוק לפי המזהה הזה: הגרסה העליונה ירדה לסל, מה שמתחתיה נשאר פעיל, והקובץ חזר לרשימה גרסה אחת אחורה. קובץ עם גרסה יחידה נעלם סופית, ולכן שני קבצים שנמחקו באותה פעולה התנהגו שונה. המזהה משמש עכשיו לזיהוי הקובץ בלבד, והמחיקה עצמה לפי שם — כלומר כל הגרסאות.fix: אותו באג היה גם בבוט, במחיקה המרובה.
rf_delete_doרץ בלולאה עלdelete_file_by_id, שסינן אף הוא לפי מזהה גרסה. מחיקה של קובץ בודד — בבוט ומעמוד הקובץ בווב — הייתה תקינה מלכתחילה, ולכן הפער נראה כמו הבדל בין בוט לווב במקום מה שהוא: הבדל בין מחיקה בודדת למחיקה מרובה.fix: כשל במחיקה מהבוט הוצג כהצלחה. הלולאה בלעה חריגות והמונה נשאר
0, ולכן תקלה במסד הופיעה למשתמש כ“✅ הועברו לסל 0 קבצים“. עכשיו יש הבחנה בין ”לא היה מה למחוק“ לבין ”לא ידוע אם נמחק“, והשנייה מוצגת כשגיאה.refactor: שאילתת המחיקה מוגדרת פעם אחת, ב-
file_deletion.pyשבשורש. הוובאפ החזיק מימוש מקביל שכותב ישירות ל-code_snippets, ולא עבר דרךdatabase/repository.py— הוא מריץ על חיבור מונגו משלו. המודול מקבל את ה-collection כפרמטר, ולכן שני הצדדים מריצים את אותו קוד בלי לחלוק חיבור. אותה תבנית שלfile_dates.py.fix: הצ’אנקים הסמנטיים יורדים לפי שם הקובץ ולא לפי מזהה הגרסה. במחיקה המרובה הם נמחקו רק עבור הגרסה האחרונה, ולכן הגרסאות הקודמות נשארו מאונדקסות בזמן שהקובץ יושב בסל.
change: ימי השהות בסל המיחזור אוחדו ל-30 לכל מסלולי המחיקה. עמוד הקובץ הבטיח 30 יום ומחיקה מרובה נתנה 7, כי כל מסלול קרא מקור אחר — קבוע בוובאפ מול
config.RECYCLE_TTL_DAYS. עכשיו יש ערך אחד, והמספר במודאל האישור נגזר ממנו במקום להיות מוקלד בתבנית.
2026-09-07
feat: מתג ”תצוגה מצומצמת“ בהגדרות, לעמוד הקבצים. כשהוא דלוק כרטיס הקובץ מציג את שם הקובץ, באדג« השפה והתגיות בלבד; אייקון השפה, מספר השורות, הגודל, התיאור, שורת ”נוצר/עודכן“ וכפתור ההורדה אינם מרונדרים. צ’קבוקס הבחירה המרובה ושאר הכפתורים נשארים —
data-file-idו-file-checkboxהם חוזה עם הבחירה המרובה, והסתרתם הייתה שוברת פעולות קבוצתיות בשקט. ההעדפה נשמרת בחשבון (ui_prefs.files_compact_view) ותקפה בכל המכשירים, ונכנסת לתוקף בטעינה הבאה של העמוד. המידע מוסתר בכך שהתבנית אינה מרנדרת אותו, ולא ב-CSS: שלושת האזורים נושאיםstyle="display: flex"inline, שגובר על כל מחלקה, ובדרך הזו גם לא נדרש שום CSS חדש ואין מה להתאים לערכות הנושא.fix: הדגל נכנס למפתח הקאש של
/files, אחרת השינוי לא היה נראה. העמוד שומר את ה-HTML המרונדר, והמפתח נבנה מפרמטרי החיפוש בלבד. בלי הדגל, משתמש שמדליק את המתג היה מקבל את ה-HTML של המצב הקודם עד שה-TTL פוקע, ומסיק שהפיצ’ר שבור. הדגל נוסף כתחילית למפתח כך שגם ענף ה-fallback מבדיל בין המצבים. ביטול קאש נשקל ונפסל: אין היום שום קוד שמבטל אתweb:files:user:*, ומפתח שמתאר את התוכן אינו זקוק לפעולה שיכולה להיכשל בשקט.fix: ההעדפה נפתרת פעם אחת לכל בקשה.
/filesבונה את מפתח הקאש לפני הרינדור וה-context processor מזין את התבנית — שתי קריאות נפרדות למסד באותה בקשה. שינוי העדפה שנוחת ביניהן היה גורם ל-HTML של מצב אחד להישמר תחת התגית של המצב השני, וכל מי שמבקש אחר כך את התצוגה ההיא היה מקבל את ההפך עד פקיעת ה-TTL. התוצאה נשמרת עכשיו עלg, ההיקף היחיד שמובטח שהוא בדיוק בקשה אחת.fix: המאזין של המתג מסומן על האלמנט, כי הבלוק
extra_jsשל עמוד ההגדרות מרונדר פעמיים.settings.htmlמצהיר אותו בתוךblock content, ולכן Jinja מרנדר אותו גם שם וגם ב-base.html. נמדד בדפדפן: לחיצה אחת, אירועchangeאחד, ושתי בקשות ל-/api/ui_prefsמשתי שורות שונות באותו עמוד. הכפילות קיימת גם ב-persistentToggle, ב-fontMinusוב-themeSelect, ולא תוקנה כאן — היא נוגעת בכל העמוד ומצריכה שינוי מבני נפרד.fix (חיפוש): כשל במסלול החיפוש המהיר נרשם ללוג במקום להיבלע. שלושה
exceptשקטים ב-_safe_search— הראשון נופל מ-$textל-$regexעלcode, השני משם לפייפליין הישן, והשלישי מחזיר ”לא נמצאו תוצאות“. אף אחד מהם לא כתב שורה אחת, ולכן כשהמסלול המהיר נשבר המערכת עברה בשקט לשאילתה אחרת: סריקה מלאה בלי אינדקס, שנמדדה כשאילתה האיטית ביותר בכל הלוח (3,730ms). ההערה שהייתה שם ניחשה ”למשל אין אינדקס טקסט“ — הניחוש נבדק ונפסל:search_text_idxקיים, ואותה שאילתת$textבדיוק רצה מול הקלאסטר ומחזירה תוצאות. ⚠️ ולא לבלבל בין שני פולבאקים שונים: להגיע לפונקציה הזו זה מסלול תקין שקורה כשמנוע החיפוש החזיר אפס תוצאות; ה-exceptהוא משהו אחר — השאילתה עצמה נזרקה. כשל שלישי מחזיר ”לא נמצאו תוצאות“, כלומר חיפוש שבור נראה בדיוק כמו חיפוש שלא מצא — וזה ההבדל היחיד שחשוב למשתמש.fix (פרופיילר): ``code`` נוסף לרשימת השדות שנשמרים עם ערכים אמיתיים. בלעדיו שאילתת החיפוש נדחתה ב-
unknown_field:code, וניתוח שלה רץ על{"code": {"$regex": "<value>"}}— regex שאינו מתאים כמעט לכלום, ולכן ה-explain דיווח ”מהיר, אפס מסמכים נסרקו“ על השאילתה האיטית ביותר במערכת. הערך שנשמר הוא דפוס החיפוש שהוקלד ולא תוכן הקובץ. ובאותו מעבר תוקנה ההערה מעל הרשימה: היא טענה שהרשימה נגזרה מ“השדות שלcode_snippets“ (נמדדו 32 שדות באוסף מול 19 ברשימה), בעוד הכלל האמיתי הוא השדות שמסננים לפיהם בשאילתות שמשויכות למשתמש — מה שגם מסביר למה שדות ה-worker אינם שייכים לשם: הם נדחים כ-owner_missingלפני שהרשימה נבדקת בכלל.fix (פרופיילר): דגלי ``$project`` נשמרים אמיתיים, ובלעדיהם השאילתה השמורה הייתה פעולה אחרת.
$projectנשאר בשלד המנורמל, ולכן כל שאילתה שנשמרה עם ערכים אמיתיים והכילה אותו יצאה עם{"file_name": "<value>"}. וזו אינה היטלה מוסתרת: מחרוזת שאינה מתחילה ב-$בתוך היטלה נקראת כ**קבוע**, כלומר ”החזר את הקבוע<value>“ במקום ”החזר את השדהfile_name“. נמדד מול MongoDB 8.0.32 — ה-explainמחזיר{"file_name": {"$const": "<value>"}}, ו-queryShapeHashשונה מזה של ההיטלה האמיתית, כלומר המנוע סופר אותן כשתי שאילתות. שלב שמושך אתcodeנותח כשלב שאינו קורא שום שדה. הכשל היה שקט לגמרי כי בניגוד ל-$limitמחרוזת בהיטלה אינה גורמת למונגו לזרוק, ולכן_fix_pipeline_for_explain— שמתקנת רק את מה שזורק — לא נגעה בה. ⚠️ הוולידציה צרה בכוונה: התיעוד של$projectאומר ”Non-zero integers are also treated as true“, כלומר{"file_name": 6865105071}הוא היטלה חוקית לגמרי שמזהה משתמש יושב בה בגלוי, וסריקת הבעלות עוברת על$matchבלבד ולעולם לא תראה אותו. לכן מותרים0/1/בוליאני/נתיב שדה בלבד, וזה חל ברקורסיה גם על היטלה מקוננת.$projectשיש בו ולו ביטוי אחד חוזר כשלד שלם ולא כתערובת — שלב שחציו אמיתי נראה שלם ומתפרש אחרת ממה שרץ.$addFields/$setו-$unsetשל אגרגציה נשארו בחוץ, כי אין להם היום מופע אמיתי לאמת מולו.fix (פרופיילר): שינוי מסנן ה-collection באמצע דפדוף כבר אינו מעלים שורות. ל-
<select>של הסינון לא היה שום מאזין שינוי — עד שנוסף העימוד זה היה פגם חוויה בלבד (בחירה לא עשתה כלום עד לחיצה על ”רענן“), אבל עם קורסור זה הפך לבאג נכונות: בחירת collection ואז ”טען עוד“ שלחה את המסנן החדש עם הקורסור הישן, שנטבע עבור אוכלוסייה אחרת. במיון יורד לפי זמן, שורות של ה-collection החדש שזמנן מאוחר מנקודת הקורסור לא היו מופיעות לעולם. שתי שכבות: ה-select מרענן ומאפס את הקורסור, והקורסור עצמו נושא עכשיו את מסנני האוכלוסייה ונדחה ב-400 כשהם משתנים — בדיוק כמו שהוא כבר נושא את שדה המיון והכיוון. הכלל אחד: קורסור תקף רק לשאילתה שהוא נטבע עבורה.docs (פרופיילר): הובהר ש-``/api/profiler/slow-queries`` מוגש בידי שני שירותים שונים. הוובאפ (סשן אדמין) מממש עימוד, מיון,
totalו-next_cursor; הראוט של הבוט (X-Profiler-Token) מממש רקlimit/collection/min_time/hours, ומתעלם בשקט מכל פרמטר אחר. דוגמת ה-curlבעמוד השתמשה בטוקן בזמן שהטקסט תיאר את הוובאפ — כלומר מי שהעתיק אותה קיבל בדיוק את ההתעלמות השקטה שהעמוד מבטיח שלא תקרה. בנוסף: הפסקה על האינדקסים הבהירה שהם משרתים את הראוט של הבוט ולא את הדשבורד, וההנמקה עלcollection_timestampמפרידה עכשיו בין ”לא מספק את המיון“ לבין ”כן היה מצמצם סריקה“ — שני דברים שהיו מעורבבים.fix (פרופיילר): ``min_time`` חזר לעבוד. הפרמטר מתועד, והראוט של הבוט עדיין משתמש בו — אבל המעבר ל-
get_slow_queries_pageהפיל אותו בשקט: הראוט המשיך לקבל אותו, להתעלם, ולהחזיר 200 עם שורות לא מסוננות. תשובה שנראית תקינה ואינה נכונה גרועה משגיאה, כי אין בה שום סימן שמשהו לא בסדר. הוא מצטרף עכשיו לפילטר החלון ולא לפילטר הדף — כלומר הוא נספר גם ב-total, בדיוק כמוcollection, אחרת ”מוצגות 2 מתוך 4“ היה מדווח על אוכלוסייה שכולה 2. ערך שאינו מספר סופי מוחזר כ-400 ולא מדולג:{"$gte": NaN}אינו מתאים לאף מסמך במונגו, כלומר טבלה ריקה בלי הסבר.fix (פרופיילר): הסיכום מהזיכרון מכבד את החלון. כשאין DB, או כשהחישוב נכשל, הסיכום נבנה מה-buffer שבזיכרון — וזה נעשה עד כה בלי חלון בכלל, כולל
unique_patternsשספר את כל הדפוסים מאז עליית התהליך. כלומר בדיוק הפער שהחלון המשותף בא לסגור, ברגע שבו קשה לשים לב אליו. שני אתרי הפולבאק עברו לספירה מסוננת, ו-unique_patternsנספר כמו ב-$addToSetשבמסלול ה-DB.fix (פרופיילר): ”יש דף נוסף“ נגזר מרשומה שנראתה ולא מניחוש. הקורסור נפלט כשהדף התמלא, ולכן בסך שמתחלק בדיוק בגודל הדף הכפתור ”טען עוד“ נשאר גלוי לצד כותרת שאומרת ”מוצגות 6 מתוך 6“, והלחיצה החזירה אפס שורות. השאילתה מושכת
limit + 1ומחזירהlimit.fix (פרופיילר): בקשות עימוד ומיון מקבילות כבר אינן דורסות זו את זו. לחיצה על כותרת מיון ואז על ”טען עוד“ לפני שהראשונה חזרה — והתגובה שמגיעה אחרונה כתבה את הטבלה ואת הקורסור. וזה חמור במיוחד כאן: הקורסור נטבע למיון מסוים, אז תגובה מאוחרת השאירה קורסור של מיון ישן, והלחיצה הבאה נדחתה ב-400 או החזירה שורות אחרות. נוסף מזהה דור, והכפתור מנוטרל בזמן בקשה — כך אותו קורסור אינו נשלח פעמיים. ההחזרה לפעילות יושבת ב-
finally: בקשה אחרונה שנכשלה הייתה משאירה כפתור מנוטרל לצמיתות בלי יציאה חוץ מרענון.fix: גודל קובץ מוצג בכל מקום לפי אותו כלל, ובסדר קריאה נכון.
105.0 KBהפך ל-105 KB— ספרה אחרי הנקודה נשארת רק כשהיא אומרת משהו. וחשוב מזה:"105 KB"בתוך כרטיס בעברית היה מתהפך על המסך ל-KB 105, כי אלגוריתם ה-bidi אינו מדביק מספר למילה לטינית דרך רווח ניטרלי — הרווח נופל לכיוון הפסקה והמחרוזת נשברת לשני קטעים. נמדד ב-Chromium:KBב-x=325 מול105ב-x=350. הערך נושא עכשיו בידוד דו-כיווני (LRI/PDI) בתוך המחרוזת עצמה ולא ב-dirעל האלמנט, כי לא לכל גודל יש אלמנט משלו —"הקובץ גדול מדי לתצוגה (105 KB)"נבנה כמחרוזת אחת. המשמעות לקוד שקורא את הערך:"105 KB" in resultעדיין נכון, השוואת שוויון מדויקת דורשת את העטיפה.change: שישה מימושים של עיצוב גודל בצד הלקוח אוחדו לאחד (
webapp/static/js/utils/size-format.js, נטען מ-base.html). הם נבדלו זה מזה בתקרת היחידות, ברווח לפני היחידה ובעיגול, ושלושה באגים אמיתיים נבעו מכך:live-previewחילק ב-1024 פעם אחת בלבד והציג קובץ של 3.5 מגה כ-3584KBושל 582 בתים כ-0.6KB;global_searchעצר ב-MB והציג 2 ג’יגה כ-2048 MB;repo-browserעצר ב-GB והחזיר3 undefinedמעל טרה-בייט. כל שלושתם תוקנו בעצם האיחוד.fix: השרת והדפדפן הסכימו על כל גודל חוץ מאחד.
f"{1.25:.1f}"בפייתון מעגל חצי לזוגי ומחזיר"1.2", ו-(1.25).toFixed(1)ב-JS מעגל חצי כלפי מעלה ומחזיר"1.3"— כלומר קובץ של 1280 בתים הוצג1.2 KBמהשרת ו-1.3 KBמהדפדפן. הפער נתפס בבדיקה שמריצה את שני המימושים על אותו וקטור ומשווה, ולא בקריאת הקוד: שני הקבצים נראים זהים, וכל אחד פשוט נשען על ברירת המחדל של שפתו. העיגול נכתב עכשיו במפורש בשתי השפות באותן פעולות על אותם doubles.fix: המודול המשותף נטען בלי
defer, וזו לא השמטה.compare_files.htmlקורא ל-CompareView.initFilesModeב-script אינליין שרץ בזמן ניתוח המסמך, ומשם — כשיש קבצים נבחרים מראש — הוא מגיע ל-formatFileSizeמיד. סקריפט דחוי עדיין לא רץ באותו רגע, ולכן עמוד ההשוואה נפל ב-TypeErrorוכרטיסי התצוגה לא נטענו. בדיקת הרגרסיה שולפת את תג ה-script מ-base.html** עצמו** ולא כותבת אותו מחדש, ולכן החזרתdeferמפילה אותה.
2026-09-06
feat (פרופיילר): הדשבורד קיבל מיון בעמודות, כפתור ”טען עוד“, ומקטע דפוסים חוזרים. וקודם כול — חלון זמן אחד: הכרטיס למעלה ספר 24 שעות, הטבלה משכה 50 שורות בלי חלון בכלל, ו-
get_pattern_statisticsעבדה על שבעה ימים. שלושה מספרים על שלוש אוכלוסיות, על אותו מסך. עכשיוPROFILER_WINDOW_HOURSהוא המקור היחיד, והטבלה אומרת ”מוצגות X מתוך Y“ — כששניהם סופרים את אותו דבר.fix (פרופיילר): הקאש של הסיכום ממופתח לפי החלון. כל עוד החלון היה קשיח ערך יחיד היה נכון; מרגע שהוא פרמטר, בקשה על 24 שעות הייתה מקבלת תוצאה שחושבה עבור שבוע — תשובה של פרמטר אחר, בשקט מוחלט.
_invalidate_summary_cacheמנקה את כל החלונות, כי רשומה חדשה משנה את כולם.feat (פרופיילר): העימוד הוא cursor ולא
skip, לפיdocs/database/cursor-pagination. האוסף מקבל כתיבות בזמן שמסתכלים בו — זו כל מטרתו — ועםskipשאילתה איטית שנרשמת בין שתי לחיצות מזיזה את החלון: שורה אחת מוצגת פעמיים והבאה נדלגת. הקורסור נושא את ערך עמודת המיון ואת_id, מקודד ב-Extended JSON כך שתאריך ומספר עוברים באותו מנגנון, ונושא גם את השדה והכיוון שהוא נטבע עבורם — קורסור שנשלח עם מיון אחר נדחה ב-400 ולא מנוחש.fix (פרופיילר): כפתור ”טען עוד“ נשאר גלוי אחרי הדף האחרון.
.btnמגדירdisplay:inline-flex, וכל הכרזתdisplayשל המחבר מנצחת את[hidden]{display:none}של הדפדפן — כלומר כפתור שסומן מוסתר המשיך להיראות, ולחיצה עליו לא עשתה כלום. אותה טעות בדיוק מתועדת ב-note-boards.css, ואותו כלל הוחל כאן. נתפס בטסט דפדפן.feat (פרופיילר): כרטיס ”דפוסים ייחודיים“ הפך לקישור עוגן אמיתי למקטע הדפוסים, כמו כרטיס ”שאילתות איטיות“.
get_pattern_statisticsהייתה כתובה, מאונדקסת, ובלי אף קורא בכל הריפו; היא קיבלה$facetעם ספירה, כי קודם היא החזירה 50 שורות בלי לדעת כמה יש — ולכן החיתוך שלה לא היה ניתן לדיווח.fix (פרופיילר): שאילתה עם תאריך נשמרת עם הערכים האמיתיים במקום להידחות.
query_rawנוסע עכשיו ב-Extended JSON — ניב JSON שנושא את הטיפוס בתוך ה-JSON עצמו ({"$date": …}) — ולכןdatetime,ObjectId,Decimal128ו-bytesחוזרים מהמסע שווים למקור. קודם הם נדחו, וזו הייתה דחייה נכונה אך יקרה: היא חסמה בדיוק את שאילתת העמוד השני של/files, זו שבגללה הפיצ’ר נבנה. נמדד מול הקלאסטר למה לא לשמור אותם כמחרוזת:created_at < <תאריך>מתאים ל-1,157 מסמכים, ואותו תאריך כמחרוזת ל-0 — כלומרexplainמהיר עם אפס סריקה ויעילות מושלמת, דוח שנראה מצוין ומסקנתו הפוכה. הבקשה שמחזירה את הערכים לניתוח מצהירה על הניב (encoding) ולא מסיקה אותו: פענוח גורף היה חל גם על השלד המנורמל, ושם{"$options": "<value>"}הופך בשקט ל-Regexעם דגלים אקראיים במקום לייצר שגיאה רועשת ממונגו.inf/NaNממשיכים להידחות, כי ההחלטה מה בטוח לשמור נעשית בזמן הכתיבה ולא בזמן ההגשה.fix (פרופיילר): הדוח שמועתק ל-AI הפסיק להצהיר ”אין בדוח נתונים אישיים“ כשיש בו כאלה. המשפט נכתב ללא תנאי, ומרגע שהניתוח רץ על הערכים האמיתיים הוא הצהיר בדיוק את ההפך ממה שהדוח מכיל. עכשיו הוא נגזר מהניב שנשלח בפועל.
fix (פרופיילר): כשל בלתי צפוי בהחלטה על הערכים האמיתיים כבר אינו מפיל את הרשומה. עד כה נתפסה שם רק חריגת הדחייה המתוכננת, וכל חריגה אחרת ברחה עד המאזין ב-DB — כלומר השאילתה האיטית לא נרשמה בכלל, ונשארה רק שורת
Profiler Error.query_rawהוא העשרה, והרשומה היא המוצר: מעכשיו התקלה נרשמת ללוג, מוצגת כ-internal_errorבדשבורד, והרשומה שורדת.feat (פרופיילר): שאילתה איטית שלך נשמרת גם עם הערכים האמיתיים, ולא רק כשלד. עד כה כל ערך הוחלף ב-
<value>, כך שגם האדמין שניתח את השאילתות של עצמו קיבל explain על{"user_id": "<value>"}— אפס תוצאות ומסקנות ריקות.PROFILER_UNREDACTED_USER_IDS(ריק = כבוי) מחריג מזהים מפורשים, ורק שאילתה שמצהירה בוודאות על אחד מהם נשמרת עם ערכים. כל דחייה נרשמת ב-raw_withheld_reasonומוצגת בדשבורד — גם בשורותupdate/deleteשאין להן כפתור ניתוח, כי דווקא שם אין רמז אחר. ”ניתן להרצה חוזרת“ מוגדר בסיבוב JSON ולא ברשימת טיפוסים, ולכןdatetimeו-infנדחים בשמם במקום להישמר כמחרוזת שמריצה שאילתה אחרת.$limit/$skip/כיווני$sortכן נשמרים אמיתיים — הם צורת השאילתה ולא נתוני משתמש, ובלעדיהם ה-explain מתאר מיון חוסם במקום top-k.query_shapeו-query_idלא השתנו, כך שקיבוץ הדפוסים נשאר כשהיה, והערכים חיים ב-DB בלבד תחת ה-TTL — ה-buffer שבזיכרון מקבל עותק בלי הערכים, כי הוא חסום בגודל ולא בגיל.fix (תיעוד): הטענה ש-
X-Profiler-Tokenהוא דרך גישה חלופית לפרופיילר הוסרה._profiler_is_authorizedמסתיימת בבדיקת אדמין בכל מסלול, וכשיש סשן אדמין היא אינה בודקת את הטוקן כלל — כלומר הוא אינו פותח גישה ואינו חוסם אותה. אומת בהרצה. הקוד לא שונה: הרפיית בדיקת הרשאות היא החלטת מוצר, ולא משהו שמתקנים בשקט מתוך תיקון תיעוד.docs (תפעול): התרופה שב-**``performance-sticky-notes``** תוקנה — ההנחיה שהייתה שם לא השפיעה על השירות. העמוד הנחה להגדיר
GUNICORN_CMD_ARGS="--timeout 180 --graceful-timeout 180"וקרא לזה ”תרופה מוכחת“.scripts/start_webapp.shמעביר ל-Gunicorn דגלי--timeoutו---graceful-timeoutמפורשים, ו-Gunicorn מחיל אתGUNICORN_CMD_ARGSלפני דגלי שורת הפקודה ומיד אחר כך דורס אותם — אומת בהרצת Gunicorn 23.0.0 (הגרסה בפרודקשן) ובקודgunicorn/app/base.py. העמוד מתאר עכשיו את המנגנון האמיתי:WEBAPP_GUNICORN_TIMEOUT/GUNICORN_TIMEOUT, ולמה עם worker של gevent ה---timeoutמודד שתיקה של worker ולא אורך בקשה. שמות הסעיפים בעמוד השתנו בהתאם.docs: ``DISABLE_STARTUP_WARMUP`` מכבה שני חימומים ולא אחד — גם את ה-Observability וגם את אינדקסי הפתקים הדביקים — והוא דלוק כברירת מחדל בקוד. התיאור שלו ברפרנס וב-Config Inspector עודכן, כי ”חימום שרץ בעלייה“ היה ההנחה שהעמוד הישן נשען עליה.
fix (חיפוש סמנטי): הצ’אנקים נחתכים לפי תקציב בייטים ולא רק לפי מספר שורות. ל-
gemini-embedding-001תקרת קלט של 2,048 טוקנים, והוא חותך בשקט כל מה שמעבר — בלי שגיאה, בלי אזהרה, עם וקטור שנראה תקין לגמרי. החיתוך הישן ספר 220 שורות בלי לבדוק כמה טקסט יצא: ב-JSON עם מספר בכל שורה זה ~4,300 בייט, ובמארקדאון עברי צפוף עד 24,000. בפרודקשן חצי מהצ’אנקים חצו את הסף, והווקטור שלהם תיאר רק את ההתחלה שלהם — כלומר סוף של קובץ ארוך פשוט לא היה נמצא בחיפוש, בלי שום סימן שמשהו שבור. נוסףCHUNK_MAX_BYTES(ברירת מחדל 2,000) עם חפיפה של 15%, שורה ארוכה מהתקציב (SVG/JS ממוזער) נחתכת לחתיכות, ואין יותר השמטת זנב — איחוד טווחי השורות מכסה את כל הקובץ. קישור ל-Issue רלוונטי: #3332.feat:
chunkerVersionעל כל קובץ מחליף פקודת re-index ידנית. אחרי דיפלוי ה-worker מזהה לבד אילו קבצים נחתכו לפי כלל ישן ומעבד אותם מחדש ברקע. השדה נכתב רק בנתיב שבו הטיפול במסמך באמת הסתיים — אחרת מסמך ריק או גרסה ישנה היו נשלפים בכל סבב ותופסים את חמשת המקומות בבאץ«, ועבודה אמיתית לא הייתה מתוזמנת לעולם.fix: כשל בהטמעת צ’אנק בודד כבר אינו מחזיר את הקובץ לתור לנצח. עד כה כל כשל סימן
needs_embedding=True, והקובץ נשלף שוב כל 300 שניות עם כל הצ’אנקים שלו — נמדד על הקוד הישן. מעכשיו רק כשל זמני (timeout, רשת, 5xx) מחזיר לתור; 400 נרשם כאירוע ולא חוסם. מיצוי ה-retries על 429 מדווח כ-429 ולא כ-0, כך שמכסה שנגמרה נראית שונה מ-timeout: ה-worker עוצר את הבאץ« ומחכה במקום לשרוף עליו עוד קריאות.fix: מחיקת קובץ מורידה גם את הצ’אנקים הסמנטיים שלו. ה-
delete_manyהיחיד עלsnippet_chunksבכל הריפו היה בתוךsave_snippet_chunks, ולכן נמדדו בפרודקשן 355 צ’אנקים יתומים (6.5MB) על פני 118 קבצים מחוקים. הניקוי חובר לכל נתיבי המחיקה — בשכבת ה-DB ובראוטי סל המיחזור שכותבים ישירות לאוסף — ושחזור מסמן את הקובץ לבנייה מחדש.feat: ג’וב יומי מנקה צ’אנקים יתומים. הוא נחוץ כי שני מקורות של יתומים אינם עוברים דרך קוד אפליקציה בכלל: פקיעת סל המיחזור נעשית ב-TTL index בצד השרת, ושמירת גרסה חדשה אינה מכבה את הקודמת — כך שכל גרסה היסטורית נשארה מאונדקסת ומתחרה על מקומות ה-ANN.
fix: אינדקס
(userId, snippetId)עלsnippet_chunksנוצר באתחול הרגיל. הוא הוגדר עד כה רק בסקריפט מיגרציה חד-פעמי, ובקלאסטר שנמדד נותר רק_id_— כלומר כל מחיקת צ’אנקים סרקה את כל הקולקציה.feat: רף ציון על החיפוש הווקטורי (
SEMANTIC_MIN_VECTOR_SCORE). חיפוש וקטורי עונה על ”מה הכי קרוב“ ולא על ”מה מתאים“, והוא תמיד ממלא את ה-limit: חיפוש ”open“ החזיר קובץ שהמילה אינה מופיעה בו אפילו פעם אחת. הרף חל על הציון הגולמי של$vectorSearch(טווח 0..1) ולא על ציון ה-RRF, שהוא בסקאלה אחרת לגמרי — סף שנבחר בטעות על הסקאלה הלא נכונה כבר סינן פעם אחת את כל התוצאות. ברירת המחדל 0 (כבוי) עד לכיול על נתונים אמיתיים.fix:
numCandidatesבחיפוש הווקטורי גדל עם הקורפוס.limit * 20הוא מספר קבוע — 200 מועמדים ל-limit=10, שהם 5.7% מ-3,483 צ’אנקים ורק 1.2% מ-17,000. אותו מספר בדיוק פוגע ברקול ככל שהקורפוס גדל, וזה היה נראה כאילו הצ’אנקים הקטנים הרעו את החיפוש.change: הווקטורים נשמרים כ-BSON BinData subtype 9 (float32) במקום כמערך doubles. נמדד: 9,887 בייט למסמך מול 3,087 — חיסכון של 3.2×. אומת מול הקלאסטר לפני המימוש: מסמך BinData נכנס ל-
vector_indexהקיים, החזיר ציון 1.0 מדויק מול הווקטור של עצמו, ובאותה שאילתה חזרו גם מסמכי מערך — כלומר שתי הצורות חיות יחד תחת אותוpathבזמן ה-re-index, ולא נדרש אינדקס חדש.feat: צ’אנקים שהם dump של מספרים (קובץ ה-export של טבלת האמבדינגים עצמה חולק ל-88 צ’אנקים ונשלח להטמעה) מדולגים. הסף כויל על כל 3,483 הצ’אנקים בפרודקשן: התוכן ה“מספרי“ ביותר שאינו dump עוצר ב-0.556, ומעל 0.6 יושבים רק ה-dump ונתיבי SVG.
מגבלה ידועה: במהלך ה-re-index (מספר שעות ברקע) החיפוש ממשיך לעבוד, אבל קובץ שטרם עובד נמצא באיכות של לפני התיקון.
save_snippet_chunksמוחק את הצ’אנקים הישנים של קובץ רק אחרי שכל החדשים שלו כבר בזיכרון, ולכן בכל רגע נתון לכל קובץ יש או צ’אנקים ישנים או חדשים — אף פעם לא שניהם.fix: הודעת ולידציה עם
\r\nאיבדה את הניקוי של שורת התיעוד, וסוגריים ריקות בתוך הערך שנדחה (input_value=[]) נמחקו יחד עם רעש ה-type=. שניהם היו מחיקה של מידע מאבחן, וזה בדיוק מה שהסיכום אמור לא לעשות.feat: הודעת השגיאה ב-
/admin/mcpמוצגת נקייה — שורה אחת לכל שדה שנדחה (lines.1 · Input should be a valid integer [input_value='9', input_type=str]) במקום ארבע שורות שרובן רעש: כותרת שנושאת את שם הכלי שכבר יש לו עמודה, הזחות, וקישור לתיעוד של Pydantic. הפירסור נכשל בטוח: מה שאינו נראה כמו הודעת Pydantic מוחזר כפי שהוא. המבנה נמדד עלpydantic 2.12.3ולא נלקח מהתיעוד — הרינדור הוא ב-Rust ואין קוד פייתון לקרוא.fix: ההודעה והכוונה מוצגות ב-LTR מבודד בתוך העמוד ה-RTL. בלי זה האלגוריתם הדו-כיווני מזיז סוגריים וסימני שוויון לקצה הלא נכון, ו-
[input_value='9']נראה שבור. הכיווניות יושבת על התא, ולכן סדר העמודות נשאר RTL.feat: לחיצה על הכוונה המקוצצת פותחת חלון עם הטקסט המלא. התא הפך ל-
<button>אמיתי במקוםspanעםtabindex, ולכן Enter ו-Space עובדים בלי קוד משלנו וקורא מסך מכריז ”כפתור“. החלון עצמו הוא<dialog>נייטיב שנפתח ב-showModal(): נעילת הפוקוס בתוכו, סגירה ב-Escapeוהחזרת הפוקוס לכפתור שפתח מגיעות מהדפדפן ולא מקוד שלנו. הטקסט נכתב ב-textContent— מסלול שני לאותו טקסט שסוכן חיצוני כתב, ולכן הוא נבדק בנפרד בדפדפן.feat: באנר ”X שגיאות חדשות מאז הביקור האחרון“ מעל טבלת הכשלים. הטבלה מציגה חלון נע של 30 יום, ולכן המספר הכולל יורד מעצמו כששגיאות ישנות נושרות ואינו יכול לשמש איתות; הבאנר משווה מול חותמת הזמן של החדשה ביותר שכבר נראתה. בביקור ראשון הוא שותק — אז הכול ”חדש“.
localStorageחסום פשוט לא מציג אותו. כשהטבלה נחתכה בתקרה וכל השורות שהוצגו חדשות, הניסוח הוא ”לפחות X“: הבאנר סופר שורות ב-DOM, ואי אפשר לספור מה שהשאילתה לא החזירה.
2026-09-02
feat: מסך
/admin/mcpמציג את הודעות השגיאה עצמן, מתחת לטבלת הכלים באותו טאב. הטבלה אמרה עד כה ”20% שגיאות“ ולא אמרה למה — כמו דוח מסירה בלי פתק המסירה. אנדפוינט חדש ב-PostHog (ck_mcp_tool_failures) מחזיר את הקריאות שנכשלו עם הכלי, הזמן, הלקוח וההודעה. קריאה שנכשלה לפני שההודעות נשמרו מוצגת עם—בעמודת ההודעה: הצינזור קרה בזמן השליחה ואינו הפיך, ותא ריק אינו כשל.feat: עמודת
file_readsבטאב עלות הניווט פוצלה לשתיים — אאוטליין מול קריאת תוכן. הן היו אותו כלי ולכן אותה עמודה, בזמן שהן הפוכות במשמעות: אאוטליין הוא המפה, קריאת תוכן היא הניווט שהמפה באה להחליף, ועמודה שסופרת את שתיהן יחד לא יכולה להראות שהאחת החליפה את השנייה. ההבחנה מגיעה מ-ck_read_mode— תווית מתוך קבוצה סגורה שהשרת מחשב מנוכחות הפרמטרים, ולא מהפרמטרים עצמם, שנשארים חסומים בשער הפרטיות. גרסה חדשה של האנדפוינט (ck_mcp_navigation_cost_v2); הקודמת נשמרת ב-PostHog כבסיס להשוואה.feat: טור בטאב עלות הניווט שמציג את המשפט שהסוכן כתב על מה שהוא מנסה להשיג, במקום מזהה סשן אטום. הטקסט עצמו, לא סיכום — מקוצץ בתצוגה עם המלא ב-
title, ומרונדר כטקסט בלבד (בלי|safe, בליinnerHTML).feat: קישורים יוצאים מהעמוד לתצוגות של PostHog שאי אפשר להביא לכאן — אשכולות כוונות וסיכומי כוונה לסשן. הם כלי API ולא שאילתות, והעמוד מדבר עם PostHog רק דרך אנדפוינטים שמורים.
change (משפיע על לקוחות MCP): לכידת הכוונה נדלקה, ולכן לכל כלי נוסף פרמטר
context. הוא מוצהר בסכימה של כל הכלים ומופיע ברשימת ה-requiredשלהם. קריאה שאינה מעבירה אותו אינה נדחית — נמדד: הכלי רץ בדיוק כמו קודם, והפרמטר אינו מגיע לגוף הכלי כלל. מה שהשתנה הוא מה שרשימת הכלים מצהירה. בלי זה$mcp_intentאינו נוצר על קריאת כלי, וטור הכוונה היה נשאר ריק לנצח. התיאור של הפרמטר הוא שלנו ולא ברירת המחדל הארוכה של ה-SDK, והוא מבקש מהסוכן במפורש לא לכתוב שם תוכן קבצים או סודות.change (פרטיות):
$mcp_error_messageנשלח עכשיו על כל סוג שגיאה, ולא רק עלValidationError; ו-$mcp_intentנשלח על כל אירוע, ולא רק על$mcp_missing_capability. שניהם עדיין עוברים רק כשהערך מחרוזת.$mcp_parametersו-$mcp_responseנשארים חסומים בכל מסלול, ו-$exception_list[*].valueנשאר מושחר בכל אירוע. המחיר מתועד ולא מוסתר: הודעת חריגה יכולה לשאת קטע מהקלט שנדחה (Pydantic שומר ראש וזנב שלinput_value), ומאז ההרחבה גם הודעה של ספרייה — נתיב, כותרת שחיפשנו. ראו את האזהרה ב-מדידת שימוש (PostHog MCP Analytics).fix (אבטחה):
POSTHOG_HOSTשנושא פרטי הזדהות נדחה.urlsplitמחזירusername=''— מחרוזת ריקה, שהיא falsy — עבורhttps://:secret@us.posthog.com, ולכן בדיקה על הערך בלבד אישרה את הכתובת. הסיסמה נשארה בערך הגולמי שממנו נבנים גם נתיב ה-API וגם הקישורים היוצאים שמרונדרים לעמוד: סוד ב-hrefשנפתח בדפדפן, כלומר בהיסטוריה וב-Referer. הבדיקה עברה ל-is not Noneעלusernameועלpassword. אותה משפחה שלPOSTHOG_PROJECT_ID, בפעם השלישית באותו קובץ.change (פרטיות):
$mcp_error_messageו-$mcp_intentעוברים סינון סודות לפני השליחה — אותה רשימת דפוסים שמסננת את הפריימר, שעברה ל-mcp_server/redaction.pyכדי ששני הצרכנים יייבאו אותה ולא יחזיקו עותק כל אחד. נמדד מה היא תופסת: כל צורת סוד מוכרת בכל מקום במחרוזת, כולל מפתח AWS בזנב שלinput_valueשל Pydantic. ונמדד גם מה לא: הכלל לפי שם (API_KEY=…) מעוגן לתחילת שורה ולכן אינו יורה על הודעה חד-שורתית. המסנן מצמצם חשיפה ואינו הופך את השדות לבטוחים.change (פרטיות):
$mcp_intent_sourceנאכף כתווית סגורה (context_parameter/inferred) במקום לעבור כמחרוזת. הוא ישב עד כה ברשימת המטא-דאטה, שאינה בודקת טיפוס כלל, ולכן מבנה שהיה מגיע תחתיו במהדורה עתידית של ה-SDK היה עובר שלם.fix: פאנל הקריאות שנכשלו והקישורים היוצאים מרונדרים גם כשאנדפוינט אחר באותו טאב נכשל או חוזר ריק. הם ישבו בתוך התנאי של שכנם, ולכן כשל בשליפת בריאות הכלים החביא פירוט שהגיע בהצלחה.
fix: הכוונה המקוצצת נגישה גם בלי עכבר —
tabindexו-aria-labelלצד ה-title, שנחשף רק ב-hover.
2026-09-01
fix: שחזור ZIP לריפו חוזר להעלות את הקבצים לשורש הריפו. ה-ZIP של גיבוי ריפו הוא ה-zipball של GitHub — שכל תוכנו תחת תיקייה בשם
owner-repo-sha— עםmetadata.jsonבשורש. המסלול החזיק חישוב שורש משלו שסופר תיקיות בלבד, ולכן הקובץ בשורש לא הפריע; המעבר ל-detect_zip_common_root, שבה קובץ בשורש מבטל את הזיהוי, גרם לכל הריפו להיפרס בתוך תיקייה. החישוב המקורי הוחזר, ונוספה בדיקה מקצה לקצה למסלול — שהיעדרה הוא שאיפשר לרגרסיה לעבור בשקט. המסך שמבקש את הקובץ מתריע עכשיו שמבנה עץ התיקיות נשמר עבור קובץ גיבוי, וש-ZIP מסוג אחר — עם קובץ בשורש לצד תיקייה אחת — עלול להיפרס שטוח.
2026-08-31
fix: תאריך היצירה של קובץ נשמר בין גרסאות. כל עריכה יוצרת מסמך חדש ב-
code_snippets, ועד עכשיו כל מסמך כזה נולד עםcreated_atטרי — כך ש“נוצר“ בוובאפ ובמסך המידע של הבוט הציג בפועל את זמן העריכה האחרונה. הירושה נוספה בשכבת ה-DB ובכל ראוט שכותב ישירות לאוסף: עריכה, שחזור גרסה, העלאה על קובץ קיים, שמירת מסמך משותף וייצוא סיפור תקלה. אותו תיקון הוחל עלlarge_files.fix: קובץ שמעולם לא נערך מקבל
created_atו-updated_atזהים בדיוק, במקום שני ערכים שנוצרו בקריאות נפרדות ל-datetime.now().fix: שורת ”עודכן“ מוסתרת בוובאפ בקובץ שמעולם לא נערך. ההחלטה נקבעת בשרת מהתאריכים הגולמיים ולא מהשוואת המחרוזות המוצגות — הפורמט הוא ברזולוציית דקה, ולכן עריכה שקרתה באותה דקה שבה הקובץ נוצר הייתה נעלמת מהמסך. גם קובץ בלי
updated_atמפסיק להציג שורת ”עודכן“ ריקה. בעמוד הקובץ ההסתרה נעשית עםhidden, ונוסף כלל CSS שגובר על.meta-item{display:flex}מ-global_search.css— בלעדיו התכונה לא הסתירה כלום.refactor: שני כללי תאריכי הקובץ עברו ל-
file_dates.pyבשורש הריפו — מודול טהור שמייבא רקdatetimeו-typing. כך שכבת ה-DB והוובאפ מייבאים את אותו כלל ישירות, בלי עותק fallback שצריך לתחזק במקביל. אותה תבנית שלuser_rolesושלsticky_notes_scope.fix: שחזור מגיבוי אישי משמר את התאריכים שנשמרו בגיבוי. הייצוא כבר שמר
created_atו-updated_atכמחרוזות ISO, והשחזור לא קרא אותם — כך שכל קובץ, סימנייה או פתק שנעדר מהמסד קיבל את תאריך היום. חל על קבצים, קבצים גדולים, סימניות ופתקיות. נוספה_str_to_dt, ההפוכה של_dt_to_strשהייתה חסרה; ערך חסר או פגום מתנהג בדיוק כמו לפני התיקון, ולכן גיבויים ישנים ממשיכים לעבוד. אין שינוי בפורמט ואין bump ל-BACKUP_FORMAT_VERSION.fix:
updated_atשל סימניות נכלל עכשיו בגיבוי, בשני מסלולי הייצוא. השדה קיים במסד אך מעולם לא נכתב לקובץ הגיבוי, ולכן השחזור לא יכול היה לקרוא אותו וכל סימנייה קיבלה את תאריך העדכון של רגע השחזור — מה שמשפיע על סדר הצגת הסימניות ועל הסטטיסטיקה ”נוצרו השבוע“.feat: לוחות הפתקים נכללים בגיבוי האישי. עד כה הייצוא קרא מ-
note_boardsרק כדי להזליג את שם הלוח על כל פתק, ומסמכי הלוח עצמם לא נשמרו — כך שכל הלוחות נעלמו בשחזור לסביבה נקייה וכל הפתקים נחתו על לוח ברירת המחדל, בלי שגיאה אחת. הלוחות משוחזרים לפני הפתקיות, כך שהשיוך לפי שם מוצא לוח אמיתי. לוח קיים באותו שם אינו מוכפל, תקרת הלוחות נאכפת גם בשחזור, ולוח ברירת המחדל של חשבון היעד שומר על שמו — אך הנעיצה שלו כן משוחזרת, כי היא העדפת תצוגה שאינה דורשת לא את השם ולא את המזהה.fix: פעולות מטא-דאטה אינן מדווחות עוד על ”עריכה“. נעיצה לדשבורד, סימון מועדף, מחיקה רכה ושחזור מהסל חתמו
updated_atעל כל גרסאות הקובץ, למרות שאף בית של תוכן לא השתנה. התוצאה הייתה שקובץ שמעולם לא נערך הציג ”עודכן“, והוא קפץ לראש המיון ”עודכן לאחרונה“ — ובהיסטוריית הפעולות בדשבורד כל גרסאותיו הופיעו בבת אחת ברגע הנעיצה. לכל פעולה כזו יש שדה משלה (pinned_at,favorited_at,deleted_at), ו-updated_atנשאר על העריכה האחרונה בפועל. הכלל חל גם על ראוטי הוובאפ שכותבים ישירות ל-code_snippetsולא דרך שכבת ה-DB — מועדפים, מועדפים מרובים, העברה לסל, מחיקה מרובה, שחזור מהסל ותיוג מרובה. בעדכון המהיר, שמטפל גם בתיאור וגם בתגיות, החתימה נעשית רק כשהתיאור השתנה. בתיוג מרובה המונה שחוזר ללקוח עבר ל-matched_count, כדי שהמספר המוצג יישאר מה שהיה —$setעלupdated_atהפך עד כה כל התאמה למודיפיקציה, ובלעדיו תיוג חוזר באותה תגית היה מדווח אפס.fix: היסטוריית הפעולות בדשבורד מציגה קובץ ולא מסמך גרסה, גם ב“טען עוד“. כל עריכה יוצרת מסמך חדש ב-
code_snippets, והשאילתה לא קיבצה לפי שם קובץ — כך שקובץ עם חמש גרסאות הופיע בחמש שורות נפרדות, וגם המונה של ”טען עוד“ ספר מסמכים ולא קבצים. השאילתה, הקיבוץ ובניית האירוע מוגדרים עכשיו פעם אחת ומשמשים גם את העמוד וגם את ה-endpoint — עד כה הם היו שני עותקים, והקיבוץ תוקן בצד אחד בלבד. המיון קיבל נפילה ל-created_at(קובץ בליupdated_atהיה שוקע לתחתית, כי מונגו משווה שדה חסר כ-nullשנמוך מ-Date) ושובר שוויון על_id($sortאינו יציב, ובדפדוף זה מתורגם לשורות כפולות בדף אחד וחסרות בבא). וכשל בספירת הקבצים כבר אינו מסתיר את כפתור ”טען עוד“: נשלפת שורה אחת מעבר לעמוד, וקיומה מכריע אם יש המשך. כך גם מספר קבצים שהוא כפולה מדויקת של גודל העמוד אינו מציע עוד לחיצה שחוזרת ריקה.fix: ה-ETag של עמוד הקובץ הוא הוולידטור היחיד שלו. מצב המועדף והנעיצה מרונדרים לתוך ה-HTML אך נעדרו מהוולידטור, ונכונות הקאש ניצלה רק בגלל שפעולות המטא-דאטה הזיזו את
updated_at— תופעת לוואי שנעלמה כשהפסקנו לחתום אותו. שני השדות נכנסו ל-ETag, ובמקבילIf-Modified-Sinceלבדו כבר אינו מייצר 304 בעמוד הזה: אין שדה שמתעד מתי מצב המועדף השתנה —favorited_atאומר מתי סומן, ובהסרת הסימון הוא מתאפס ו-Last-Modifiedנסוג אחורה, כך שלקוח שהחזיק את הערך המאוחר היה מקבל 304 עם כוכב תקוע.Last-Modifiedממשיך להישלח כמידע.fix: מספר גרסה ייחודי לכל קובץ, גם כשגרסאות יושבות בסל המיחזור. מחיקה רכה אינה מוחקת — המסמכים נשארים ויכולים לחזור בשחזור — אבל חישוב הגרסה הבאה התעלם מהם, ולכן שמירה בזמן שהקובץ בסל קיבלה מספר שכבר תפוס. אחרי שחזור מהסל הבחירה לפי הגרסה הגבוהה ביותר נתנה לתוכן הישן לגבור, והתוכן שנשמר אחריו נקבר בלי סימן. המספור נשאל עכשיו על כל המסמכים; ירושת המטא-דאטה נשארה על הגרסה הפעילה, כדי שקובץ חדש בשם ממוחזר לא יירש תאריך של קובץ שנזרק.
change:
codekeeper_save_fileב-MCP חוסם שמירה על שם קובץ שכבר קיים, ומפנה ל-codekeeper_edit_fileאו ל-codekeeper_append_file. שמירה כזו יצרה גרסה חדשה, והתוכן הקודם נעלם משני המקומות שבהם מחפשים אותו — החיפוש מקבץ לגרסה האחרונה לכל שם, ועמוד הקובץ מציג אותה בלבד. קובץ ותיק שחלק שם עם מה שנשמר הפך לבלתי נגיש בלי שהכותב ידע שדרס משהו. וכשל בבדיקת הקיום אינו נחשב ”אין קובץ“ אלא מוחזר בקוד נפרד (existence_check_unavailable) שעוצר את השמירה — דווקא שם דריסה שקטה אפשרית, ולכן תשובה מנוחשת גרועה מסירוב.מגבלה ידועה: אין מיגרציה. קובץ שכבר נערך לפני התיקון נושא בגרסה האחרונה תאריך שגוי, והתיקון מקפיא אותו במקום לדחוף אותו קדימה שוב. התאריך המקורי נשאר על מסמך גרסה 1 וניתן לראותו במסך היסטוריית הגרסאות.
2026-08-25
docs:
DEV_WEB_PUSH.mdהוסר, ותוכנו מוזג ל-עובדי Push שהוא מעכשיו העמוד היחיד ל-Web Push. שני העמודים תיארו את אותה מערכת באותה רמה, והישן כבר היה מיושן בשלוש נקודות. הקישור הישן/DEV_WEB_PUSH.htmlאינו קיים עוד — סימניות ישנות יקבלו 404, והיעד הוא/deployment/workers.html.docs: תוקן משפט שגוי שהיה ב-
DEV_WEB_PUSH.md: ”מפתח ה-VAPID הפרטי אינו נדרש ע"י Flask“. הוא נדרש במסלול המקומי (pywebpush), ונכון רק כשהמשלוח מואצל ל-Worker.docs: תועד מי באמת שולח את תזכורות הפתקים — thread דמון
push-senderבתוך תהליך ה-WebApp, עם נעילתflockשמונעת שני שולחים. קודם זה הופיע כ“תהליך רקע בשרת“ בלבד.docs: אוחדו כפילויות בין פתקים דביקים (Sticky Notes) ל-עקרונות להוספת פיצ’ר לסטיקי-נוטס — הנימוקים חיים בעמוד המפתחים, ועמוד המשתמש מקצר לתוצאה ומקשר.
2026-08-21
feat: לוחות פתקים — פתקים שאינם צמודים לקובץ. יצירה, שינוי שם, מחיקה עם העברת הפתקים ללוח ברירת המחדל, ושני מצבי מיקום (מוצמד ללוח / צמוד למסך). כניסה מכפתור קיצורי הדרך בסרגל העליון.
feat: צ’קבוקסים לחיצים בתוך פתק. הלחיצה נשמרת למסד, והשרת מאמת בקריאה חוזרת שהתו אכן השתנה — בכשל התצוגה חוזרת אחורה עם חיווי.
feat: קישור קבוע
/note/<note_id>שמפנה לקובץ או ללוח לפי סוג הפתק. התראות תזכורת והפעמון עברו להשתמש בו במקום לבנות/md/<file_id>, מה שגרם לפתקי לוח ליפול לשורש האתר.fix: תקרות הפתקים (200 ללוח, 1000 למשתמש) נאכפות בשכבת ה-API. הן היו מתועדות ונאכפו רק חלקית במסלול ה-MCP. אדמין פטור.
fix: פתקי לוח שורדים גיבוי ושחזור. השחזור דילג בשקט על כל פתק בלי
file_name.fix:
_notes_scope_filterב-MCP החזיר את כל הפתקים של המשתמש כשאין scope, במקום את הפתקים של הקובץ המבוקש.fix: נוסף אינדקס
(user_id, scope_id)שחסר ב-webapp, למרות שזה ענף מרכזי בשאילתת הפתקים.refactor:
is_admin/is_premiumאוחדו ל-user_roles. הלוגיקה הייתה משוכפלת מילה במילה בשני קבצים.
2026-01-29
feat: תיוג פריטים ב“אוספים שלי“ עם תגיות אימוג’י, עורך תגיות, סינון/מיון ובחירה מרובה.
feat: נוספו endpoints ל־metadata ול־עדכון תגיות + אירועי observability.
docs: עודכן
user/my_collectionsעם תיאור תיוג, סינון וייצוא/ייבוא.
2025-12-11
docs: עמוד חדש
webapp/theming_and_cssכולל טבלת טוקנים, שכבות, בדיקות, דוגמאות קוד ותרשימי SVG עבור Classic ו‑High Contrast.docs: עודכן
index.rstעם קטגוריה ”Frontend > Theming“ וקישורים מ-development.rstו-README.docs: נוספו קישורים הדדיים ל‑
css_refactor_plan.md(FEATURE_SUGGESTIONS + WebApp) ול‑webapp_theme_palettes.md.
2025-11-11
docs: עמוד חדש ”web 🌐 ממשקי משתמשים“ עם זרימות בוט, שדות חובה, חיווי מצב וממשק אדמין.
docs: עדכון ”webapp/snippet-library“ לגרסה המשודרגת – איחוד Curated+DB, הגבלות דפדוף, Deferred Highlight, ושיפורי אדמין.
docs: מדריך קצר לסוכני AI להפעלה דרך הבוט וה‑WebApp בשני הפיצ’רים.
2025-11-06
docs: WebApp Onboarding – דף חדש עם זרימה, קישורים ו‑JS לדגל has_seen_welcome_modal.
docs: WebApp API Reference – נוספו POST /api/welcome/ack ו‑POST /api/shared/save כולל קלט/פלט ושגיאות.
docs: Observability – דף ”Tracing Hotspots“ עם תרשימי Mermaid, טבלת כיסוי ודוגמאות @traced.
docs: Log‑based Alerts – פרק ”טקסונומיית שגיאות וחתימות“, ”תצוגה ב‑ChatOps“ ו‑classify_error().
docs: Resilience – דף חדש למדיניות Retry + Circuit Breaker עם טבלת ENV ודוגמת שימוש.
docs: Architecture – הופרדה אחריות למסמכים (DocumentHandler) + HOWTO חדש handlers/document-flow.
docs: Runbooks – צ’קליסט Incident חדש.
קישורים ל‑Issues רלוונטיים: #1198, #1239.
2025-11-05
docs: הרחבת דף הבית – נוספו Bookmarks, Collections, Sticky Notes, Favorites, Reminders ל“סקירה כללית“ ו“תכונות עיקריות“, כולל הבהרה ש‑WebApp בלבד.
docs: הרחבת webapp/overview – הוספת פירוט (CodeMirror, Markdown מתקדם, Bulk Actions, Status) וקישורי :doc: לעמודים רלוונטיים; תיקון קישורים מוחלטים למניעת אזהרות RTD.
docs: עדכון examples – שימוש ב‑create_application ו‑app.run_polling() במקום API ישן.
docs: תיקון installation – קישור ריפו ל‑https://github.com/amirbiron/CodeBot.git.
docs: quickstart – קישור ל‑webapp/overview בסעיף ”מה הלאה?“.
2025-10-30
fix: תמיכה בגרירה לסידור פריטים באוספים במגע (נייד/טאבלט). גרירה מתבצעת מהידית
⋮⋮והסדר נשמר אוטומטית. בדסקטופ אין שינוי.
2025-10-29
נוסף סשן
aiohttpמשותף דרךhttp_async.get_session+ כיבוי אוטומטי ב‑atexit.תיעוד עודכן: Configuration (Async HTTP), Architecture (תשתית HTTP), Troubleshooting (לולאות asyncio), ו‑API Reference (
http_async).
2025-10-15
נוספו עמודים: Caching, Indexing, Cursor Pagination, Static Checklist.
הורחב WebApp API Reference עבור /files.
נוספה טבלת ENV מרוכזת.
הורחב Troubleshooting עם Gotchas.
נוסף מדריך כותבי תיעוד.
הוגדרה מדיניות עוגנים יציבים.