You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix: תקרת הסימבולים נוסעת לתוך פרסור ה-RST במקום לסנן את הפלט - #3378
התקרה על מספר הסימבולים יושבת בסורקים ולא במנתב בכוונה — סינון של פלט אינו חסימה של עבודה — אבל בסורק ה-RST היא נגעה רק ברשימה שנבנתה אחרי ש-parse_document סיים. לכן קובץ שנכנס בנוחות לתקרת ה-10MB של קריאת הטווח יכול לקנות עבודה גדולה: 10MB של כותרת בת תו אחד בכל שתי שורות הם 2.6 מיליון סקשנים, 18.5 שניות ו-1,132MB. אחרי השינוי: 0.30 שניות ו-83MB. הזיכרון הוא מה שמצדיק את ה-PR — בקשה איטית היא בקשה איטית, אבל ג'יגה-בייט בתהליך משותף מפיל שירות.
זה חיווט של התקרה שכבר קיימת, ולא כפתור חדש: אפס קבועים חדשים, אפס אוצר מילים חדש בתשובה, ואותו גבול בדיוק.
📦 שינויים עיקריים
קוד (Backend)
בוט טלגרם
מסד נתונים/מיגרציות
תיעוד (docs/)
DevOps/CI/CD
פירוט:
services/rst_parser.py — parse_document מקבל max_sections (keyword-only) שברירת המחדל שלו ללא תקרה, ומרים TooManySections בזמן הפרסור. שלושת אתרי ההוספה — כותרת עם קו תחתון, כותרת עם קו מעליה, וקו עליון קצר שהודח — עוברים דרך הגדרה אחת (add_section) ולא שלוש בדיקות זהות. זה הלקח מכלל בליעת הפסקה שנכתב במודול הזה בשני אתרים, ואתר אחד נשאר מאחור עד שהסבב הבא תפס אותו.
mcp_server/outline_scanners/rst.py — מעביר max_sections=_ceiling.MAX_SYMBOLS, והפרסור עבר לתוך ה-try. ה-except תופס את שתי החריגות בשמן ולא רחב מזה: TooManySections כשהכותרות לבדן ממלאות את התקרה, ו-TooManySymbols מהמכל כשהכותרות והתוויות יחד חוצות אותה.
mcp_server/docs_handlers.py — לא נגעתי. זה הצרכן השני של הפארסר (docs_get_section), הוא אינו מעביר תקרה, ולכן הוא זהה בית-בית. יש לזה טסט שעובר דרכו.
docs/mcp-server.rst — סעיף mcp-limits חדש עם כל תקרה שנאכפת בקוד, כולל תקרות העימוד שלא היו מתועדות כלל; הכלל שערך מעל התקרה נצמד בשקט ואינו נדחה (per_page=1000 באאוטליין מקבל 500, ואין שדה שמצהיר על זה); הרשימה הסגורה של ערכי statusבתשובת כלי מול error כעיקרון — ותשובת HTTP של השרת אינה תשובת כלי, כי 401 נושא error בלי ok; המחלקה היחידה שסורק ה-RST אינו ממודל (בלוק literal מצוטט); שורה בפתרון תקלות על כך שתהליך ה-MCP אינו מגדיר לוגים — ולכן היעדר לוג אינו סימן שהכלי לא נקרא (תהליך ה-MCP אינו מתקין קונפיגורציית לוגים — כל רשומה בו אינה נראית #3375); ומה ש-file.encoding באמת אומר — utf-8/utf-8-sig הם זיהוי, latin-1 הוא נפילה-לאחור שאינה נכשלת לעולם (נמדד: קובץ עברי ב-cp1255 חוזר משובש), ו-BOM מוסר מ-content בזמן ש-size סופר אותו.
🧪 בדיקות
Unit
Integration
Manual
מה
תוצאה
הטסטים החדשים על הקוד שלפני השינוי
חמישה נופלים שם; טסט הצרכן שומר על רגרסיה עתידית, ולכן נבדק בשלוש מוטציות
התקרה דרך ממשק ה-MCP
10MB פתולוגי ← no_outline · too_many_symbols · max, ב-0.31 שניות ו-85MB. בדיוק 50,000 כותרות ← ok עם 50,000 סימבולים. 50,001 ← סירוב
הגבול משני הכיוונים, כולל השער השני
49,999 כותרות + תווית אחת ← ok עם 50,000. 49,999 + שתיים ← סירוב (המכל הוא שעוצר)
עבודה ולא רק תשובה
מונה בנייה של Section: על 200 כותרות ותקרה של 20 נבנים 21 (20 שנכנסו, ואחד שנבנה ונדחה) — מימוש שמסנן פלט היה בונה 200. האסרשן הוא חסם ולא מספר מדויק, כדי שריפקטור לגיטימי לא ייפול עם הודעה שמאשימה בהיפוך
שלוש מוטציות על בדיקת הצרכן
ברירת מחדל שהופכת לתקרה · הכלי מעביר ליטרל · הכלי מעביר את קבוע הסורק — שלושתן נתפסות, והבדיקה O(1) ואינה תלויה בגודל התקרה
אפס דיף בפלט על כל קובצי ה-RST של התיעוד
208 קבצים, אפס דיף מול הפארסר שלפני השינוי
הסכמה מלאה עם docutils 0.23 על הקורפוס
אפס קבצים חולקים על רמות או על ספירה (1,340 = 1,340). ההבדל היחיד הוא טקסט כותרת גולמי מול מרונדר, וזו התנהגות מוצהרת בתיעוד
שתי טבלאות האמת מול docutils
3,840 צורות ← 100% · 6,600 צורות ← 100%
שאר השפות
1,734 קבצים (פייתון, HTML, CSS) — שני קבצים בלבד שינו פלט, ושניהם הקבצים שנערכו
17 UP006 · 4 UP045 · 3 E501 · 2 UP035 — זהה, אפס ממצאים חדשים
העמוד שנגעתי בו מול docutils
אפס אזהרות ואפס שגיאות; רק INFO על עוגנים שאינם מופנים, בדיוק כמו העוגנים שהיו בעמוד לפני
ומה שלא נחסם, במפורש — כתוב בקוד, בתיעוד וכאן: התקרה מגבילה סקשנים ולא שורות. 10MB של שורות ריקות הוא אפס סקשנים ו-6.3 שניות, ואותו זמן בדיוק עם התקרה ובלעדיה, כי אין שם מה לחסום. השארית נקבעת על ידי מספר השורות, והחסם היחיד עליה היום הוא תקרת הבתים — והיא מתועדת באישו #3379, עם המדידה, עם ההגדרים שקיימים היום (require_admin על מסלול האאוטליין, ו-500KB על docs_get_section הציבורי) ועם שני הכיוונים. נמדד גם ששתי מיקרו-אופטימיזציות שנשקלו אינן נוגעות בשארית הזאת כלל, ולכן הן לא נכנסו ל-PR.
ומה שהופך את השארית לנזק משותף ולא רק לבקשה איטית: פונקציית הכלי סינכרונית, ואומת מהמקור של ה-SDK המותקן (mcp 1.28.1, server/fastmcp/utilities/func_metadata.py) שהיא נקראת ישר, בלי to_thread. זה חל על כל הכלים ולא על RST בלבד, ולכן הוא PR נפרד — אחרי מיפוי של כלים שמסתמכים על ריצה בתהליך הראשי. גם הוא באישו #3379.
🧪 בדיקות נדרשות ב‑PR
🔍 Code Quality & Security
Unit Tests (3.11)
Unit Tests (3.12)
📝 סוג שינוי
fix: תיקון באג
✅ צ'קליסט
הקוד עוקב אחרי הסגנון (flake8 0, התפלגות ruff זהה ל-main, mypy נקי)
אין מחיקות מסוכנות/פעולות על root — כל כתיבת הטסטים ל-tmp_path בלבד
הודעת הקומיט תואמת Conventional Commits
עיינתי במסמכי אתר התיעוד — AI-MAP.md כנקודת כניסה, ואז docs/doc-authoring.rst ו-docs/versioning-stable-anchors.rst לפני הכתיבה, ושני העמודים שנגעתי בהם
שתי הערות על הקריאה הזאת.K11 הוא מה שקבע שערוץ הכשל יהיה זריקה ולא ערך החזרה: "עצור" כערך דורש בדיקה בכל אתר קריאה, וכשל בדיקה בודד פירושו שהסריקה ממשיכה כאילו כלום. ו-line-number-coupling הוא הסיבה שטבלת הקבועים בתיעוד מפנה לשם המודול ולשם הקבוע ולא למספר שורה. external-input-isinstance נבדק והוכרע כלא-רלוונטי: max_sections הוא קבוע פנימי ולא ערך שהגיע מחוץ לתהליך.
🧩 השפעות/סיכונים
services/rst_parser.py הוא מודול משותף עם צרכן בייצור, ולכן הסיכון מרוכז בשאלה אחת: האם הצרכן ההוא השתנה. הוא לא — max_sections הוא keyword-only שברירת המחדל שלו None, ו-docs_get_section אינו מעביר אותו. יש לזה טסט שבודק את התקרה האפקטיבית — שהכלי אינו מעביר תקרה, ושברירת המחדל היא None — ולא טסט שמציף את התקרה, כי מחירו של הטסט ההוא היה צמוד לקבוע שהוא מגן עליו.
החוזה כלפי חוץ לא זז: התשובה על קובץ שחוצה את התקרה נשארה no_outline עם too_many_symbols ו-max, בלי symbols ובלי total. מה שהשתנה הוא מתי מפסיקים לעבוד.
Move the existing symbol ceiling into RST parsing to prevent excessive work and memory consumption on large pathological documents while keeping externally visible behavior unchanged.
Bug Fixes:
Enforce the existing symbol ceiling during RST parsing so pathological documents stop generating sections before the full parse completes, reducing excessive memory use while preserving the existing response contract.
Enhancements:
Centralize RST section creation behind a single ceiling-aware path and preserve uncapped parsing for the document-section consumer.
Document MCP limits, ceiling behavior, encoding semantics, tool response conventions, and related operational constraints.
Documentation:
Add comprehensive documentation for enforced MCP limits and update the release notes.
Tests:
Add coverage for all RST heading forms, exact ceiling boundaries, early parsing termination, consumer behavior without a ceiling, and protection against bypassing the centralized section limit.
התקרה על מספר הסימבולים נגעה ברשימה שנבנתה **אחרי** ש-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 שניות — זהה עם התקרה ובלעדיה.
בתיעוד: טבלת הקבועים והתקרות במקום אחד כולל תקרות העימוד שלא היו
מתועדות, הכלל שערך מעל התקרה נצמד בשקט, הרשימה הסגורה של status מול
error כעיקרון, המחלקה שסורק ה-RST אינו ממודל, ושורה בפתרון תקלות על כך
שתהליך ה-MCP אינו מגדיר לוגים.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANHj2BCPu1bCSpsMQvZNQo
The PR wires the existing outline symbol ceiling into RST parsing, stopping section construction at the limit while preserving the public error response and uncapped behavior for the other parser consumer. It consolidates the parser’s section insertion logic, adds boundary and performance-oriented regression tests, and updates MCP documentation to describe the limits and their remaining scope.
Sequence diagram for early RST section limit enforcement
sequenceDiagram
participant Caller as RSTOutlineScanner
participant Parser as parse_document
participant Gate as add_section
participant Ceiling as SymbolCeiling
Caller->>Parser: parse_document(text, max_sections=MAX_SYMBOLS)
Parser->>Gate: add_section(Section)
alt section limit reached
Gate-->>Parser: raise TooManySections
Parser-->>Caller: TooManySections
Caller->>Ceiling: too_many_symbols()
Ceiling-->>Caller: no_outline / too_many_symbols / max
else limit not reached
Gate->>Gate: sections.append(section)
Parser-->>Caller: Document
Caller->>Ceiling: Capped.append(symbols)
alt combined symbols exceed limit
Ceiling-->>Caller: TooManySymbols
Caller->>Ceiling: too_many_symbols()
else within limit
Ceiling-->>Caller: symbols
end
end
Loading
File-Level Changes
Change
Details
Files
Move the RST section ceiling into document parsing so oversized inputs stop generating sections instead of being filtered after parsing.
Add an optional keyword-only section limit and a dedicated TooManySections exception while preserving uncapped parser behavior by default.
Centralize all three heading-to-section insertion paths through one guarded helper with the existing inclusive boundary semantics.
Pass the outline symbol ceiling into the parser and translate parser/container limit exceptions into the existing no_outline response.
Keep the docs section reader uncapped and add regression coverage for both parser behavior and work avoided.
Document the enforced MCP limits, truncation semantics, RST coverage, and remaining synchronous-work limitations.
Add the new mcp-limits documentation covering symbol, pagination, status/error, unsupported literal-block behavior, and logging troubleshooting.
Update RST scanner performance notes and the what's-new entry to describe early parser termination and its scope.
Clarify that section limits do not bound line-processing work or address synchronous tool execution.
docs/mcp-server.rst docs/whats-new.rst
Tips and commands
Interacting with Sourcery
Trigger a new review: Comment @sourcery-ai review on the pull request.
Continue discussions: Reply directly to Sourcery's review comments.
Generate a GitHub issue from a review comment: Ask Sourcery to create an
issue from a review comment by replying to it. You can also reply to a
review comment with @sourcery-ai issue to create an issue from it.
Generate a pull request title: Write @sourcery-ai anywhere in the pull
request title to generate a title at any time. You can also comment @sourcery-ai title on the pull request to (re-)generate the title at any time.
Generate a pull request summary: Write @sourcery-ai summary anywhere in
the pull request body to generate a PR summary at any time exactly where you
want it. You can also comment @sourcery-ai summary on the pull request to
(re-)generate the summary at any time.
Generate reviewer's guide: Comment @sourcery-ai guide on the pull
request to (re-)generate the reviewer's guide at any time.
Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
pull request to resolve all Sourcery comments. Useful if you've already
addressed all the comments and don't want to see them anymore.
Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
request to dismiss all existing Sourcery reviews. Especially useful if you
want to start fresh with a new review - don't forget to comment @sourcery-ai review to trigger a new review!
No actionable comments were generated in the recent review. 🎉
ℹ️ Recent review info⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Advanced
Run ID: f61f14be-a547-4728-a35b-ca00b4cf3d95
📥 Commits
Reviewing files that changed from the base of the PR and between c72ea05 and 65fedde.
📒 Files selected for processing (3)
docs/mcp-server.rst
docs/whats-new.rst
tests/test_rst_parser.py
🚧 Files skipped from review as they are similar to previous changes (2)
docs/mcp-server.rst
docs/whats-new.rst
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.
📝 Walkthrough
Walkthrough
Changes
נוספה תקרת סקשנים אופציונלית ל־parse_document. הפרסר עוצר בזמן יצירת סקשנים. סורק ה־RST ממיר את החריגה לתשובת too_many_symbols. נוספו בדיקות ותיעוד למגבלות MCP. תיעוד whats-new כולל גם תיקונים נוספים.
תקרת סקשנים ותיעוד MCP
שכבה / קבצים
סיכום
חוזה ואכיפת תקרת הפרסר services/rst_parser.py, tests/test_rst_parser.py
נוספו TooManySections ו־max_sections. כל מסלולי יצירת הסקשנים משתמשים בבדיקת התקרה. הבדיקות מאמתות את הגבול ואת היעדר מסלולי הוספה עוקפים.
חיבור הסורק ובדיקות הקצה mcp_server/outline_scanners/rst.py, tests/test_mcp_outline.py, tests/test_mcp_docs_handlers.py
סורק ה־RST מעביר את MAX_SYMBOLS בזמן הפרסור ומחזיר too_many_symbols. הבדיקות מאמתות עצירת עבודה ואת ברירת המחדל של parse_document.
The section limit stops pathological RST inputs during parsing while preserving the existing MCP limit response. No actionable current-head risk remains.
כותרת ה-PR מתארת באופן ברור את השינוי המרכזי: העברת אכיפת תקרת הסימבולים לתוך פרסור ה-RST במקום סינון הפלט לאחר הפרסור.
Description check
✅ Passed
תיאור ה-PR מקיף ורלוונטי. הוא כולל את מטרת השינוי, פירוט הקוד והתיעוד, תוצאות בדיקות, סיכונים, קישורים לבעיות ותוכנית rollback. רוב סעיפי התבנית מולאו, כולל סוג השינוי והצ'קליסט.
Docstring Coverage
✅ Passed
Docstring coverage is 88.24% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 5 files. (2 skipped: 2 …
Linked Issues check
✅ Passed
Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check
✅ Passed
Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches📝 Generate docstrings
Create stacked PR
Commit on current branch
🧪 Generate unit tests (beta)
Create PR with unit tests
Commit unit tests in branch claude/rst-outline-section-ceiling-91q1m5
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
הסקשנים נעצרים בזמן ובדיוק, הסורק מחזיר גבול ברור וחיוק. התיעוד מאיר כל תקרה, והבדיקות שומרות על המסגרת. Claude Code קידד במסלול מאובטח, CodeKeeper forever 💫
Comment @coderabbitai help to get the list of available commands.
שדה ה-encoding נוסע בכל תשובה של קריאת קובץ ולא היה מתועד, והמילים BOM
ו-utf-8-sig לא הופיעו בעמוד אף פעם. שלושת המצבים נמדדו מול
_try_decode_content, שזה אותו מסלול שהכלי עובר בו דרך get_file_at_commit:
- utf-8 — זיהוי.
- utf-8-sig — היה BOM, והוא מוסר מ-content. זה החריג היחיד לכלל שהטקסט
חוזר בית-בית כפי שהוא בקובץ, ו-size מגיע מ-git וסופר גם אותו.
- latin-1 — נפילה-לאחור ולא זיהוי: latin-1 מפענח כל רצף בתים ואינו נכשל,
ולכן הוא תופס כל מה ש-UTF-8 דחה. נמדד שקובץ עברי שנכתב ב-cp1255 חוזר
עם התווית latin-1 ועם תוכן משובש, כלומר cp1255 אינו נגיש אחריו.
המשמעות לקורא: תווית latin-1 היא סימן לחשד ולא אישור. סדר הקודקים עצמו
לא נגע כאן — הוא משותף עם הוובאפ, וזו הכרעה בפני עצמה.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANHj2BCPu1bCSpsMQvZNQo
שלושת הממצאים מסבב הסקירה, ועוד הערה.
1. טסט חוסם-העבודה קיבע len(built) == 21, כלומר פרט מימוש מקרי: הסקשן
שחוצה את התקרה נבנה ונזרק כי הארגומנט מחושב לפני הקריאה. הריפקטור
שה-docstring של add_section מתאר כחלופה ייתן 20, ואז השוואה מדויקת
הייתה נופלת עם ההודעה "הפרסור לא נעצר על התקרה" — כלומר מאשימה
בהיפוך. עכשיו חסם, וההבטחה שנשמרת היא שמימוש שמסנן פלט בונה 200.
2. הטסט שמוכיח שהצרכן בייצור אינו חסום בנה MAX_SYMBOLS + 1 סקשנים
אמיתיים, כלומר מחירו היה צמוד לקבוע שהוא מגן עליו: נמדד שבהעלאת
התקרה פי עשר — מה שההערה ליד הקבוע מזמינה במפורש — אותו טסט היה
בונה 8.9MB ו-500,001 סקשנים, 6.64 שניות ו-482MB בכל ריצת CI. הוא
בודק עכשיו את התקרה האפקטיבית ולא מציף אותה: שהכלי אינו מעביר
max_sections, ושברירת המחדל היא None. שתי הבדיקות O(1) ואינן תלויות
בגודל התקרה, ושלוש מוטציות מוכיחות שהן תופסות — ברירת מחדל שהופכת
לתקרה, הכלי שמעביר ליטרל, והכלי שמעביר את קבוע הסורק.
3. השארית שהתקרה אינה חוסמת — סקשנים ולא שורות — קיבלה אישו #3379 עם
המדידה, עם ההגדרים שקיימים היום, ועם שני הכיוונים. הודאה בהערה אינה
מעקב, ושתי ההערות שמודות בה מפנות עכשיו לאישו.
ובתיעוד: הכלל ש-error מגיע תמיד עם ok: false הוגבל ל"תשובת כלי". תשובת
HTTP של השרת אינה תשובת כלי — 401 מחזיר {"error": ...} לבדו — וסוכן
שרואה JSON אינו מבחין בין השתיים.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANHj2BCPu1bCSpsMQvZNQo
The reason will be displayed to describe this comment to others. Learn more.
`parse_document` has a cyclomatic complexity of 43 with "very-high" risk
A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.
שני ממצאים מסבב סקירה על הענף.
1. התקרה חיה כולה ב-add_section, ו-sections היא list רגילה שאינה יכולה
לסרב בעצמה — כלומר אתר כותרת רביעי עם sections.append ישיר היה פותח
מחדש את החור, וכל הבדיקות היו ממשיכות לעבור. השומר המקביל שקיים
בסורקים חוסם חמש דרכי עקיפה של המכל, אבל הוא מגלוב רק
mcp_server/outline_scanners/*.py ולכן אינו רואה את המודול הזה.
נוסף שומר ast ב-tests/test_rst_parser.py שמאמת שאין שום שינוי של
sections בתוך parse_document מחוץ ל-add_section, וארבעת הגלאים שלו
מוכחים במוטציות: append ישיר, +=, השמה ל-slice, ו-list.append.
2. הפסקה בתיעוד טענה שערך שאינו מספר שלם חוזר לברירת המחדל, וזה לא
נכון. נמדד על _clamp: '250' נותן 250, 2.9 נותן 2, True נותן 1, ורק
None ומחרוזת שאינה מספר חוזרים לברירת המחדל. ונמדד גם על הסכימה של
הכלי עצמו, שהיא הגבול האמיתי: ערך עם שבר, null ומחרוזת לא-מספרית
נדחים לפני שהכלי רץ, מחרוזת מספרית ומספר עשרוני שלם עוברים ומומרים,
ובוליאני עובר ונעשה 1 בלי שגיאה — אותה המרה ש-lines חוסם ב-strict
וכאן אינה חסומה. הפסקה אומרת עכשיו את זה.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANHj2BCPu1bCSpsMQvZNQo
…תקרה לסקירה של #3429
שני ממצאי CodeRabbit על #3429, אומתו מול הקוד:
- ייחוס: הורדת התקרה מ-32 ל-12 מיוחסת עכשיו לסקירה של #3429 (ולא ל-#3433) ב-docs/mcp-server.rst וב-docs/whats-new.rst; #3433 נשאר על חסימת הקריאה במקור.
- הצורה העוינת של RST במסלול הציבורי: `codekeeper_docs_get_section` פרסר בלי תקרה, וכותרת בת תו אחד בכל שורה ב-500KB עלתה 44.0MiB לפרסור — 90.2 בתים לכל בית קלט, מעל 35.2MiB שהמחלק (72) מקצה לחוט; עשרה כאלה במקביל חורגים מהתוכנית (92 + 10 × 44 = 532MiB). התיקון בשורש ולא במחלק: הכלי מעביר ל-`rst_parser.parse_document` את `max_sections=_ceiling.MAX_SYMBOLS` (50,000, אותה תקרה של האאוטליין) ומתרגם `TooManySections` לתשובת `{"ok": false, "error": "too_many_sections", "max": 50000}`. נמדד: אותה צורה נעצרת ב-20.1MiB (41.1 בתים לבית), בתוך ההקצאה; אף עמוד אמיתי אינו מתקרב — 500KB של העמוד הצפוף ביותר הם כ-3,300 סקשנים. ברירת המחדל של הפרסר נשארת None; המחלק נשאר 72, כפי שהוחלט.
- הטסט שקיבע מ-#3378 "הצרכן בייצור אינו מוגבל בתקרה" הוחלף בכוונה ב-`test_the_docs_reader_passes_the_shared_section_ceiling_and_refuses_above_it`, עם הנימוק המדוד בדוקסטרינג; הוא נופל על הקוד הקודם (ae5e140) ונבדק בלי להציף את התקרה (מוקטנת ל-2 דרך המודול). הרשומות שתיארו "אין מתקשר שתופס את החריגה" ב-services/doc_sections.py וב-services/rst_parser.py עודכנו; docs/mcp-server.rst מתעד את הקוד החדש בסעיף של הכלי ובמודל הריצה.
לא נלקחה החלופה של תמחור המחלק לפי 90.2 (8 חוטים): היא מתמחרת את המאגר לפי צורה שהתקרה עכשיו חוסמת, ומצמצמת את המסלול הציבורי בגלל קלט שאין לו מקור.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx
…רח (#3391) (#3429)
* fix(mcp): מאגר הקריאות נגזר ממכסת הזיכרון של הקונטיינר, לא ממעבדי המארח (#3391)
כלי הקריאה רצים ב-asyncio.to_thread, כלומר ב-executor ברירת המחדל של
הלולאה, שגודלו min(32, os.cpu_count() + 4). בקונטיינר os.cpu_count() הוא
מספר הליבות של המארח — CPython מתעד זאת במפורש — ושורת הקיבולת מהייצור
אמרה את זה במספרים: 12 חוטים (os.cpu_count=8) על מכסה של 0.50 cpu.
לפרסור בפייתון טהור אין מקביליות ב-CPU בגלל ה-GIL, ולכן 12 חוטים היו 12
עצי פרסור בזיכרון בו-זמנית — רגע לפני שהפרסור הופך לכלי ציבורי (PR 5).
התיקון: attach_read_pool מתקין בעליית ה-lifespan של אפליקציית ה-ASGI
מאגר משלו, בגודל (מגבלת הזיכרון − קו בסיס − שוליים) ÷ עלות פרסור אחד.
המגבלה נקראת מ-cgroup בזמן ריצה (memory.max ב-v2, memory.limit_in_bytes
ב-v1) ולא מקובעת — השירות כבר עבר תוכנית ואזור פעם אחת. קו הבסיס (92MiB)
והשוליים (64MiB) הם מדידות מהשירות בייצור עם תאריך (Render metrics,
2026-09-19/20). עלות הפרסור נמדדה מחדש על markdown-it-py הנעוץ (3.0.0)
דרך services.md_parser עצמו — שיא RSS, פרסור לכל תהליך נקי — והיא תלויה
בצפיפות הטוקנים של המסמך ולא בגודלו: הקורפוס עולה ~25 בתים לכל בית קלט,
המסמך הצפוף ביותר בו 72 (הקבוע), וצורה עוינת של שורות-תבליט ~290 — מה
ששום רוחב מאגר אינו סופג ומחכה לתקרת טוקנים בפרסר. הקלט חסום ב-500KB כי
docs_get_section דוחה too_large לפני הפרסור. על תוכנית הייצור: 10 חוטים
במקום 12. רצפה 2, תקרה 32, ו-fallback לרצפה כשאין מגבלה קריאה — לא
cpu_count + 4 — עם WARNING שאומר שזה קרה.
ה-seam: loop.set_default_executor דורש לולאה רצה ומקבל רק
ThreadPoolExecutor, build_app רץ בזמן ייבוא, ו-FastMCP.streamable_http_app
בונה את Starlette עם lifespan= משלה כך ש-on_startup אינו רץ — לכן עטיפת
router.lifespan_context, כמו ניקוז ה-PostHog. המאגר אינו נסגר ביציאה:
asyncio.run של uvicorn סוגר אותו ב-shutdown_default_executor עם המתנה.
שורת הקיבולת מפסיקה לחשב בעצמה: היא נרשמת מתוך ה-lifespan, קוראת את
_max_workers של המאגר שהותקן, ומדווחת גם את מגבלת הזיכרון. רשומה שמתארת
מצב ומתעדכנת בנפרד ממנו היא בדיוק מה שמשקר בשקט.
טסטים: כל תנאי עם מוטציה שמפילה אותו (תשע מוטציות, כולן נהרגו, ב-worktree
נפרד); הטסט המרכזי נכנס ל-lifespan של האפליקציה האמיתית ומוכיח ש-to_thread
נוחת על חוט mcp-read; שורת הקיבולת נבדקת בתת-תהליך נקי דרך הפלט האמיתי.
הדמויות של pathlib עברו להחליף את המודול כפי ש-server רואה אותו ולא את
pathlib.Path הגלובלי, שהיה מפיל את pytest ב-INTERNALERROR על כל כישלון.
scripts/measure_md_parse_cost.py מודד את עלות הפרסור מחדש.
תיעוד: "מודל הריצה של הכלים" ב-docs/mcp-server.rst, רשומה ב-whats-new,
הסקריפט ב-docs/development/scripts.rst, ושורת פתרון התקלות על "אין לוג
מהשרת" שהתיישנה מאז #3393.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx
* fix(mcp): תיקוני הסקירה של #3429 — הקבוע מיוחס לפרסר הנכון, תקרה 12, סקריפט המדידה נבדק
מה השתנה, לפי ממצאי הסקירה (הדוח: code-review-3429-read-pool.md ב-Ck):
- WARN-002/003: `_PARSE_RSS_PER_INPUT_BYTE` ו-`_PARSE_COST_BYTES` אומרים במפורש על איזה פרסר נמדד הקבוע (`services.md_parser`, מקדים-סידור לכלי ה-Markdown של PR 5), מה עולה `services.rst_parser` שהכלי הציבורי מריץ היום (קורפוס: כלום מעל השיא שלפני הפרסור, הצפוף ביותר 6.4, עוין 90.2 בתים לכל בית קלט), ומהי ההחזקה הגדולה ביותר של חוט קריאה בפועל: outline של RST בגודל 10MB — 61–77MiB לאורך המסלול, וקריאת `lines=` של `md_preview.bundle.js` האמיתי — כ-37MiB. ההחלטה מתועדת: המחלק נשאר מתומחר לפי הפרסור (כלי הטווח הם אדמין בלבד; 92 + 10 × 37.2 = 464MiB נכנס ב-512MiB; צורת 61–77MiB דורשת קובץ RST של 10MB שאין באף מראה), ולכן טסטי הגזירה של הנוסחה אינם משתנים. השורש — חסימת הקריאה במקור לפני `git show` — באישו #3433.
- YAGNI-001: `_READ_POOL_CAP` יורד מ-32 ל-12, הרוחב שהייצור כבר הריץ, כדי שמעבר לתוכנית גדולה לא ירחיב את המאגר על בסיס מחלק שמתאר את הפרסור ולא את קריאות הטווח. טסט התקרה עודכן (1GiB ו-4GiB ← 12).
- WARN-004/005, SUGG-009/011: `scripts/measure_md_parse_cost.py` מודד את שני הפרסרים, קורא `VmHWM` גם לפני הפרסור ומזקף לפרסור רק מה שמעל הגבוה מבין ה-RSS ושיא-העבר, מדפיס שורת `skipped` ל-stderr על קובץ שאינו UTF-8, יוצא עם הודעה ברורה כשאין קובץ מתאים, ומשתמש ב-`clip_to_bytes` הקיים במקום עותק מקומי (R6). `tests/test_measure_md_parse_cost_script.py` חדש: הטסט הראשון מוכיח שהמועמד לקבוע הוא המקסימום ולא הראשון (מוטציה ל-`next` נתפסת; כל הטסטים נופלים על be33ca9).
- YAGNI-002/003: שני טסטי שורת הקיבולת אוחדו; `DENSEST_TO_MEASURE` הוסר — נמדד רק המסמך הצפוף ביותר.
- WARN-001, SUGG-002/003/012/013: שורת הסטאב הכפולה ב-whats-new נמחקה; `_mib` מסביר למה לא `size_format`; "110 על אותה עקומה" רוכך (גם גרסת הפרסר השתנתה); "6" ← 10 בדוקסטרינג הטסט; הדוקסטרינג של `_fake_cgroup` מתאר רק את ה-seam המשותף.
- תיעוד: "מודל הריצה של הכלים" ב-docs/mcp-server.rst ו-docs/development/scripts.rst עם המספרים החדשים והטריגר למדידה מחדש בשני הפרסרים.
מספרים מהרצת הסקריפט על הקוד הזה (2026-09-20): md — קורפוס 21.3, צפוף 70.7 (הקבוע 72 נשאר), עוין 289.3; rst — 0.0 / 6.4 / 90.2; outline 10MB — 51.3MiB מעל השיא שלפני הפרסור. קריאת הטווח של הבאנדל: 37.2MiB בשלוש הרצות.
שאר ההצעות (SUGG-001/004/005/006/007/008/010/014) רוכזו ב-#3433.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx
* fix(mcp): docs_get_section מעביר לפרסר את תקרת הסקשנים; ייחוס הורדת התקרה לסקירה של #3429
שני ממצאי CodeRabbit על #3429, אומתו מול הקוד:
- ייחוס: הורדת התקרה מ-32 ל-12 מיוחסת עכשיו לסקירה של #3429 (ולא ל-#3433) ב-docs/mcp-server.rst וב-docs/whats-new.rst; #3433 נשאר על חסימת הקריאה במקור.
- הצורה העוינת של RST במסלול הציבורי: `codekeeper_docs_get_section` פרסר בלי תקרה, וכותרת בת תו אחד בכל שורה ב-500KB עלתה 44.0MiB לפרסור — 90.2 בתים לכל בית קלט, מעל 35.2MiB שהמחלק (72) מקצה לחוט; עשרה כאלה במקביל חורגים מהתוכנית (92 + 10 × 44 = 532MiB). התיקון בשורש ולא במחלק: הכלי מעביר ל-`rst_parser.parse_document` את `max_sections=_ceiling.MAX_SYMBOLS` (50,000, אותה תקרה של האאוטליין) ומתרגם `TooManySections` לתשובת `{"ok": false, "error": "too_many_sections", "max": 50000}`. נמדד: אותה צורה נעצרת ב-20.1MiB (41.1 בתים לבית), בתוך ההקצאה; אף עמוד אמיתי אינו מתקרב — 500KB של העמוד הצפוף ביותר הם כ-3,300 סקשנים. ברירת המחדל של הפרסר נשארת None; המחלק נשאר 72, כפי שהוחלט.
- הטסט שקיבע מ-#3378 "הצרכן בייצור אינו מוגבל בתקרה" הוחלף בכוונה ב-`test_the_docs_reader_passes_the_shared_section_ceiling_and_refuses_above_it`, עם הנימוק המדוד בדוקסטרינג; הוא נופל על הקוד הקודם (ae5e140) ונבדק בלי להציף את התקרה (מוקטנת ל-2 דרך המודול). הרשומות שתיארו "אין מתקשר שתופס את החריגה" ב-services/doc_sections.py וב-services/rst_parser.py עודכנו; docs/mcp-server.rst מתעד את הקוד החדש בסעיף של הכלי ובמודל הריצה.
לא נלקחה החלופה של תמחור המחלק לפי 90.2 (8 חוטים): היא מתמחרת את המאגר לפי צורה שהתקרה עכשיו חוסמת, ומצמצמת את המסלול הציבורי בגלל קלט שאין לו מקור.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx
---------
Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
✨ תיאור קצר
התקרה על מספר הסימבולים יושבת בסורקים ולא במנתב בכוונה — סינון של פלט אינו חסימה של עבודה — אבל בסורק ה-RST היא נגעה רק ברשימה שנבנתה אחרי ש-
parse_documentסיים. לכן קובץ שנכנס בנוחות לתקרת ה-10MB של קריאת הטווח יכול לקנות עבודה גדולה: 10MB של כותרת בת תו אחד בכל שתי שורות הם 2.6 מיליון סקשנים, 18.5 שניות ו-1,132MB. אחרי השינוי: 0.30 שניות ו-83MB. הזיכרון הוא מה שמצדיק את ה-PR — בקשה איטית היא בקשה איטית, אבל ג'יגה-בייט בתהליך משותף מפיל שירות.זה חיווט של התקרה שכבר קיימת, ולא כפתור חדש: אפס קבועים חדשים, אפס אוצר מילים חדש בתשובה, ואותו גבול בדיוק.
📦 שינויים עיקריים
פירוט:
services/rst_parser.py—parse_documentמקבלmax_sections(keyword-only) שברירת המחדל שלו ללא תקרה, ומריםTooManySectionsבזמן הפרסור. שלושת אתרי ההוספה — כותרת עם קו תחתון, כותרת עם קו מעליה, וקו עליון קצר שהודח — עוברים דרך הגדרה אחת (add_section) ולא שלוש בדיקות זהות. זה הלקח מכלל בליעת הפסקה שנכתב במודול הזה בשני אתרים, ואתר אחד נשאר מאחור עד שהסבב הבא תפס אותו.mcp_server/outline_scanners/rst.py— מעבירmax_sections=_ceiling.MAX_SYMBOLS, והפרסור עבר לתוך ה-try. ה-exceptתופס את שתי החריגות בשמן ולא רחב מזה:TooManySectionsכשהכותרות לבדן ממלאות את התקרה, ו-TooManySymbolsמהמכל כשהכותרות והתוויות יחד חוצות אותה.mcp_server/docs_handlers.py— לא נגעתי. זה הצרכן השני של הפארסר (docs_get_section), הוא אינו מעביר תקרה, ולכן הוא זהה בית-בית. יש לזה טסט שעובר דרכו.docs/mcp-server.rst— סעיףmcp-limitsחדש עם כל תקרה שנאכפת בקוד, כולל תקרות העימוד שלא היו מתועדות כלל; הכלל שערך מעל התקרה נצמד בשקט ואינו נדחה (per_page=1000באאוטליין מקבל 500, ואין שדה שמצהיר על זה); הרשימה הסגורה של ערכיstatusבתשובת כלי מולerrorכעיקרון — ותשובת HTTP של השרת אינה תשובת כלי, כי401נושאerrorבליok; המחלקה היחידה שסורק ה-RST אינו ממודל (בלוק literal מצוטט); שורה בפתרון תקלות על כך שתהליך ה-MCP אינו מגדיר לוגים — ולכן היעדר לוג אינו סימן שהכלי לא נקרא (תהליך ה-MCP אינו מתקין קונפיגורציית לוגים — כל רשומה בו אינה נראית #3375); ומה ש-file.encodingבאמת אומר —utf-8/utf-8-sigהם זיהוי,latin-1הוא נפילה-לאחור שאינה נכשלת לעולם (נמדד: קובץ עברי ב-cp1255חוזר משובש), ו-BOM מוסר מ-contentבזמן ש-sizeסופר אותו.🧪 בדיקות
no_outline·too_many_symbols·max, ב-0.31 שניות ו-85MB. בדיוק 50,000 כותרות ←okעם 50,000 סימבולים. 50,001 ← סירובokעם 50,000. 49,999 + שתיים ← סירוב (המכל הוא שעוצר)Section: על 200 כותרות ותקרה של 20 נבנים 21 (20 שנכנסו, ואחד שנבנה ונדחה) — מימוש שמסנן פלט היה בונה 200. האסרשן הוא חסם ולא מספר מדויק, כדי שריפקטור לגיטימי לא ייפול עם הודעה שמאשימה בהיפוךflake8 --select=E9,F63,F7,F82·py_compile·mypy· תווי BiDiruff, מול mainומה שלא נחסם, במפורש — כתוב בקוד, בתיעוד וכאן: התקרה מגבילה סקשנים ולא שורות. 10MB של שורות ריקות הוא אפס סקשנים ו-6.3 שניות, ואותו זמן בדיוק עם התקרה ובלעדיה, כי אין שם מה לחסום. השארית נקבעת על ידי מספר השורות, והחסם היחיד עליה היום הוא תקרת הבתים — והיא מתועדת באישו #3379, עם המדידה, עם ההגדרים שקיימים היום (
require_adminעל מסלול האאוטליין, ו-500KB עלdocs_get_sectionהציבורי) ועם שני הכיוונים. נמדד גם ששתי מיקרו-אופטימיזציות שנשקלו אינן נוגעות בשארית הזאת כלל, ולכן הן לא נכנסו ל-PR.ומה שהופך את השארית לנזק משותף ולא רק לבקשה איטית: פונקציית הכלי סינכרונית, ואומת מהמקור של ה-SDK המותקן (
mcp 1.28.1,server/fastmcp/utilities/func_metadata.py) שהיא נקראת ישר, בליto_thread. זה חל על כל הכלים ולא על RST בלבד, ולכן הוא PR נפרד — אחרי מיפוי של כלים שמסתמכים על ריצה בתהליך הראשי. גם הוא באישו #3379.🧪 בדיקות נדרשות ב‑PR
📝 סוג שינוי
✅ צ'קליסט
docs/mcp-server.rst,docs/whats-new.rst)tmp_pathבלבדAI-MAP.mdכנקודת כניסה, ואזdocs/doc-authoring.rstו-docs/versioning-stable-anchors.rstלפני הכתיבה, ושני העמודים שנגעתי בהםamir-bug-patterns—CRITICAL-PATTERNS.mdK11 במלואו,bugbot-rules/silent-fallback-to-worse-path.md,claude-md-snippets/testing.md,bugbot-rules/line-number-coupling.md, ו-bugbot-rules/external-input-isinstance.mdשתי הערות על הקריאה הזאת.
K11הוא מה שקבע שערוץ הכשל יהיה זריקה ולא ערך החזרה: "עצור" כערך דורש בדיקה בכל אתר קריאה, וכשל בדיקה בודד פירושו שהסריקה ממשיכה כאילו כלום. ו-line-number-couplingהוא הסיבה שטבלת הקבועים בתיעוד מפנה לשם המודול ולשם הקבוע ולא למספר שורה.external-input-isinstanceנבדק והוכרע כלא-רלוונטי:max_sectionsהוא קבוע פנימי ולא ערך שהגיע מחוץ לתהליך.🧩 השפעות/סיכונים
services/rst_parser.pyהוא מודול משותף עם צרכן בייצור, ולכן הסיכון מרוכז בשאלה אחת: האם הצרכן ההוא השתנה. הוא לא —max_sectionsהוא keyword-only שברירת המחדל שלוNone, ו-docs_get_sectionאינו מעביר אותו. יש לזה טסט שבודק את התקרה האפקטיבית — שהכלי אינו מעביר תקרה, ושברירת המחדל היאNone— ולא טסט שמציף את התקרה, כי מחירו של הטסט ההוא היה צמוד לקבוע שהוא מגן עליו.החוזה כלפי חוץ לא זז: התשובה על קובץ שחוצה את התקרה נשארה
no_outlineעםtoo_many_symbolsו-max, בליsymbolsובליtotal. מה שהשתנה הוא מתי מפסיקים לעבוד.🔗 קישורים
🧯 סיכון / החזרה לאחור (Rollback)
git revertשל הקומיטים. אין מיגרציה, אין מצב מתמשך, ואין תלות חדשה.🤖 Generated with Claude Code
https://claude.ai/code/session_01ANHj2BCPu1bCSpsMQvZNQo
Summary by Sourcery
Move the existing symbol ceiling into RST parsing to prevent excessive work and memory consumption on large pathological documents while keeping externally visible behavior unchanged.
Bug Fixes:
Enhancements:
Documentation:
Tests: