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, שדורש את הגוף הנוכחי המדויק, חייבה משיכה של הלוח כולו. הכלי החדש מחזיר את הגוף הנוכחי בדיוק כפי שהוא מאוחסן — פתק ישן עם &quot; חוזר עם הישות, כמו ב-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: טסט שמקבע שגם במסלול ה-Markdown suggestions הוא רשימת מחרוזות ולא ה-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.

  • נוסף מדריך כותבי תיעוד.

  • הוגדרה מדיניות עוגנים יציבים.