:summary: איך שרת ה-MCP קורא ומגיש תוכן: מה כלי הריפו עונים כשקריאה לא מגיעה לקובץ, איזה קובץ כלי התיעוד קורא ואיך הוא מוצא כותרת, הפערים של מפת ה-Markdown, מפת הסימבולים, טווח שורות, חיפוש וספירת מופעים, קריאה לפי סעיף בקובץ שמור, ספירת שורות בעץ הריפו והקריאה הקבוצתית. שרת ה-MCP — קריאה, פרסור וחיפוש בתוכן ===================================== העמוד הזה הוא חלק מתיעוד שרת ה-MCP. התשתית, טבלת הכלים וטבלת הקבועים — ב-:doc:`mcp-server`. .. _mcp-repo-read-misses: ענף שלא במראה, מול קובץ שלא קיים -------------------------------------- **מה המראה מחזיקה, ומתי היא נמשכת מ-GitHub** — ראו :ref:`mcp-repo-autosync`. **כלי שמקבל ``ref`` לעולם אינו מושך.** בקשה לענף שאינו במראה עונה שהוא אינו שם, ולא מפעילה ``git fetch`` — אחרת כל קורא היה יכול להפעיל משיכות מ-GitHub. אילו ענפים נכנסים למראה היא מדיניות של ה-autosync בלבד. **מה הכלים עונים כשהקריאה לא מגיעה לקובץ.** ההבחנה נולדת במקום אחד, ``_validate_ref_with_git`` ב-``services/git_mirror_service.py``, ועוברת דרך ``resolve_commit`` ו-``get_file_at_commit`` לכל כלי הריפו. ``codekeeper_list_repo_tree`` פותר את הענף באותה בדיקה לפני ``ls-tree``, וקורא את העץ מה-commit שחזר. .. list-table:: :header-rows: 1 :widths: 22 46 32 * - ``error`` - מתי, ומה בתשובה - מה הקורא עושה * - ``ref_too_long`` - ``ref`` ארוך מ-``MAX_REF_CHARS`` תווים — 4,096, אותו מספר ומאותו נימוק כמו ``MAX_PATH_CHARS``, שניהם ב-``mcp_server/docs_handlers.py``. נבדק ב-``RepoBackend`` לפני שהמראה נשאלת, ולכן גובר על ריפו או נתיב פגומים ועל ריפו בלי מראה; מה שנבדק לפניו עונה כמו תמיד — ``path_denied``, ``outline`` יחד עם ``lines``, טווח ``lines`` פגום, ובכלי התיעוד גם מדיניות הנתיבים. נושא ``max_chars`` ו-``actual_chars`` **ולא את ה-``ref``**: עד היום ``invalid_ref`` החזיר אותו כמו שהוא, ו-``ref`` ענק הוליד סירוב גדול מהתקציב, שהרשת החליפה בסירוב כללי בלי הסיבה. ב-``codekeeper_docs_get_section`` גם ``repo`` ו-``path`` - לתקן את השם; ``ref`` אמיתי קצר בהרבה מהתקרה * - ``invalid_ref`` - השם לא עבר את הבדיקה הבסיסית (``BASIC_REF_PATTERN``), ולכן git לא נשאל עליו בכלל. נושא ``repo``, ``ref`` ו-``message``, ובכלים שמקבלים נתיב גם ``path`` - לתקן את השם * - ``ref_not_mirrored`` - השם תקין, והמראה של המארח הזה לא מכירה אותו — ענף או SHA. **"לא במראה", ולא "לא קיים"**: המראה יודעת רק מה היה ב-GitHub במשיכה האחרונה שלה. נושא ``repo``, ``ref`` ו-``message``, ובכלים שמקבלים נתיב גם ``path`` - לא לנסות שמות קבצים אחרים. ענף שנדחף לאחרונה יגיע עם השינוי הבא בענף הראשי * - ``not_found`` - הענף נפתר, והקובץ אינו ב-commit שלו. נושא ``ref`` — מה שנקרא בפועל: מה שהתבקש, או הענף הראשי — ו-``resolved_commit``; ב-``codekeeper_docs_get_section`` גם ``repo`` ו-``path`` - לבדוק את הנתיב מול ה-commit שב-``resolved_commit`` * - ``repo_not_mirrored`` - למארח אין עותק של הריפו כלל - עניין של המפעיל * - ``read_failed`` - הריפו נמצא והקריאה נכשלה: מראה שבורה, timeout, או כשל אחר של git - לנסות שוב מאוחר יותר; אם זה חוזר — המפעיל ``sync_in_progress`` גובר על כל אלה חוץ מ-``invalid_ref``: קריאה שנכשלה בזמן שסנכרון רץ אולי ראתה את המראה באמצע משיכה, ו"נסה שוב" נכון יותר מ"לא במראה". שם פגום אינו תלוי בסנכרון, ולכן הוא נענה מיד. ``codekeeper_search_repo`` ופריטי ``codekeeper_read_batch`` אינם מקבלים ``ref`` — הם קוראים תמיד את הענף הראשי — ולכן ``ref_not_mirrored`` מגיע מהם רק כשהענף הראשי עצמו חסר במראה, למשל כשהמטא-דאטה במונגו מצביעה על ענף שאין בה. בבאץ' ``ref_not_mirrored`` הוא תשובה רגילה ואינו נרשם בלוג, כמו ``invalid_ref``. **פגום, חסר ושבור — לפי קוד היציאה של git.** ``git rev-parse --verify --quiet`` יוצא ב-1 כשהשם אינו נפתר לרוויזיה אחת (``die_no_single_rev`` ב-``builtin/rev-parse.c``), וכל תקלה אחרת עוברת דרך ``die()`` ויוצאת ב-128 (``usage.c``) — שניהם ב-git v2.43.0. יציאה 1 היא ``ref_not_mirrored``, וכל קוד אחר הוא ``git_error``, שנרשם כ-WARNING עם ה-stderr ומגיע לכלים כ-``read_failed``. כך מראה שנמחקה, או תיקייה שאינה ריפו, אינה נקראת "ענף שלא נמשך". יציאה 1 מכסה גם שני מצבים נוספים, ונמדד: ref שמצביע על אובייקט שאינו commit (תגית של עץ), ו-ref שהאובייקט שלו חסר במאגר. בשניהם ה-commit אכן אינו במראה. **הוובאפ צורך את אותה בדיקה.** ``/repo/api/history`` ו-``/repo/api/file-at-commit/`` מחזירים 404 על ``ref_not_mirrored``, 400 על ``invalid_ref`` ו-500 על ``git_error``. הממשק עצמו אינו מציע בחירת ענף, ולכן משתמש לא מגיע לזה משם. ``tests/test_repo_browser_ref_errors.py`` קורא את רשימת הקודים מקוד המקור של הבדיקה, ונופל על קוד שאין לו סטטוס באחת משתי המפות. .. _mcp-docs-path-policy: איזה קובץ ``docs_get_section`` קורא, ובאיזה ריפו -------------------------------------------------- לכל ריפו יש **מדיניות נתיבים משלו**: איפה התיעוד שלו יושב, ובאיזה פורמט. .. list-table:: :header-rows: 1 :widths: 30 25 15 30 * - ריפו - שורש - סיומת - דוגמה ל-slug קצר * - ``CodeBot`` - ``docs/`` - ``.rst`` - ``mcp-server`` ← ``docs/mcp-server.rst`` * - ``amir-bug-patterns`` - שורש הריפו - ``.md`` - ``CRITICAL-PATTERNS`` ← ``CRITICAL-PATTERNS.md`` **שני שערים, ולא אחד.** ``MCP_DOCS_REPO`` הוא מה שהפריסה **מתירה** בזמן ריצה; הטבלה שלמעלה, שחיה ב-``mcp_server/docs_handlers.py``, היא מה שהקוד **יודע לקרוא**. ריפו חייב לעבור את שניהם. ריפו שנמצא במשתנה הסביבה ואין לו מדיניות נדחה ב-``repo_not_configured`` ו**אינו** נופל לברירת מחדל מתירנית — אחרת ריפו שמישהו הוסיף להגדרות היה מוגש תחת כללים שאיש לא החליט עליהם. **בלוק ה-``instructions`` נוקב בריפואים שעוברים את שני השערים**, ובאותו סדר: המשפט "It serves ..." נבנה בעליית השרת מ-``MCP_DOCS_REPO`` ומהטבלה (``served_docs_repos`` ב-``mcp_server/docs_handlers.py``), כך שריפו שמותר ואין לו מדיניות אינו מופיע בו, ושינוי של משתנה הסביבה משנה את הבלוק בלי שינוי קוד. .. important:: **הכניסה הראשונה ב-``MCP_DOCS_REPO`` היא ברירת המחדל, ולכן היא קובעת גם את הפורמט.** קריאה שאינה נוקבת ב-``repo`` מקבלת את השורש והסיומת של אותו ריפו. שינוי הסדר במשתנה הסביבה הוא שינוי התנהגות, לא סידור: אותו ``path="mcp-server"`` יחפש ``docs/mcp-server.rst`` בסדר אחד ו- ``mcp-server.md`` בשני. **כלל ה-slug.** קלט **בלי** ``/`` מקבל את השורש ואת הסיומת של הריפו; קלט שכבר יש בו ``/`` נלקח כמות שהוא, ורק הסיומת מתווספת אם היא חסרה. מכאן ששני דברים נכונים בו-זמנית: בריפו ששורשו ``docs/`` קובץ בתת-תיקייה דורש את הנתיב המלא (``docs/observability/error_codes``, לא ``observability/error_codes``), ובריפו ששורשו שורש הריפו הנתיב המקונן עובד מאליו (``bugbot-rules/race-toctou``). **בקשה לפורמט שהריפו אינו מגיש נדחית בשמה.** ``.md`` תחת ``CodeBot``, או ``.rst`` תחת ``amir-bug-patterns``, מחזירים:: {"ok": false, "error": "suffix_not_allowed", "repo": "CodeBot", "requested_path": "CRITICAL-PATTERNS.md", "allowed_suffixes": [".rst"]} הסיומת **אינה** מושלמת בשקט מעל הקיימת. אחרת הבקשה הייתה הופכת ל- ``docs/CRITICAL-PATTERNS.md.rst``, חוזרת כ-``not_found``, והקורא היה מחפש את הבעיה בקובץ במקום במדיניות. .. note:: **הסדר: עגינה, אחריה נרמול, ורק אז בדיקת הגבול — והגבול נבדק כיחידת נתיב ולא כרצף תווים.** ``docsecret/x`` אינו תחת ``docs/`` אף שהוא מתחיל באותם תווים, ו-``docs/../secrets`` נדחה כי הוא **נעגן לפני שנפתר**, ולכן הנרמול מוציא אותו אל מחוץ לשורש במקום לתוכו. מנגד ``bugbot-rules/../README.md`` **מתקבל** — הוא נפתר אל תוך הריפו, ו-``..`` אינו מילה אסורה אלא נרמול שנבדק לפי **לאן** הוא מגיע. **ושורש ריק אינו "הכול".** הוא "כל דבר בתוך הריפו", והסיומת עדיין מסננת: ב-``amir-bug-patterns`` כל קובץ ``.md`` נגיש בכל עומק, כולל תחת ``.claude/`` — וזו החלטה מודעת, על ריפו ציבורי שכל 93 קובצי ה-``.md`` בו הם מסמכים שנועדו לקריאה. ``.claude/settings.json`` אינו נגיש, כי הוא אינו ``.md``. **נתיב ארוך מדי נדחה לפני שמישהו קורא אותו.** ``path`` מעל 4,096 תווים מקבל ``path_too_long`` עם ``max_chars`` ו-``actual_chars``, והסירוב קורה בשלב הראשון של פתרון הנתיב — לפני מדיניות הסודות, לפני המראה, ולפני הפרסור. הנתיב הארוך ביותר בכל הריפואים שמוגשים כאן קצר מ-120 תווים, ולכן התקרה אינה חוסמת שום קובץ אמיתי; מה שהיא חוסמת הוא העבודה שגדלה עם אורך הקלט, ובמסלול הזה העבודה הזאת היא סריקת הרכיבים של מדיניות הסודות. **זו הקטנת משטח ולא הגבלת קצב** — היא מגבילה את המחיר של בקשה אחת, לא את מספר הבקשות; ראו אישו #3430. **ו-``ref`` ארוך מדי נדחה גם הוא, בכל שלושת הכלים שמקבלים אותו.** ``ref`` מעל ``MAX_REF_CHARS`` — אותו מספר, ומאותו נימוק — מקבל ``ref_too_long`` עם ``max_chars`` ו-``actual_chars``, בלי ה-``ref`` עצמו (ראו :ref:`mcp-repo-read-misses`). הוולידציה של השם נשארת במקום אחד, ``BASIC_REF_PATTERN``; התקרה רק מונעת מ-``ref`` ענק לחזור בתשובה ולהפוך אותה לגדולה מהתקציב. **ועוד שני סירובים שנוקבים בסיבתם, משתי סיבות שהתחזו לאחרות (#3432).** ``path_outside_root`` — הנתיב תקין בצורתו ופותר אל מחוץ לשורש התיעוד של הריפו (``docs/../secrets``, ``/etc/hosts``, ``../README.md`` בריפו ששורשו ריק); התשובה נושאת את ``root``, שממילא כתוב בתיאור הכלי. עד היום זה חזר כ-``missing_path``, והקורא לא ידע אם שכח נתיב או ניסה לצאת מהשורש; ``missing_path`` נשאר לנתיב ריק בלבד. ו-``repo_not_mirrored`` — למארח אין עותק של הריפו כלל (כל כלי הריפו מחזירים אותו באותו מצב — ראו :ref:`mcp-repo-read-misses`). עד היום גם זה חזר כ-``not_found``, וסוכן שביקש עמוד אמיתי ניסה שם אחר בזמן שהבעיה הייתה אצל המפעיל. אם sync רץ באותו רגע, התשובה היא ``sync_in_progress`` כמו תמיד. **הסירובים שמגיעים מהפרסור עצמו**, וכולם ``error`` ולא כשל כללי: ``inconsistent_line_endings`` בקובץ יש ``\r`` שאינו חלק מ-``\r\n`` — הפורמט של Mac שלפני 2001. שם שכבר קיים באוצר המילים: ``outline`` מחזיר אותו כ-``reason`` על אותה מחלקת קלט בדיוק. בקובץ כזה ספירת השורות אינה חד-משמעית, ומפה שמצביעה לשורה שאי אפשר לבקש גרועה ממפה שאין. **CRLF עובד במלואו.** את הקובץ עצמו אפשר לקרוא כולו, מדופדף, ב-``whole_document=true``. ``too_many_lines`` קובץ Markdown ארוך מ-``MAX_LINES`` שורות (``services/md_parser.py``; הערך ב-:ref:`mcp-limits`). נבדק **לפני** הפרסור, ולכן הסירוב אינו עולה דבר. התשובה נושאת ``max`` — התקרה — ו-``total_lines``, מספר השורות באותה יחידה כמו כל שדה ``lines`` בתשובות ה-MCP; ``line`` אין בה, כי הפרסור לא התחיל. קובץ כזה אפשר לקרוא בטווחים, ב-``codekeeper_get_repo_file`` עם ``lines`` (אדמין), או כולו, מדופדף, ב-``whole_document=true``. **למה תקרת שורות ולא רק תקרת טוקנים:** חלק מהזיכרון של ``markdown-it-py`` נגזר ממספר השורות ולא ממספר הטוקנים — מערכים עם ערך לכל שורה שנבנים לפני שכלל כלשהו רץ, הגדרות קישור שנשמרות בלי טוקן, וציטוט מקונן ששומר רשימות לכל שורה בכל רמה. קובץ כזה יכול לעלות את כל ההקצאה לחוט עם כמה עשרות טוקנים בלבד. ``too_many_tokens`` הפרסור הגיע ל-``MAX_TOKENS`` טוקני בלוק (``services/md_parser.py``; הערך ב-:ref:`mcp-limits`). התקרה נבדקת **ברגע שנוצר כל טוקן** — גם באמצע טבלה ובאמצע רשימה, שנבנות בקריאה אחת לכלל של ``markdown-it-py`` — ולכן הפרסור נעצר באמצע, ולא אחרי שכל העבודה כבר נעשתה. התשובה נושאת ``max`` ואת ``line``, השורה שהפרסור הגיע אליה (בלי ``line`` כשעוד לא נוצר טוקן שיש לו מיקום). את הקובץ אפשר לקרוא כולו, מדופדף, ב-``whole_document=true``. **למה היא נחוצה גם אחרי השדרוג ל-4.2.0:** התקרה של upstream לטבלה שמשלימה תאים ריקים (``MAX_AUTOCOMPLETED_CELLS``, #364) סופרת **לכל טבלה**, והמונה מתאפס בכל טבלה חדשה — קובץ של 128KB עם הרבה טבלאות כאלה הגיע ל-3GB בלי התקרה ונפל ב-``MemoryError``. **איך נבחרו שתי התקרות (#3391)** — יחד, משני תנאים שחייבים להתקיים בו-זמנית: הקלט העוין הגרוע שנמצא בתקרת הקריאה נכנס בעלות שמאגר הקריאות מקצה לפרסור אחד (``_PARSE_COST_BYTES`` ב-``mcp_server/server.py``; ראו :ref:`mcp-runtime-model`), **וגם** אף קובץ אמיתי אינו נחסם — לא ב-``docs/``, לא בכל הריפו, לא ב-amir-bug-patterns ולא בקבצים השמורים. המרווח יושב בשורות ולא בטוקנים, בכוונה: הקובץ האמיתי הארוך ביותר קרוב לתקרה שלו יותר מהצפוף ביותר לשלו, וקובץ שגדל בשורות הוא התרחיש השכיח. הקלט, המדידות, התאריך והסקריפט שמשחזר אותן — ליד ``MAX_LINES`` ו-``MAX_TOKENS`` ב- ``services/md_parser.py``; ``scripts/measure_md_parse_cost.py`` בודק את שני התנאים מחדש בכל שינוי של הפרסר או של התקרות. ``too_many_sections`` הקובץ חוצה את תקרת הכותרות — 50,000 בשני המסלולים, ובאותה דרך: ברירת המחדל של ``max_sections`` בשני הפארסרים היא ``MAX_SECTIONS`` של ``services.doc_sections``, והכלי אינו מעביר תקרה משלו. (עד #3420 ברירת המחדל של ``rst_parser`` הייתה ללא תקרה, והכלי העביר לו את ``_ceiling.MAX_SYMBOLS`` במפורש מאז #3429; היישור נעשה אחרי מדידה על כל 208 קובצי ה-RST — הקובץ העשיר ביותר נושא 50 סקשנים, אחד לאלף מהתקרה — ופלט הכלי על כולם זהה לפני ואחרי.) התקרה של סורק האאוטליין, ``_ceiling.MAX_SYMBOLS``, היא אותו מספר, וטסט מחזיק את השניים שווים. **ב-Markdown היא אינה יכולה להיפגע עם ברירות המחדל** (מאז #3391): כל כותרת פותחת בשורה משלה, ו-``MAX_LINES`` קטן מ-``MAX_SECTIONS`` — כלומר קובץ שהיה חוצה אותה נעצר קודם, ב-``too_many_lines``. עד #3391 זה היה הפוך: Markdown עוין של שורות-תבליט בלי אף כותרת עבר אותה בלי שום סירוב, כי היא סופרת כותרות. היא נשארת בשביל RST, ובחוזה המשותף של שני הפרסרים. גם כאן את הקובץ אפשר לקרוא כולו, מדופדף, ב-``whole_document=true``. כשהפארסר מספק מספר שורה, התשובה נושאת גם ``line`` — **השורה שגרמה לסירוב**, 1-מבוססת, כפי שהפארסר עצמו חישב אותה. זה מה שהופך את הסירוב לפעולה: ``inconsistent_line_endings`` בלי מספר אומר "יש איפשהו ``\r`` בקובץ של 4,000 שורות". ``md_parser`` מספק אותו ל-``inconsistent_line_endings``, ל-``too_many_tokens`` ול-``too_many_sections``; ``too_many_lines`` נושא במקומו את ``total_lines``, כי הפרסור לא התחיל; ``rst_parser`` מרים ``TooManySections`` בלי מספר, ולכן סירוב RST בא בלי השדה. השדה מופיע רק כשיש מספר, ולא כ-``null`` — כמו ``suggestions_truncated`` ו-``remaining_chars`` באותו כלי. כל התשובות האלה נושאות ``repo``, ``path``, ``ref`` ו-``resolved_commit``, כדי שאפשר יהיה לפתוח בדיוק את הקובץ שסירב. **``includes`` הוא שדה של RST.** הוא מונה יעדי ``.. include::``, ול-Markdown אין צורה כזאת — ולכן בעמוד ``.md`` הוא **תמיד** ``[]``. אפס שם אינו "לא בדקנו" אלא "אין מה לבדוק". **ותקרת הגודל היא זו של שירות המראה, 500KB.** הכלי קורא את הקובץ כולו, בלי ``lines`` ובלי ``outline``, ולכן קובץ גדול יותר מחזיר ``unreadable_too_large`` ולא תשובה חתוכה — גם במצב מסמך. למדידה: הקובץ הגדול ביותר ב- ``amir-bug-patterns`` הוא 107KB, כלומר מרווח של פי 4.7. **קובץ שלם:** ``whole_document=true``\ **.** סעיף מתחיל בכותרת, ולכן שני דברים אינם מגיעים מאף סעיף: מה שלפני הכותרת הראשונה — front matter, הקדמה, תווית RST — וקובץ שיש בו כמה כותרות עליונות, שאינו תת-עץ של אף אחת מהן. וסוכן שהגיע עם נתיב, למשל מטבלת טריגרים, אינו יודע מה הכותרת הראשונה בלי קריאה נוספת למפה. במצב מסמך הכלי מחזיר את הקובץ כולו, משורה 1: ``mode: document``, עם ``content``, ``offset``, ``max_chars`` כפי שנאכף, ``truncated``, ו-``remaining_chars`` ו-``next_offset`` כשנשאר עוד. זה העימוד של סעיף, מאותה פונקציה (``_text_page`` ב-``mcp_server/docs_handlers.py``), כולל עמוד שנגמר מוקדם על ``byte_budget`` (:ref:`mcp-docs-section`). התשובה נושאת ``repo``, ``path``, ``ref`` ו-``resolved_commit`` כמו כל תשובה של הכלי, ו-``encoding`` — התווית שהפענוח במראה נתן, באותה משמעות כמו ``file.encoding`` (:ref:`mcp-line-range`): ``latin-1`` שם הוא אזהרה ולא זיהוי. אין בה ``breadcrumb``, ``subsections``, ``neighbors``, ``includes`` ו-``section_count``, כי כולם תוצרים של פרסור — **ובמצב מסמך אין פרסור.** לכן ארבעת הסירובים שלמעלה אינם חלים בו, וקובץ שהפרסר מסרב לו נקרא שלם. מה שכן חל הוא כל מה שקודם לפרסור, באותן תשובות בדיוק: מדיניות הנתיבים שבראש הסעיף, מדיניות הסודות, הריפו, ``ref`` ותקרת הגודל. ``content`` הוא מה ש-``codekeeper_get_repo_file`` מחזיר על אותו קובץ ו-``ref`` — אותה קריאה, אותו פענוח ואותה הסרת BOM — ולכן שום קובץ שלא נקרא היום סעיף-סעיף אינו נקרא במצב הזה. וקריאה מלאה ב-``codekeeper_get_repo_file`` שאינה נכנסת בתקציב (``answer_too_large``) מפנה לכאן, כשכלי התיעוד מגיש את הקובץ: ``read_with`` הוא שם הכלי, ו-``read_with_arguments`` נושאים ``whole_document: true`` — מי שביקש את הקובץ כולו מקבל אותו כולו, ולא מפה שממנה צריך עוד לבחור סעיפים. ההפניה של ``outline`` על קובץ בלי מפה נשארת למפת הכותרות, כי שם ביקשו מפה. ``section`` יחד עם הדגל נדחה ב-``section_and_whole_document`` (גם ``section`` ריק), ו-``max_depth`` ב-``max_depth_and_whole_document``, שניהם לפני כל בדיקה של ריפו או נתיב; ל-``include_subsections`` אין משמעות במצב הזה, ואי אפשר לדעת אם נשלח. בלי הדגל התשובה זהה למה שהייתה לפניו. ראו גם :ref:`mcp-secrets-policy` — מדיניות הסודות חלה על הכלי הזה במלואה. .. _mcp-md-known-divergences: איפה מפת ה-Markdown חולקת על GitHub ---------------------------------------- המפה ש-``services.md_parser`` בונה נמדדת מול cmark-gfm — הפארסר ש-GitHub מריץ — על צורות מחוללות, ב-``tests/test_md_parser_oracle.py``. הפערים הידועים והמדודים יושבים **בטבלה אחת בקוד**, ``_KNOWN_DIVERGENCES``, וכל שורה בה היא **סיבה** ולא רשימת מופעים. הטבלה כאן מעתיקה ממנה, והטסט ``test_the_documentation_table_is_this_table`` משווה ביניהן בכל שורה בנפרד: הסיבה, המספרים, הגרסאות והתאריך. מה שכל צד מחזיר על הצורה לדוגמה נבדק מול שני הפארסרים עצמם, בטסט אחר. .. list-table:: :header-rows: 1 * - הסיבה - צורה לדוגמה - מה GitHub מחזיר - מה אנחנו מחזירים - נמדד - upstream * - תגית HTML מסוג 7 צמודה לשורת מכל (פריט רשימה או ציטוט) - ``## לפני``, שורה ריקה, ``- פריט``, ומיד אחריה ``
`` ואז ``## אחרי`` - סעיף אחד: ``## אחרי`` נבלע בבלוק ה-HTML - שני סעיפים: ``## אחרי`` הוא כותרת - 12 מתוך 26 צורות שנמדדו; markdown-it-py 4.2.0, cmarkgfm 2025.10.22; 2026-09-27 - https://github.com/executablebooks/markdown-it-py/issues/434 * - רשימת תגיות הבלוק של CommonMark 0.31.2: source/search - ``## לפני``, שורה ריקה, ``טקסט``, ומיד אחריה ```` ואז ``## אחרי`` — ובאותה צורה עם ```` - עם ````: סעיף אחד (``## אחרי`` נבלע). עם ````: שני סעיפים - עם ````: שני סעיפים. עם ````: סעיף אחד (``## אחרי`` נבלע) - 28 מתוך 60 צורות שנמדדו; markdown-it-py 4.2.0, cmarkgfm 2025.10.22; 2026-09-27 - אין אישו — זה שינוי במפרט: ``source`` יצאה מרשימת התגיות ב-CommonMark 0.31, ו-``search`` נמצאת בה (https://spec.commonmark.org/0.31.2/#html-blocks, תנאי פתיחה 6). ``markdown-it-py`` עובד לפי הרשימה הזאת מאז 4.0.0, ו-cmark-gfm עוד לפי הקודמת. * - טבלה שעוברת את תקרת התאים המשלימים של markdown-it-py 4.x - ``# כותרת``, שורה ריקה, טבלה ברוחב 300 עמודות עם 230 שורות גוף של תא אחד, ומיד אחריה ``---`` - סעיף אחד: כל השורות בטבלה, ו-``---`` הוא קו מפריד - שני סעיפים: הטבלה נחתכת בתקרה, השורות שנשארו בחוץ הן פסקה, ו-``---`` הופך אותה לכותרת - 2 מתוך 8 צורות שנמדדו; markdown-it-py 4.2.0, cmarkgfm 2025.10.22; 2026-09-27 - אין אישו — זו תקרה מכוונת, שנוספה ב-markdown-it-py 4.0.0 (#364, https://github.com/executablebooks/markdown-it-py/pull/364). ב-3.0.0 אין תקרה, ושם המפה הסכימה עם GitHub. * - כותרת setext שצמודה להגדרת קישור - ``[a]: /u``, ומיד אחריה ``אחרי`` ו-``---`` - הכותרת מתחילה בשורה 1, שורת ההגדרה - הכותרת מתחילה בשורה 2, שורת הכותרת עצמה - 92 מתוך 720 צורות שנמדדו; markdown-it-py 4.2.0, cmarkgfm 2025.10.22; 2026-09-27 - אין אישו. הפער קדם לשדרוג: 3.0.0 מחזיר בדיוק את אותן מפות. * - תו רווח ש-markdown-it-py מזהה ו-cmark-gfm לא, אחרי שם תגית HTML - ``## לפני``, שורה ריקה, ``טקסט``, ומיד אחריה ````, ואז ``## אחרי`` - שני סעיפים: אצל cmark-gfm השורה היא פסקה, ו-``## אחרי`` הוא כותרת - סעיף אחד: אצלנו השורה פותחת בלוק HTML, ו-``## אחרי`` נבלע בו - 2,316 מתוך 5,075 צורות שנמדדו; markdown-it-py 4.2.0, cmarkgfm 2025.10.22; 2026-09-27 - אין אישו. הפער קדם לשדרוג: 3.0.0 מחזיר בדיוק את אותן מפות. **"N מתוך M צורות שנמדדו" הוא מדידה, לא גבול.** M הוא מספר הצורות שהמחולל של אותה שורה בטבלה מייצר, ו-N הוא כמה מהן חולקות על GitHub. על צורה שלא נמדדה המספר אינו אומר דבר. **איך הפערים נמצאו.** שתי השורות הראשונות נמדדו לפני השדרוג ובמהלכו. שלוש האחרונות נמצאו בסקירה של השדרוג: אף מחולל לא בנה טבלה רחבה כל כך, הגדרת קישור או תגית עם רווח כזה. מאז כל אחת מהן היא משפחה קבועה באורקל, ולידן משפחה של כל תגית בלוק ותגית מסוג 1, בכל צורות הכתיבה של ``_HTML_TAG_FORMS`` ובכל ההקשרים של השורה השנייה. כך שינוי ברשימת התגיות של ``markdown-it-py`` יתגלה בשדרוג הבא בלי שמישהו יחשוד בו מראש: ``test_the_two_block_tag_lists_differ_only_by_the_known_row`` נופל, וגם הצורות של השם שנוסף או הוסר. **הפער של** ``source``/``search`` **בפירוט.** נמדדו 60 צורות: 2 תגיות, 6 צורות כתיבה (פתיחה, סגירה, עם מאפיין, באותיות גדולות, עם טקסט אחרי התגית, ולא סגורה) ו-5 הקשרים. מה שמכריע הוא אם השם נמצא ברשימה (בלוק HTML "מסוג 6", שקוטע פסקה) או לא (לכל היותר "סוג 7", שדורש תגית שלמה ולבדה בשורה ואינו קוטע פסקה): - תגית שלמה ולבדה בשורה, **מיד אחרי שורת פסקה** — פער בשתי התגיות, בכיוונים הפוכים. - אותה תגית **מיד אחרי שורת פריט ברשימה** — פער ב-``source`` בלבד. אצלנו היא סוג 7, וזה בדיוק המנגנון של השורה הראשונה בטבלה (#434). **כלל השיוך:** צורה ששורתה מתחילה ב-`` טקסט``) או לא סגורה (```` שלמה ולבדה, מיד אחרי שורת פריט ברשימה. ב-markdown-it-py 3.0.0, שבו ``search`` עוד לא הייתה ברשימה, זה היה מופע של #434 — ``## אחרי`` נשאר כותרת אצלנו ונבלע אצל GitHub. מאז 4.0.0 ``search`` פותחת בלוק HTML שקוטע את הפריט, כמו אצל GitHub, ושני הצדדים מחזירים סעיף אחד. גם זה מקובע בטסט (``closed`` בשורה של הטבלה). **הטבלה שעוברת את התקרה בפירוט.** מ-4.0.0, ``markdown-it-py`` מגביל את מספר התאים שהוא משלים בטבלה אחת: ``MAX_AUTOCOMPLETED_CELLS`` (65,536) ב-``markdown_it/rules_block/table.py``. שורת גוף שיש בה פחות תאים משורת הכותרות מקבלת את התאים החסרים, והספירה מצטברת משורה לשורה. השורה שמעבירה אותה מעל התקרה כבר אינה בטבלה: הטבלה נגמרת שם, וכל השורות שנשארו בחוץ הן פסקה. ``---`` או ``===`` שבא מיד אחריהן הופך את הפסקה לכותרת setext, ברמה 2 או 1. ב-cmark-gfm אין תקרה כזאת: כל השורות בטבלה, והקו שאחריה אינו יוצר כותרת. המשפחה היא טבלה שבדיוק בתקרה וטבלה שעוברת אותה בשורה אחת, ואחרי כל אחת כל אחד מההמשכים של ``_TABLE_CAP_FOLLOWERS``: ``---``, ``===``, שורת טקסט, או סוף הקובץ. פער יש רק כשהטבלה נחתכה וגם קו setext בא אחריה. הקובץ לדוגמה בטבלה הוא 1,682 בתים בלבד: כדי לעבור את התקרה לא צריך קובץ גדול, מספיקה טבלה רחבה. ב-3.0.0 אין תקרה, וכל הצורות מסכימות. **והפער הזה** אינו נגיש דרך הכלי: הפער מתחיל רק אחרי 196,608 טוקנים — שלושה לכל תא משלים עד התקרה — והכלי מסרב לכל מסמך שעובר את ``MAX_TOKENS`` ב-``too_many_tokens``, לפני שהוא מגיע לשם. האורקל משווה בלי התקרות של ``md_parser``, בכוונה: הוא שואל איך הפרסר מבין את הטקסט, והתקרות הן מדיניות משאבים שקובעת אם הכלי יפרסר קובץ בכלל, ולא מה הוא רואה בו (#3391). **הגדרות הקישור בפירוט.** המשפחה היא כל צירוף של הקשר שלפני ההגדרה, צורה של הגדרה, ומה שבא מיד אחריה (``_reference_cells``). יש שלושה סוגי פער: - **82 — אותה כותרת, בשורה אחרת.** כותרת setext שמתחילה מיד אחרי הגדרה. cmark-gfm מדווח את השורה שבה ההגדרה מתחילה: אצלו הכותרת נבנית מהפסקה שהתחילה בשורת ההגדרה. כשמגיע קו ה-setext, הוא מוציא מהפסקה את ההגדרה והופך את מה שנשאר לכותרת, והכותרת שומרת את שורת ההתחלה של הפסקה (``open_new_blocks`` ב-``src/blocks.c``). אנחנו מדווחים את השורה של הכותרת עצמה. - **6 — כותרת שיש רק ב-GitHub.** ``[a]:`` בלי יעד, ומיד אחריה ``===``. ‏``markdown-it-py`` קורא את ``===`` כיעד של ההגדרה, ואין כותרת. cmark-gfm מנסה לקרוא את ``[a]:`` לבדה כהגדרה, לא מצליח, ומקבל כותרת setext שהטקסט שלה ``[a]:``. - **4 — כותרת שיש רק אצלנו.** הגדרה מוזחת לתוך פריט רשימה, ואחריה שורת טקסט וקו setext בלי הזחה. אצלנו הפריט נגמר אחרי ההגדרה, והקו הופך את שורת הטקסט לכותרת. cmark-gfm ממשיך את הפסקה בתוך הפריט, והקו הוא קו מפריד. הפער קדם לשדרוג: 3.0.0 מחזיר בדיוק את אותן מפות, והשכתוב של כלל ההגדרות ב-4.0.0 (#367, תיקון של סיבוכיות ריבועית) לא הזיז אף צורה. **הרווח שאינו רווח בפירוט.** ``markdown-it-py`` כותב עם ``\s`` של פייתון גם את תנאי הפתיחה של בלוק HTML (``HTML_SEQUENCES`` ב-``markdown_it/rules_block/html_block.py``) וגם את התגית עצמה (``markdown_it/common/html_re.py``). cmark-gfm 0.29.0.gfm.13 מקבל שם רק את ``spacechar`` שלו (``src/scanners.re``): רווח, טאב, טאב אנכי, מעבר דף ושני התווים של מעבר שורה. תו ש-``\s`` מזהה ו-``spacechar`` לא — למשל רווח שאינו נשבר (NBSP), שיכול להגיע בהעתקה מדף אינטרנט — פותח אצלנו בלוק HTML, ואצל cmark-gfm הוא אינו חלק מתגית בכלל, והשורה היא פסקה. לכן בכל פער הכיוון אחד: כותרת שיש ב-GitHub ואין אצלנו. זה קורה ב-23 תווים (``_PYTHON_ONLY_SPACES``), ב-61 תגיות הבלוק (כל שם ברשימה של אחד הפארסרים, חוץ מ-``source`` ו-``search``), ב-4 התגיות מסוג 1, ובתגיות מסוג 7: - **תגית בלוק או תגית מסוג 1, והתו צמוד לשם** — פער בכל הקשר מחוץ לציטוט. אצלנו השורה פותחת בלוק שקוטע פסקה. - **תגית בלוק או תגית מסוג 1, ורווח רגיל כבר הפריד את השם** — אין פער. שני הצדדים פתחו בלוק על הרווח הרגיל. - **כל תגית אחרת**, כולל תגית סגירה של סוג 1 — פער רק אחרי שורה ריקה או מיד אחרי כותרת. אצלנו זו תגית שלמה מסוג 7, והיא אינה קוטעת פסקה. - בתוך ציטוט — אין פער. גם הפער הזה קדם לשדרוג, ו-3.0.0 מחזיר בדיוק את אותן מפות. הטריגר של השורה הזאת מסמן רק תו כזה **מיד אחרי שם התגית**. המשפחה מראה פער גם כשהתו יושב בין מאפיינים או לפני ``>`` של תגית מסוג 7, אבל סריקה של שורה אחת אינה יכולה להבדיל שם בין רווח לבין תו בתוך ערך של מאפיין, בלי לכתוב מחדש את הדקדוק של תגית. **קבצים אמיתיים — האם מישהו מושפע:** - **עמודי ה-``.md`` תחת ``docs/``, ב-CI** — הטסט ``test_no_markdown_page_under_docs_holds_a_known_divergence_trigger`` סורק כל עמוד, ונופל על כל שורה שמפעילה שורה בטבלה **שיש לה טריגר** (``trigger`` ב-``_KNOWN_DIVERGENCES``). שני סוגי שורות אינם נסרקים, והפארסר עצמו הוא שקובע אותם ולא כלל שנכתב ביד: שורה בתוך בלוק קוד (גדר, קוד מוזח, front matter), ושורה בתוך בלוק HTML שנפתח בשורה קודמת — כמו ```` בתוך ``