Skip to content

fix: תקרת הסימבולים נוסעת לתוך פרסור ה-RST במקום לסנן את הפלט - #3378

Merged
amirbiron merged 5 commits into
mainfrom
claude/rst-outline-section-ceiling-91q1m5
Sep 14, 2026
Merged

amirbiron merged 5 commits into
mainfrom
claude/rst-outline-section-ceiling-91q1m5

Conversation

@amirbiron

@amirbiron amirbiron commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner

✨ תיאור קצר

התקרה על מספר הסימבולים יושבת בסורקים ולא במנתב בכוונה — סינון של פלט אינו חסימה של עבודה — אבל בסורק ה-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) — שני קבצים בלבד שינו פלט, ושניהם הקבצים שנערכו
הסוויטה 415 עוברים
flake8 --select=E9,F63,F7,F82 · py_compile · mypy · תווי BiDi 0 · נקי · נקי · 0
התפלגות ruff, מול main 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 נקי)
  • בדיקות רצות ועוברות (415)
  • תיעוד עודכן (docs/mcp-server.rst, docs/whats-new.rst)
  • אין סודות/מפתחות בקוד
  • אין מחיקות מסוכנות/פעולות על root — כל כתיבת הטסטים ל-tmp_path בלבד
  • הודעת הקומיט תואמת Conventional Commits
  • עיינתי במסמכי אתר התיעוד — AI-MAP.md כנקודת כניסה, ואז docs/doc-authoring.rst ו-docs/versioning-stable-anchors.rst לפני הכתיבה, ושני העמודים שנגעתי בהם
  • עיינתי ב-amir-bug-patterns — CRITICAL-PATTERNS.md K11 במלואו, 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:

  • 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
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@sourcery-ai

sourcery-ai Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

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.
services/rst_parser.py
mcp_server/outline_scanners/rst.py
tests/test_rst_parser.py
tests/test_mcp_outline.py
tests/test_mcp_docs_handlers.py
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!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

Copy link
Copy Markdown
Contributor

🧯 Dangerous deletes guard report

Policy: see .cursorrules — dangerous deletions are blocked unless wrapped safely.

Summary:

  • Flagged findings (blocking): 0
    0
  • Excluded matches (not blocking): 15
  • Total matches (all files): 129

Flagged findings (file:line:snippet):
(none)

Excluded matches (by path pattern)
./webapp/static/js/md_preview.bundle.js.map:4:  "sourcesContent": ["// Markdown-it plugin to render GitHub-style task lists; see\n//\n// https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments\n// https://github.com/blog/1825-t … [truncated]
./docs/DOCUMENTATION_GUIDE.md:453:rm -rf _build
./docs/Makefile:24:	rm -rf $(BUILDDIR)
./Dockerfile:42:    rm -rf /var/lib/apt/lists/*
./Dockerfile:121:    rm -rf /var/lib/apt/lists/*
./node_modules/katex/package.json:153:    "build": "rimraf dist/ && mkdirp dist && cp README.md dist && rollup -c --failAfterWarnings && webpack && node update-sri.js package dist/README.md",
./node_modules/katex/src/fonts/Makefile:139:	rm -rf pfa ff otf ttf woff woff2
./node_modules/mermaid/dist/mermaid.js.map:4:  "sourcesContent": ["/**\n* Default values for dimensions\n*/\nconst defaultIconDimensions = Object.freeze({\n\tleft: 0,\n\ttop: 0,\n\twidth: 16,\n\theight: 16\n});\n/**\n* Default values for tr … [truncated]
./node_modules/mermaid/dist/chunks/mermaid.esm/chunk-2M32CCKP.mjs.map:4:  "sourcesContent": ["{\n  \"name\": \"mermaid\",\n  \"version\": \"11.12.0\",\n  \"description\": \"Markdown-ish syntax for generating flowcharts, mindmaps, sequence d … [truncated]
./node_modules/mermaid/dist/chunks/mermaid.esm.min/chunk-4HFYJGYH.mjs.map:4:  "sourcesContent": ["{\n  \"name\": \"mermaid\",\n  \"version\": \"11.12.0\",\n  \"description\": \"Markdown-ish syntax for generating flowcharts, mindmaps, sequen … [truncated]
./node_modules/mermaid/dist/chunks/mermaid.esm.min/chunk-4HFYJGYH.mjs:1:var r={name:"mermaid",version:"11.12.0",description:"Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graph … [truncated]
./node_modules/mermaid/dist/chunks/mermaid.core/chunk-KS23V3DP.mjs.map:4:  "sourcesContent": ["{\n  \"name\": \"mermaid\",\n  \"version\": \"11.12.0\",\n  \"description\": \"Markdown-ish syntax for generating flowcharts, mindmaps, sequence  … [truncated]
./node_modules/mermaid/dist/mermaid.min.js:1524:`,"getStyles"),c1e=RQe});var h1e={};dr(h1e,{diagram:()=>NQe});var NQe,f1e=N(()=>{"use strict";$ge();a1e();l1e();u1e();NQe={parser:Fge,db:n1e,renderer:o1e,styles:c1e}});var m1e,g1e=N(()=>{"use  … [truncated]
./node_modules/mermaid/dist/mermaid.min.js.map:4:  "sourcesContent": ["/**\n* Default values for dimensions\n*/\nconst defaultIconDimensions = Object.freeze({\n\tleft: 0,\n\ttop: 0,\n\twidth: 16,\n\theight: 16\n});\n/**\n* Default values fo … [truncated]
./README.md:842:find . -name "__pycache__" -exec rm -rf {} +

@coderabbitai

coderabbitai Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

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.
תיעוד מגבלות MCP
docs/mcp-server.rst, docs/whats-new.rst
נוסף mcp-limits עם תקרות, ערכי תגובה, כללי RST, קידוד קבצים, מידע על לוגים ותיעוד תיקונים נוספים.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 65fed

The section limit stops pathological RST inputs during parsing while preserving the existing MCP limit response. No actionable current-head risk remains.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed כותרת ה-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.

❤️ Share

הסקשנים נעצרים בזמן ובדיוק,
הסורק מחזיר גבול ברור וחיוק.
התיעוד מאיר כל תקרה,
והבדיקות שומרות על המסגרת.
Claude Code קידד במסלול מאובטח,
CodeKeeper forever 💫

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Approved.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

⏱️ Performance report

(No performance test durations collected. Mark tests with @pytest.mark.performance.)

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

📖 Documentation Preview

The documentation has been built successfully!

To view locally:

  1. Download the artifacts
  2. Extract the zip file
  3. Open index.html in your browser

@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

claude and others added 3 commits September 14, 2026 07:41
שדה ה-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
Comment thread services/rst_parser.py


def parse_document(text: str) -> Document:
def parse_document(text: str, *, max_sections: int | None = None) -> Document:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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.

Repository owner deleted a comment from deepsource-io Bot Sep 14, 2026
שני ממצאים מסבב סקירה על הענף.

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
@amirbiron
amirbiron merged commit 13b6f6d into main Sep 14, 2026
29 checks passed
amirbiron pushed a commit that referenced this pull request Sep 20, 2026
…תקרה לסקירה של #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
amirbiron added a commit that referenced this pull request Sep 20, 2026
…רח (#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants