Skip to content

fix(mcp): תקרה לרשימת המועמדים של ambiguous_section, וטסט שמקבע את חוזה Suggestions במסלול ה-Markdown (#3426) - #3434

Closed
amirbiron wants to merge 1 commit into
mainfrom
claude/gracious-einstein-sevk8p
Closed

amirbiron wants to merge 1 commit into
mainfrom
claude/gracious-einstein-sevk8p

Conversation

@amirbiron

@amirbiron amirbiron commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

✨ תיאור קצר

📦 שינויים עיקריים

  • קוד (Backend)
  • בוט טלגרם
  • מסד נתונים/מיגרציות
  • תיעוד (docs/)
  • DevOps/CI/CD

פירוט נקודות (רשימת תבליטים):

  • אומת מול הקוד של היום, לא מול גוף האישו: פריט 1 עדיין פתוח (candidates נבנה מכל matches בלי גבול); פריט 2 השתנה מאז שנכתב — "הצרכן השני" מ-feat(mcp): docs_get_section קורא גם Markdown, לפי מדיניות נתיבים לכל ריפו #3428 הוא בפועל אותו אתר קריאה שמשרת את שני הפורמטים (doc_sections.suggest נקרא פעם אחת ב-docs_handlers), והוא כבר כותב .titles. לכן שם אין תיקון, יש טסט שמקבע.
  • mcp_server/docs_handlers.py: _CANDIDATES_MAX = 50 — אותו מספר כמו doc_sections.MAX_IDENTIFIER_SUGGESTIONS ומאותה סיבה: רשימת המועמדים היא מלאי בסדר הופעה ולא דירוג, ולכן חיתוך שלה מסתיר פריטים בלי קריטריון, והתקרה גבוהה מספיק שכל עמוד אמיתי ייענה במלואו (כותרת כפולה בקורפוס חוזרת פעמיים או שלוש). השוויון בין שני המספרים מקובע בטסט, כמו שני גבולות האאוטליין והפארסר.
  • R6, ולא עותק שני: החיתוך של toc (שהיה inline ב-_toc) ושל candidates עובר דרך עוזר אחד, _capped(items, limit), שמחזיר (items, truncated) — True רק כשהיו יותר מהתקרה, ולעולם לא על רשימה שבדיוק בגודלה (אותו גבול כמו Capped.append ו-max_sections).
  • הדגל מותנה, וזה מה שמחזיק את אפס-הדיף: שדה שקיים תמיד היה משנה כל תשובת ambiguous_section בתצלום בלי ששום התנהגות השתנתה.
  • תיאור הפרמטר section ב-mcp_server/server.py אומר את התקרה והדגל; docs/mcp-server.rst (הסעיף על ambiguous_section) ו-docs/whats-new.rst עודכנו.
  • שורה ריקה אחת נוספה ב-server.py לפני _build_docs_path_doc — E302 שהגיע עם feat(mcp): docs_get_section קורא גם Markdown, לפי מדיניות נתיבים לכל ריפו #3428; CI בוחר רק E9,F63,F7,F82 ולכן לא נפל שם, אבל flake8 בקונפיג של הריפו כן.

🧪 בדיקות

  • איך בדקתם? מה עבר? מה נשאר?
  • Unit — 135 טסטים ב-tests/test_mcp_docs_handlers.py + tests/test_doc_sections.py, ו-36 ב-tests/test_mcp_server_build.py, ירוקים; flake8 נקי.
  • שלושה טסטים חדשים: test_candidates_are_capped_and_the_flag_appears_only_when_they_were_cut (התקרה מוקטנת ל-2 דרך המודול, כמו בטסט תקרת הסקשנים, במקום לבנות 51 כותרות: בדיוק בתקרה — בלי דגל; מעליה — שני הראשונים בסדר הופעה והדגל True), test_the_candidates_cap_is_the_suggestions_inventory_number, ו-test_suggestions_on_a_markdown_page_are_a_list_of_strings_too (דרך הריפו של ה-Markdown, both_repos).
  • הטסטים החדשים על הקוד הישן (origin/main, ב-worktree נפרד): טסט התקרה וטסט השוויון נופלים; טסט ה-Markdown עובר — הוא מקבע התנהגות קיימת, וזה מה שהאישו ביקש.
  • אפס-דיף מדוד: scripts/docs_section_zero_diff.py על אותו קורפוס (ה-docs/ של origin/main, כדי שעריכות התיעוד של ה-PR לא יזהמו את ההשוואה), פעם על הקוד הישן ופעם על החדש — 5,928 רשומות בשתיהן, sha256 זהה (f31c53f5…2db87), cmp בינארי: זהה.
  • Integration — לא רלוונטי (אין שירות חיצוני).
  • Manual — docutils על שני עמודי התיעוד בלי אזהרות מרקאפ (לא בנייה מלאה, לפי CLAUDE.md; RTD יתפוס על ה-PR).

🧪 בדיקות נדרשות ב‑PR

  • 🔍 Code Quality & Security
  • Unit Tests (3.11)
  • Unit Tests (3.12)

📝 סוג שינוי

  • feat: פיצ'ר חדש
  • fix: תיקון באג
  • docs: שינוי תיעוד בלבד
  • refactor: שינוי קוד ללא שינוי התנהגות
  • perf: שיפור ביצועים
  • chore/ci: תשתית/CI
  • breaking change: שינוי שובר תאימות

✅ צ'קליסט

  • הקוד עוקב אחרי הסגנון (Black/isort/flake8/mypy)
  • בדיקות רצות ועוברות
  • תיעוד עודכן (README/Docs)
  • אם נוספו ג'ובים חדשים (Background Jobs) – לא נוספו
  • אם נוספו/שונו משתני סביבה – לא נוספו
  • אם נוספו/השתנו טוקנים – לא רלוונטי
  • אין סודות/מפתחות בקוד
  • אין מחיקות מסוכנות/פעולות על root (ראו .cursorrules)
  • הודעת הקומיט תואמת Conventional Commits (ע"פ הטבלה)
  • CHANGELOG עודכן אם נדרש — docs/whats-new.rst
  • כל ה‑Required Checks לעיל ירוקים
  • צילום/וידאו UI מצורף אם רלוונטי — אין UI
  • עיינתי במסמכי אתר התיעוד — נתיב: AI-MAP.md ← docs/mcp-server.rst ("איך docs_get_section מוצא כותרת", והסעיף על 50 ההצעות) | המשפט: "והכמות: עד 50 הצעות. ... התקרה גבוהה מספיק כדי שעמוד רגיל ייענה במלואו" — זה הנימוק שהתקרה החדשה יורשת. וגם docs/doc-authoring.rst (בלי מספרי שורות, בלי שינוי עוגנים). מ-amir-bug-patterns: bugbot-rules/silent-truncation-at-sink.md (חיתוך שאינו שקט — הדגל), RECURRING-PATTERNS.md R6 (עוזר אחד לשני הגבולות), claude-md-snippets/testing.md (הטסטים הורצו על הקוד הישן).
  • לא נדרש עיון — לא רלוונטי, עיון נעשה

🧩 השפעות/סיכונים

  • שינוי חוזה קטן בכלי ציבורי: תשובת ambiguous_section נושאת לכל היותר 50 מועמדים, ומעבר לזה candidates_truncated: true. אף עמוד אמיתי בקורפוס אינו מגיע לזה (אפס-דיף על 208 קובצי ה-RST), ולכן ההשפעה בפועל היא רק על קלט שנבנה בכוונה.

🔗 קישורים

🧯 סיכון / החזרה לאחור (Rollback)

  • תוכנית חזרה לאחור במקרה תקלה: revert של הקומיט היחיד; אין שינוי בסכימה, במסד או במשתני סביבה.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx


Generated by Claude Code

Review in cubic

Summary by Sourcery

Prevent oversized ambiguous-section responses while preserving existing results and suggestion output contracts.

Bug Fixes:

  • Cap ambiguous-section candidate results at 50 items and report candidates_truncated only when candidates are actually omitted.
  • Add regression coverage ensuring Markdown suggestions remain a list of strings.

Enhancements:

  • Centralize TOC and candidate truncation behavior to preserve consistent boundary semantics and document the updated response contract.

Documentation:

  • Document the candidate limit and conditional truncation flag for ambiguous section responses and record the change in release notes.

Tests:

  • Add coverage for candidate caps, exact-limit behavior, shared cap alignment with identifier suggestions, and Markdown suggestions.

…זה Suggestions במסלול ה-Markdown (#3426)

- ‏`candidates` היה השדה היחיד בתשובת `codekeeper_docs_get_section` בלי תקרה: ‏`toc` חסום ב-400, ‏`suggestions` ב-50, ורק המועמדים חזרו כולם — נמדד בסקירת #3425: 13,030 מועמדים ו-1,393,787 בתים על שאילתה בת שלושה תווים בעמוד סינתטי של 512KB. הענף נדלק מכותרות שנושאות מזהה, ומאז #3428 הקורפוס שנושא אותם (amir-bug-patterns) מוגש.
- התקרה היא 50 — אותו מספר כמו MAX_IDENTIFIER_SUGGESTIONS ומאותה סיבה (מלאי בסדר הופעה ולא דירוג, גבוה מספיק שכל עמוד אמיתי ייענה במלואו), והשוויון מקובע בטסט. ‏`candidates_truncated: true` מופיע רק כשנחתך, כמו `suggestions_truncated`: תצלום אפס-הדיף של הכלי על קורפוס ה-RST של main זהה בית-בית לפני ואחרי (5,928 רשומות, אותו sha256).
- R6: החיתוך של `toc` ושל `candidates` עובר דרך עוזר אחד, ‏`_capped`, במקום עותק שני של "עד התקרה, ודגל רק כשנחתך".
- הצרכן השני מ-#3426 הוא בפועל אותו אתר קריאה לשני הפורמטים (מאז #3428), והוא כותב `.titles`; נוסף טסט שמקבע את זה במסלול ה-Markdown, כי אזהרה ב-docstring אינה מנגנון.
- תיאור הפרמטר `section` ו-docs/mcp-server.rst אומרים את התקרה והדגל; רשומה ב-whats-new. שורה ריקה אחת נוספה ב-server.py לפני `_build_docs_path_doc` (E302 שהגיע עם #3428; CI בוחר רק E9/F63/F7/F82 ולכן לא נפל שם).

טסטים: 135 ב-test_mcp_docs_handlers + test_doc_sections ו-36 ב-test_mcp_server_build ירוקים; טסט התקרה וטסט השוויון נופלים על origin/main ב-worktree נפרד.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx
@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 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.

Sorry @amirbiron, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 1 hour and 22 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sourcery-ai

sourcery-ai Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR bounds ambiguous_section candidate lists to 50 entries, reports candidates_truncated only when candidates are actually omitted, and consolidates truncation behavior while adding regression tests for both the new contract and Markdown suggestion serialization. Documentation and tool descriptions are updated, with corpus-level zero-diff validation intended to confirm no changes to unaffected responses.

Sequence diagram for capped ambiguous section candidates

sequenceDiagram
    participant Client
    participant DocsHandler as docs_handlers
    participant Document

    Client->>DocsHandler: docs_get_section(section)
    DocsHandler->>Document: find matching sections
    Document-->>DocsHandler: matches
    alt multiple matches
        DocsHandler->>DocsHandler: _capped(matches, _CANDIDATES_MAX)
        DocsHandler-->>Client: ambiguous_section with up to 50 candidates
        opt more than 50 matches
            DocsHandler-->>Client: candidates_truncated: true
        end
    end
Loading

File-Level Changes

Change Details Files
Cap ambiguous-section candidate results and expose truncation only when data is actually omitted.
  • Add a 50-item candidates limit aligned with the suggestions inventory limit.
  • Centralize TOC and candidate slicing in a shared helper with exact-boundary truncation semantics.
  • Return candidates in document order and add conditional candidates_truncated metadata.
  • Document the response contract and rationale for the new cap.
mcp_server/docs_handlers.py
mcp_server/server.py
docs/mcp-server.rst
docs/whats-new.rst
Add regression coverage for candidate limiting and Markdown suggestion output.
  • Verify exact-cap responses omit the truncation flag while oversized responses retain the first items and set it.
  • Lock equality between the candidates cap and MAX_IDENTIFIER_SUGGESTIONS.
  • Verify Markdown suggestions are serialized as a list of strings.
tests/test_mcp_docs_handlers.py
Preserve existing behavior for unaffected documentation responses.
  • Validate zero-diff output against the existing corpus after introducing the conditional field.
  • Keep the formatting fix required by repository flake8 configuration.
mcp_server/docs_handlers.py
mcp_server/server.py
tests/test_mcp_docs_handlers.py

Possibly linked issues


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): 128

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 20, 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: 5d7d990b-2af8-446e-8d50-01da2fa920ed

📥 Commits

Reviewing files that changed from the base of the PR and between 3b8628f and b15461a.

📒 Files selected for processing (5)
  • docs/mcp-server.rst
  • docs/whats-new.rst
  • mcp_server/docs_handlers.py
  • mcp_server/server.py
  • tests/test_mcp_docs_handlers.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

העדכון מגביל את ambiguous_section ל-50 מועמדים, מוסיף את candidates_truncated רק לאחר חיתוך, מאחד את לוגיקת החיתוך, מוסיף בדיקות, ומעדכן את תיעוד השרת ויומן השינויים.

Changes

הגבלת מועמדים בזרימת החיפוש

Layer / File(s) Summary
מימוש תקרת המועמדים
mcp_server/docs_handlers.py
נוסף _CANDIDATES_MAX עם ערך 50 ונוסף _capped. מסלול ambiguous_section מחזיר עד 50 מועמדים ומוסיף candidates_truncated: True רק כאשר הרשימה נחתכת.
בדיקות ההתנהגות
tests/test_mcp_docs_handlers.py
הבדיקות מאמתות חיתוך לפי התקרה, שמירת סדר ההופעה, היעדר הדגל כאשר אין חיתוך, התאמה ל-MAX_IDENTIFIER_SUGGESTIONS, וסוג suggestions במסלול Markdown.
תיעוד חוזה התשובה
mcp_server/server.py, docs/mcp-server.rst, docs/whats-new.rst
תיעוד השרת, מדריך ה-MCP ויומן השינויים מתארים את התקרה ואת candidates_truncated. נוסף גם תיקון עיצוב קטן בתיעוד path.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 3 files. (2 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed הכותרת מתארת בצורה ברורה את השינוי המרכזי: הגבלת רשימת המועמדים של ambiguous_section והוספת בדיקת חוזה ל-Suggestions במסלול Markdown. הכותרת מעט ארוכה, אך היא ממוקדת ורלוונטית.
Description check ✅ Passed תיאור ה-PR מקיף את המטרה, השינויים, הבדיקות, הסיכונים, התיעוד ותוכנית החזרה לאחור. הוא מציין גם שבדיקות ה-Required Checks עדיין לא סומנו כירוקות ושאין קישור פעיל ל-Docs Preview, אך אלה אינם מונעים מעב…
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 3 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@github-actions

Copy link
Copy Markdown
Contributor

⏱️ Performance report

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

@github-actions

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 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

amirbiron added a commit that referenced this pull request Sep 21, 2026
… סקירת שבעת ה-PRים (#3434–#3441) (#3443)

* fix(mcp): תקרה לרשימת המועמדים של ambiguous_section, וטסט שמקבע את חוזה Suggestions במסלול ה-Markdown (#3426)

- ‏`candidates` היה השדה היחיד בתשובת `codekeeper_docs_get_section` בלי תקרה: ‏`toc` חסום ב-400, ‏`suggestions` ב-50, ורק המועמדים חזרו כולם — נמדד בסקירת #3425: 13,030 מועמדים ו-1,393,787 בתים על שאילתה בת שלושה תווים בעמוד סינתטי של 512KB. הענף נדלק מכותרות שנושאות מזהה, ומאז #3428 הקורפוס שנושא אותם (amir-bug-patterns) מוגש.
- התקרה היא 50 — אותו מספר כמו MAX_IDENTIFIER_SUGGESTIONS ומאותה סיבה (מלאי בסדר הופעה ולא דירוג, גבוה מספיק שכל עמוד אמיתי ייענה במלואו), והשוויון מקובע בטסט. ‏`candidates_truncated: true` מופיע רק כשנחתך, כמו `suggestions_truncated`: תצלום אפס-הדיף של הכלי על קורפוס ה-RST של main זהה בית-בית לפני ואחרי (5,928 רשומות, אותו sha256).
- R6: החיתוך של `toc` ושל `candidates` עובר דרך עוזר אחד, ‏`_capped`, במקום עותק שני של "עד התקרה, ודגל רק כשנחתך".
- הצרכן השני מ-#3426 הוא בפועל אותו אתר קריאה לשני הפורמטים (מאז #3428), והוא כותב `.titles`; נוסף טסט שמקבע את זה במסלול ה-Markdown, כי אזהרה ב-docstring אינה מנגנון.
- תיאור הפרמטר `section` ו-docs/mcp-server.rst אומרים את התקרה והדגל; רשומה ב-whats-new. שורה ריקה אחת נוספה ב-server.py לפני `_build_docs_path_doc` (E302 שהגיע עם #3428; CI בוחר רק E9/F63/F7/F82 ולכן לא נפל שם).

טסטים: 135 ב-test_mcp_docs_handlers + test_doc_sections ו-36 ב-test_mcp_server_build ירוקים; טסט התקרה וטסט השוויון נופלים על origin/main ב-worktree נפרד.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx

* fix(mcp): תקרת גודל לגוף בקשה והגבלת קצב לפי זהות, עם סירוב שנוקב בסיבתו (#3431)

עד היום המידלוור היחיד בשרת ה-MCP היה האימות: כל משתמש מאומת יכול היה
לשלוח בקשות בכל גודל ובכל תדירות. שני גבולות חדשים, כל אחד בשכבה שלו,
במודול חדש mcp_server/limits.py:

- גודל הגוף: מידלוור ASGI טהור (BodySizeLimitMiddleware) שמסרב ב-413
  {"error": "body_too_large", "max_bytes": ...} לפני שהטרנספורט של ה-SDK
  קורא ומפענח JSON. Content-Length שמצהיר על יותר מהתקרה נדחה לפני שנקרא
  בית אחד; כותרת חסרה או מכזבת נתפסת בספירה של מה שבאמת מגיע. חיץ ולא
  חריגה מתוך receive, כי _handle_post_request של ה-SDK עוטף את קריאת הגוף
  ב-try שתופס Exception ומחזיר שגיאת JSON-RPC בלי סיבה. ברירת המחדל 1MiB,
  נגזרת מ-MAX_CODE_SIZE (100,000 תווים, עד ~600KB כ-JSON עם ensure_ascii).
- קצב לפי זהות: ב-AdminAwareFastMCP.call_tool, המתודה שה-SDK רושם כמטפל
  של tools/call, כלומר נקודה אחת שכל קריאת כלי עוברת בה בשני מצבי האימות
  (במצב OAuth PATAuthMiddleware אינו מותקן, ורק הקונטקסט של הקריאה רואה את
  הזהות). ההכרעה נופלת לפני שגוף סינכרוני נמסר לחוט ולפני שגוף אסינכרוני
  רץ; קריאה שנדחתה מחזירה תשובת כלי רגילה {"ok": false, "error":
  "rate_limited", "limit_per_minute": ..., "retry_after_seconds": ...}.
  מחוץ לבקשה (LookupError מ-request_context) אין את מי לחייב, ולכן הטסטים
  שקוראים לכלים ישירות ממשיכים כמו היום. 60 בדקה, מהמדידות בתגובה באישו:
  0.24 שניות מעבד לעמוד RST עוין של 500KB, 0.47 למסמך Markdown הצפוף, 2.3
  לצורה העוינת, מול מכסה של 0.5 מעבד (30 שניות-מעבד בדקה).
- הפטור לנתיבי הדופק מבני ולא רשימה (blanket-policy-silent-block §7):
  /healthz אינו קריאת כלי ואין לו גוף, ומקובע בטסט על האפליקציה האמיתית.
- rate_limiter.RateLimiter הקיים של הבוט משמש כמנוע (R6): ניקוי החלון אוחד
  ל-_live_entries, ונוספה seconds_until_allowed בשביל retry_after_seconds.
- כיוון דרך MCP_MAX_REQUEST_BYTES (מינימום 65536) ו-MCP_RATE_LIMIT_PER_MINUTE
  (0 מכבה במפורש עם WARNING), נקראים ב-create_app ולא בזמן ייבוא; ערך פגום
  לעולם אינו מרחיב את הגבול (K12 §3). נרשמו ב-config_inspector_service
  ובתיעוד.

אומת עם uvicorn אמיתי: 401 לפני 413 במצב PAT, 413 על Content-Length ועל
גוף chunked בלי כותרת, 120 דגימות של /healthz תחת מגבלה של קריאה אחת
בדקה כולן 200; ועם לקוח ה-MCP של ה-SDK על Streamable HTTP: הקריאה השנייה
מחזירה rate_limited ו-tools/list אחריה עדיין עונה.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx

* fix(parsers): rst_parser בודק את הקלט בכניסה, וברירת המחדל של max_sections מיושרת ל-MAX_SECTIONS (#3421, #3420)

שני הפארסרים מתועדים כבני-החלפה, ועל קלט שאינו מחרוזת הם התנהגו אחרת:
rst_parser.parse_document(None) החזיר מסמך ריק בשקט (מפה ריקה שמתחזה למפה
של קובץ בלי כותרות), ו-17 נפל ב-AttributeError גולמי מתוך .replace, בעוד
md_parser זרק TypeError שאומר מה התקבל. וברירת המחדל של max_sections הייתה
None ב-rst_parser ו-MAX_SECTIONS ב-md_parser, כלומר קורא ששכח להעביר תקרה
קיבל הגנה מהפארסר האחד ולא מהשני.

- #3421: בדיקת כניסה אחת לשניהם, doc_sections.require_str, שמרימה TypeError
  עם אותן מילים ("parse_document expects str, got NoneType"). אף קורא לא
  נשען על הצורה הישנה: המטפל, הסורק והסקריפטים מעבירים תמיד מחרוזת.
  docs_get_section אינו תופס את החריגה, כמו שלא תפס אותה מ-md_parser:
  "חוזה נשבר" ולא "הקלט נדחה", ו-content שם תמיד מחרוזת.
- #3420, ההכרעה: יישור. MAX_SECTIONS עובר ל-services.doc_sections (המודול
  המשותף), מיוצא מחדש משני הפארסרים, והוא ברירת המחדל בשניהם. נמדד לפני
  השינוי על כל 208 קובצי ה-RST ב-docs/: 1,384 סקשנים בסך הכול, הקובץ העשיר
  ביותר (docs/mcp-server.rst) נושא 50 מול תקרה של 50,000, אחד לאלף, ואף
  קובץ אינו נחסם. אפס-דיף על docs_get_section: 5,931 רשומות זהות בית-בית
  לפני ואחרי על אותו קורפוס.
- בעקבות היישור docs_get_section אינו מעביר תקרה לאף פארסר (עד עכשיו העביר
  ל-rst_parser את _ceiling.MAX_SYMBOLS במפורש, כי ברירת המחדל שם הייתה
  None), ו-"max" בסירוב הוא doc_sections.MAX_SECTIONS, המספר שהפרסר באמת
  השתמש בו. הייבוא של _ceiling מהמטפל הוסר.
- outline_scanners/rst.py ממשיך להעביר את _ceiling.MAX_SYMBOLS במפורש
  (המספר של המפה, כותרות ותוויות יחד), וטסט חדש מקבע זאת: מוטציה שמוחקת
  את הארגומנט מפילה אותו.
- scripts/measure_md_parse_cost.py: שלוש הצורות רצות עכשיו על ברירת המחדל
  בשני הפרסרים, כמו הכלי; מתועד בדוקסטרינג, והסקריפט רץ מקצה לקצה.

טסטים: 12 טסטים חדשים או שנערכו נופלים על origin/main; הפין של הסורק עובר
שם בכוונה. שלוש מוטציות ב-worktree (הסורק בלי הארגומנט, המטפל שמעביר תקרה
שוב, require_str שממיר None ל-"") מפילות כל אחת את הטסט שלה.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx

* fix(mirror): get_file_at_commit בודק את גודל האובייקט לפני git show, וקובץ מעל התקרה אינו נטען כלל (#3433, פריט 9)

עד היום git show נטען כולו לזיכרון (capture_output=True) ורק אז max_size
נבדק, כלומר התקרה הייתה בדיקה בדיעבד ולא חסם על הזיכרון: קובץ של 12MB
שנדחה עלה 20MiB שיא בחוט הקריאה לפני שהתשובה הייתה file_too_large. זה
השורש של WARN-003 בסקירת #3429, ומה שהניח את "קריאה אחת עולה לכל היותר X"
כהערה ולא כתכונה של הקוד.

- _object_size: git cat-file -s <sha>:<path> מחזיר את גודל האובייקט
  מהמאגר בלי לקרוא אותו. שתי הפקודות פונות לאותו sha שנפתר פעם אחת
  ב-_validate_ref_with_git, ולכן אין חלון בין הבדיקה לקריאה שסנכרון של
  המראה יכול להיכנס בו (TOCTOU נבחן ונדחה: אובייקט בקומיט נתון אינו משתנה).
- כשל בבדיקת הגודל הוא סירוב באותה מפה של git show, לא נפילה לקריאה בלי
  תקרה; פלט שאינו מספר הוא כשל ולא אפס (U3). מיפוי ה-stderr אוחד
  ל-_object_read_error, כי git 2.43 מדפיס את אותן הודעות לשתי הפקודות על
  נתיב חסר ועל קומיט שאינו מכיל אותו (נמדד).
- החסם על מה שמוחזר נשאר: לנתיב של תיקייה cat-file -s מודד את אובייקט
  העץ ואילו git show מדפיס רשימה, ומה שחוזר לעולם אינו גדול מ-max_size.
- אותה תשובה ואותו size בסירוב (גודל הבלוב), ואותה תשובה מתחת לתקרה.

נמדד (VmHWM, תהליך נקי, מראה bare עם קבצי טקסט): 12MB מול תקרה של 500KB
ושל 10MiB — 20.2 ו-20.4MiB שיא לפני, 0.0 אחרי; 7MB מתחת לתקרה — 14.2
לפני ו-14.5 אחרי, כי אותו כן קוראים.

טסטים על מראה git אמיתית ב-tmp_path עם מרגל על subprocess.run: ארבעה
נופלים על origin/main (הסירוב לפני git show, אותו sha לשתי הפקודות והסדר
ביניהן, כשל בבדיקה שאינו נופל לקריאה, גודל שאינו מספר), ושלושה עוברים
שם בכוונה כבקרות (מתחת לתקרה, נתיב חסר, החסם על מה שמוחזר); מוטציה שמוחקת
את החסם השני מפילה את הפין שלו. התיעוד וההערות שתיארו את "טוענת את ה-blob
כולו לפני בדיקת הגודל" עודכנו.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx

* chore(mcp): ארבעה פריטים קטנים מסקירת #3429 — SUGG-014, SUGG-001, SUGG-004, SUGG-010 (#3433)

- SUGG-004: שורת הקיבולת קוראת את ThreadPoolExecutor._max_workers הפרטי
  דרך _installed_width, וכשהמאפיין ייעלם היא מדפיסה את הרוחב שהתבקש עם
  "(requested; installed width unreadable)" ו-WARNING שאומר למה — במקום
  שהשירות לא יעלה בגלל שורת לוג, ובמקום להדהד את הבקשה כאילו היא המצב
  (state-record-without-state-change). אותו כלל גם ב-WARNING של ה-fallback.
- SUGG-010: md_parser.token_count — פונקציה ציבורית ומתועדת שסופרת טוקנים
  על _MD, המופע שהכלי מריץ — במקום שהסקריפט יקרא ל-_build_parser הפרטית
  ויבנה פרסר חדש לכל קובץ. מחוץ ל-__all__ בכוונה: הרשימה שם היא החוזה של
  "שני פארסרים בני-החלפה", וטסט מקבע את ההפרש בינה לבין זו של rst_parser.
- SUGG-014: שני טסטים לקובץ memory.max שקיים אבל ריק (IndexError) או
  לא-מספרי (ValueError) — שניהם נופלים ל-cgroup v1 ומחזירים את הערך שלו.
  כיסוי השורות היה מלא; התרחיש חסר. מוטציה שמסירה כל אחת מהחריגות
  מה-except מפילה את הטסט שלה.
- SUGG-001: האסרשן שלא יכול היה ליפול הוחלף: os.cpu_count מוצמד ל-16 כדי
  ש"cpu_count + 4" יהיה 20, מספר שהרצפה לעולם אינה — ומוטציה שמחזירה את
  ה-fallback ל-min(32, cpu_count + 4) מפילה אותו.

על origin/main: שלושה טסטים נופלים (הרוחב הלא-קריא, token_count, הסקריפט
דרך הפונקציה הציבורית); שלושת האחרים עוברים שם ומוכחים במוטציות.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx

* refactor: שלושה כללים שנכתבו פעמיים חזרו למקום אחד — CR בודד, גבול front matter, תווים נסתרים (#3419, #3427)

- כלל ה-\r הבודד (סיומת שורה שאינה חד-משמעית) ישב גם ב-mcp_server/outline.py
  וגם ב-services/md_parser.py, ושני העותקים היו צריכים להסכים לנצח (R6).
  עכשיו הוא במודול עלה חדש, services/line_endings.py (CR_WITHOUT_LF,
  find_lone_cr), ושני הצרכנים קוראים לו; הכיוון חד-סטרי (mcp_server מייבא
  מ-services). טסט מוכיח מול markdown-it-py שהכלל נכון, וטסט מבני מוכיח
  ששני הצרכנים מייבאים אותו ואף אחד מהם אינו מחזיק עותק.
- scripts/generate_ai_map.py::_body_start כתב ביד את גבול ה-front matter,
  ונמדד ב-#3418 שהוא חולק על התוסף ש-MyST מריץ בחמש מתוך שתים-עשרה צורות.
  עכשיו הוא קורא את הגבול מהפארסר של הכלי — md_parser.front_matter_end,
  דרך mdit_py_plugins.front_matter על אותו מופע — והסקריפט מוסיף את שורש
  הריפו ל-sys.path כדי לרוץ לבדו כמו קודם. אפס-דיף: AI-MAP.md שנוצר זהה
  בית-בית לזה שבריפו (וגם לפלט הקוד הישן). הטסט שקיבע את הפער בכוונה
  (test_the_front_matter_rules_still_disagree_as_measured) נערך יחד עם
  הסגירה, כפי שה-docstring שלו דרש: הטבלה נשארה עם עמודת אמת אחת (התוסף),
  ושני הקוראים נבדקים מולה; הטסט שהצמיד את המספר "חמש מתוך שתים-עשרה"
  לפרוזה הוסר יחד עם הפער, והפרוזה (md_parser, requirements/base.txt)
  מספרת עכשיו את ההיסטוריה.
- #3427: known_hex4 (utils) ו-_KNOWN_ESCAPE_HEX4 (CodeNormalizer) — שתי
  רשימות זהות של 16 קודים שנבדקו לפני בדיקת הקטגוריה, וכל 16 הם Cf, כלומר
  הרשימה לא הוסיפה דבר. שתיהן נמחקו, ושתי הפונקציות אוחדו
  ל-strip_hidden_escapes אחת בשכבת הדומיין (טהורה, בלי I/O); utils.normalize_code
  מייבא אותה במפורש (הייבוא האופציונלי של הדומיין הפך לייבוא רגיל — מסלול
  ישן שרץ בלעדיו היה עותק שני מחדש). ענף ה-Variation Selectors (Mn, לא Cf)
  נשאר נפרד ונדלק רק עם remove_variation_selectors=True, כמו קודם.
  אפס-דיף מדוד: 39 קלטים של רצפי בריחה × 6 צירופי אפשרויות, שני הנרמולים,
  זהים לפני ואחרי.

טסטים: מה שנוגע בשמות החדשים נופל על origin/main (line_endings,
front_matter_end, strip_hidden_escapes); טסטי ההתנהגות עוברים שם בכוונה
(אפס-דיף) ומוכחים במוטציות — בדיקת הקטגוריה שנמחקת מפילה את 16 טסטי
הקודים, ועותק פרטי של כלל ה-CR בפארסר מפיל את הטסט המבני.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx

* chore(mcp): יתרת ממצאי הסקירה על #3428 — שני סירובים בשמם, טבלאות קפואות, זנב תשובה נפרד, וטסטים שחסרו (#3432)

שמונה מאחת-עשרה ההצעות ממומשות, שלוש מוכרעות במפורש:

- SUGG-013: path_outside_root — נתיב תקין בצורתו שפותר אל מחוץ לשורש
  התיעוד (docs/../secrets, /etc/hosts, ../README.md בשורש ריק) נדחה בשמו,
  עם root בתשובה; missing_path נשאר לנתיב ריק או עם NUL בלבד.
- SUGG-011: repo_not_mirrored — מראה שאין למארח (repo_not_found מהשירות)
  אינה not_found; invalid_commit נשאר not_found (הריפו קיים, ה-ref הוא מה
  שהקורא יכול לשנות); בזמן sync שניהם sync_in_progress כמו קודם. עובר גם
  דרך codekeeper_docs_get_section עם repo ו-path.
- SUGG-021: _PARSERS ו-DOCS_PATH_POLICY מיוצאים כ-MappingProxyType — הוולידציה
  בייבוא היא הבטחה רק אם הטבלה אינה משתנה אחריה; _PARSER_TABLE ו-
  _DOCS_PATH_POLICY_TABLE הם התפר לטסטים (monkeypatch.setitem).
- SUGG-020: זנב עיצוב התשובה של docs_get_section נפרד ל-_answer_from_document
  (ארבע צורות התשובה מתוך מסמך שכבר נפרסר). אפס-דיף: 5,935 רשומות על אותו
  קורפוס, 5,933 זהות בית-בית; השתיים ששונות הן בדיוק שני הפרובים של
  SUGG-013 (missing_path ← path_outside_root).
- SUGG-009: חמישה טסטים לשתי בדיקות הנרמול ב-_validate_policy_tables
  (סיומת ברישיות/בלי נקודה, שורש מוחלט/עם לוכסן סוגר/עם ./).
- SUGG-010: טסט caplog לשורת ה-WARNING על repo_not_configured.
- SUGG-004: טסט ההסכמה בין חיפוש לקריאה אינו דורש עוד returned == allowed —
  repo_policy מצהיר ש-is_denied נשאר שכבה אחרונה, ושוויון מדויק דרש את
  ההפך; נשאר "אפס חסומים בתוצאות" + "הקובץ המותר כן חוזר" נגד ריקנות.
- SUGG-005: _require_git אחד לשני הטסטים תלויי-git — דילוג בלי הבינארי
  במקום skip באחד וקריסה על check=True בשני.
- SUGG-022 (הכרעה, מתועדת ב-docs/mcp-server.rst): סירוב הוא תשובה של
  הפרוטוקול ולא תקרית; אף מסלול סירוב בשכבת המטפלים אינו רושם שורה, בכל
  הקבצים באותה מידה; לוג למה שהמפעיל צריך לדעת, תשובה למה שהקורא צריך;
  ספירת סירובים — ב-PostHog.
- SUGG-012 (נדחה בנימוק): תצוגת התצורה ב-services אינה יכולה לייבא את
  mcp_server.docs_handlers — הכיוון חד-סטרי ומוצהר בכמה מקומות; התקלה
  כבר קולנית בזמן ריצה (WARNING לכל קריאה + repo_not_configured).
- SUGG-018 (נדחה בנימוק): שני חצאי מדיניות הסודות נשארים בשתי שפות כי אין
  מנוע אחד לשניהם; מה שמחזיק אותם יחד — רשימת תבניות אחת, טסט אינטגרציה
  על מראה אמיתית, וטסט ישיר על ארבע הצורות.

טסטים: 17 נופלים על origin/main (הקודים החדשים, הפרוקסי, הוולידטור,
המראה החסרה), 7 עוברים שם כפינים ומוכחים במוטציות — מחיקת ה-WARNING,
טבלאות כ-dict רגיל, ומחיקת בדיקת הסיומת מהוולידטור מפילות כל אחת את
הטסט שלה.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx

* fix(mcp): תקרת הגוף מפסיקה לקרוא גוף אנונימי לפני האימות, ויתרת ממצאי סקירת שבעת ה-PRים

סגירת הממצאים מסקירת Han על #3434–#3441 (הדוח: code-review-7prs-3434-3441.md ב-CodeKeeper).

SEC-001 (רגרסיה של #3431), שלושה חלקים, כל אחד עם טסט שנפל לפני התיקון:
- Content-Length תקין (ספרות ASCII, בלי Transfer-Encoding) עובר בלי קריאה —
  השרת תוחם את הגוף. נמדד על האפליקציה במצב OAuth: 5 קריאות receive()
  לפני ה-401 → 0.
- גוף בלי אורך מוצהר נקרא עד התקרה תחת דדליין של 30 שניות על הלולאה
  כולה (anyio.fail_after), ואז 408 body_read_timeout עם כמה נקרא. נמדד מול
  uvicorn אמיתי: לפני — אין תשובה אחרי 35 שניות; אחרי — 408 אחרי 30.005
  שניות, Connection: close, והשרת סוגר את החיבור.
- רק POST/PUT/PATCH נבדקות (WARN-002): GET /healthz עם Content-Length מזויף
  מחזיר 200 במקום 413, בשני מצבי האימות.
שני הסירובים נושאים Connection: close. טסט סדר-התקנה למצב OAuth לצד זה של
מצב PAT, ותיקון המשפט "הפטור מבני" בתיעוד, ב-docstring ובהערה.

WARN-001: טסט בתת-תהליך שמייבא את mcp_server.app כמו uvicorn, בשני המצבים,
עם ריצת בקרה — שני משתני הסביבה ו-MAX_CODE_SIZE מגיעים לאפליקציה הבנויה.
WARN-003: הוחלט — התיעוד אומר שסירובי מכסה אינם נספרים ב-PostHog (ואף
סירוב אינו נספר לפי סוג), והעיצוב פתוח באישו #3442.
WARN-004: הצורה העוינת במדידת הפרסור רצה עם max_sections=None במפורש.

הצעות שנכנסו: SUGG-022 (התקרה נגזרת מ-MAX_CODE_SIZE ב-request_bytes_for,
גם בעליית השירות; טסט מצמיד ומודד שקובץ עברי מקסימלי נכנס), SUGG-009
(הסירוב הוא CallToolResult שלם — כלי עם טיפוס החזרה ושם לא מוכר מקבלים
rate_limited), SUGG-023 (is None במקום or), SUGG-024 (25 קריאות מקבילות,
תקציב 2 — בדיוק 2 עוברות), SUGG-011 (RateLimiter אינו יוצר רשומה לשאלה
בלבד; פנקס האזהרות מנוקה), SUGG-019 (token_count ו-front_matter_end עוברים
בשער הכניסה של parse_document), SUGG-013, SUGG-002, SUGG-020, SUGG-021,
SUGG-005, SUGG-006, SUGG-010, SUGG-001. YAGNI-001 נמחק, YAGNI-002 נשאר
ומתועד כהחלטה סגורה. עשר ההצעות שנדחו — אישו #3442.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfJTSpDAhDr2yhtmpFkwTx

* fix(normalize): וריאציה-סלקטור מוסר לפי קוד התו ולא לפי צורת הכתיב — \U0000FE0F כמו \uFE0F (CodeRabbit על #3443)

הענף של \UXXXXXXXX ב-strip_hidden_escapes בדק רק את הטווח האידאוגרפי (U+E0100–U+E01EF), ולכן \U0000FE0F נשאר כמות שהוא בזמן ש-\uFE0F הוסר עם remove_variation_selectors=True. עכשיו כלל אחד (_is_hidden) לשתי הצורות: Cf תמיד, ושני טווחי ה-VS רק לפי בקשה. טסט רגרסיה שנפל על הקוד הישן.

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>
@amirbiron

Copy link
Copy Markdown
Owner Author

נכנס דרך #3443

@amirbiron amirbiron closed this Sep 21, 2026
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