fix(mcp): תקרת שורות ותקרת טוקנים בפרסר ה-Markdown, ומגבלת קצב של 45 (#3391)

Merged
AM
1mo ago
+3430-441

✨ תיאור קצר

  • עד עכשיו התקרה היחידה בתוך md_parser ספרה כותרות, וקובץ עוין בלי אף כותרת עבר אותה בלי סירוב: 500KB של שורות-תבליט עלו כ-142.5MiB לפרסור אחד, וקובץ של 128KB עם הרבה טבלאות רחבות הגיע ל-3GB ונפל ב-MemoryError (התקרה של upstream לתאים משלימים, #364, היא לכל טבלה).
  • ה-PR מוסיף שתי תקרות בתוך הפרסר: 8,000 שורות שנבדקות לפני הפרסור, ו-45,000 טוקנים שנבדקים ברגע שנוצר כל טוקן — גם באמצע טבלה ובאמצע רשימה. עם שתיהן, הקלט העוין הגרוע שנמצא נכנס בהקצאה לחוט — החסם העליון שלו 95.9% ממנה — ואף קובץ אמיתי אינו נחסם. מגבלת הקצב יורדת מ-60 ל-45 בדקה, כדי ש"זהות אחת לא עוברת דקת מעבד בדקה" תהיה טענה שנבדקת.
  • ובסבב 2 תוקנה שיטת המדידה עצמה: מאז #3429 הסקריפט הסתיר עד כחצי MB מהשיא של כל פרסור, תמיד לכיוון "נכנס". עכשיו השיא נמדד מאיפוס של VmHWM, על 40 סידורי זיכרון, ונשפט כחסם עליון. כל מספר שנמדד בשיטה הישנה ומופיע בתיעוד נמדד מחדש ועודכן כאן. הקבוע 72 נשאר, ומחזיק בזכות התקרות של ה-PR הזה, במרווח של 4.1% (פירוט בסעיף "סבב סקירה 2").
  • ובסבב 3: קלטי המעבד נמדדים עכשיו בזיכרון על כל 40 הסידורים, ופסק הדין בודק שכל מדידה רצה על כולם — ולא רק המדידה המכריעה. ויש ממצא פתוח על זמן המעבד שדורש החלטה שלך (בסוף הסעיף "סבב סקירה 3").

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

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

פירוט נקודות:

  • services/doc_sections.py — שתי חריגות סירוב חדשות: TooManyLines(total_lines, limit) ו-TooManyTokens(line, limit), ליד InconsistentLineEndings ובאותו נימוק (חוזה הסירוב המשותף; המטפל מייבא משם).

  • services/md_parser.py — MAX_LINES = 8_000, MAX_TOKENS = 45_000 (עם המדידות ליד כל אחד), ו-WORST_CASE_CPU_SECONDS = 0.66. תקרת השורות רצה אחרי שער הכניסה ולפני כל העתקה, פיצול או פרסור, ביחידה של count_lines. תקרת הטוקנים יושבת ב-append של רשימת הטוקנים עצמה (_CappedTokens), שכלל core (ck_max_tokens) מתקין לפני block — לא בבנאי של StateCore, ששם רשימה ריקה מוחלפת בשקט (tokens or []). מעקה אחרי הפרסור בודק שהרשימה שחזרה היא זו שהותקנה, ומעקה בכלל עצמו בודק שלא נוצרו טוקנים לפניו — שניהם RuntimeError. המצב של כל פרסור יושב ב-env (U1), והחימום נשאר הצעד האחרון (K15). None מכבה כל תקרה; token_count ו-front_matter_end נשארים בלי תקרות, בכוונה (סקריפטים על קבצי הריפו).

  • mcp_server/docs_handlers.py — too_many_lines (עם max ו-total_lines, בלי line) ו-too_many_tokens (עם max ו-line, בלי line כשעוד אין מיקום). max הוא התקרה שהחריגה נבנתה איתה.

  • mcp_server/limits.py — DEFAULT_RATE_LIMIT_PER_MINUTE = 45, והנימוק בשמות (לא במספרים). גם services/config_inspector_service.py ו-docs/environment-variables.rst.

  • scripts/measure_md_parse_cost.py — מודד עכשיו גם את הקלט העוין כמו שהכלי מקבל אותו: 30 צורות עוינות (עשר הצורות, טבלאות מעל תקרת התאים, הגדרות קישור, ציטוט מקונן, \r\n עם תו אסטרלי ועוד), הקלט המשולב שהוא הגרוע בזיכרון, והקלטים הגרועים במעבד (על כל 40 הסידורים; זמן המעבד שלהם נשפט על 5 הריצות הראשונות) — כל אחד בתהליך נקי, עם RLIMIT_AS וזמן מעבד. שורת פסק דין, וקוד יציאה 1 כשמדידה כמו הכלי לא מדדה פרסור (MEASURED_OUTCOMES: לא סירוב לפני הפרסור ולא MemoryError), כשהזיכרון עובר את _PARSE_COST_BYTES, או כשהמעבד עובר את WORST_CASE_CPU_SECONDS. --doubling — בדיקת ההכפלה (30 צורות × 3 גדלים, בלי תקרות) שנשארת בריפו; משאב שמתחת לרצפת הרעש שלו (MEMORY_NOISE_FLOOR_BYTES, CPU_NOISE_FLOOR_SECONDS) אינו נבדק לגדילה.

  • scripts/md_parser_upgrade_zero_diff.py — _REFUSALS כולל את שתי החריגות החדשות, ומקרה הסירוב md_too_many_sections הוחלף ב-md_too_many_lines ו-md_too_many_tokens.

  • tests/test_md_parser_oracle.py — האורקל משווה בלי התקרות (החלטה 2), ובשורה 3 של _KNOWN_DIVERGENCES עצמה (שדה unreachable חדש) כתוב שהפער אינו נגיש דרך הכלי כי הוא מתחיל רק אחרי 196,608 טוקנים. טסט גוזר את המספר (שלושה טוקנים לכל תא משלים × התקרה) ובודק שהכלי מסרב לכל 8 הצורות.

  • תיעוד — docs/mcp-server.rst: מודל הריצה, טבלת mcp-limits (+MAX_LINES, MAX_TOKENS, WORST_CASE_CPU_SECONDS, 45), גבולות הבקשה (החשבון 45 × 0.66 = 29.7 שניות-מעבד בדקה, מתוך 30, ו-JSON עם 45), mcp-docs-path-policy (שני הסירובים החדשים + איך נבחרו; too_many_sections אינה נגישה ב-Markdown), mcp-docs-section, הדדליין של read_batch (10 + 13.2 = 23.2, מתחת ל-60), ושורה 3 בפערים הידועים. וגם docs/development/scripts.rst, docs/whats-new.rst, FEATURES_SUMMARY.md, requirements/base.txt, ה-docstrings ב-server.py וב-read_batch.py.

  • סבב 2 (0955b3d, 6f4557c) — scripts/measure_md_parse_cost.py (איפוס, probe, 40 סידורים, חסם עליון, הצמדה למעבד, כישלון בקול), הטסטים שלו, וכל המספרים בתיעוד. פירוט בסעיף "סבב סקירה 2" למטה.

  • סבב 3 (a154817) — קלטי המעבד על כל 40 הסידורים לזיכרון, ופסק הדין בודק כל מדידה ולא רק את המכריעה. פירוט בסעיף "סבב סקירה 3" למטה.

  • סבב 4 (5baaeed) — החסם של פיגור המונים נבדק רק על קרנל שהוא מוכח עליו (לינוקס מ-6.16), וטענת הקצב של תור הכתיבה ב-limits.py נצמדת למדגם. פירוט בסעיף "סבב סקירה 4" למטה.

המספר 2.3 שניות: כל ששת המופעים החיים שלו עודכנו (שלושה ב-mcp-server.rst, limits.py, read_batch.py, docs_handlers.py). לחשבונות יש עכשיו בעלים אחד — WORST_CASE_CPU_SECONDS — ו-tests/test_md_parse_worst_case_claims.py גוזר ממנו את מגבלת הקצב, את הדדליין ואת החשבון בתיעוד.

🧪 בדיקות

  • Unit
  • Integration (באץ' על מראת git אמיתית, דרך השרת)
  • Manual (מדידות, אפס-דיף, מוטציות)

שני התנאים שהמספרים חייבים לקיים יחד (2026-09-27, markdown-it-py 4.2.0, Python 3.11, scripts/measure_md_parse_cost.py על הקוד הסופי):

  • הקלט העוין הגרוע נכנס בהקצאה לחוט: ציטוט בעומק 19 בכל אחת מ-8,000 השורות, ממולא עד 512,000 בתים, \r\n ותו אסטרלי בכל שורה, וטבלה שעוברת את התקרה בסוף הציטוט. בשיטה המתוקנת (סבב 2) השיא נע בין 34,004,992 ל-34,959,360 בתים (92.2%–94.8% מ-36,864,000), והחסם העליון — השיא הגבוה ועוד פיגור המונים של הקרנל — הוא 35,340,288 (95.9%). הריצה המלאה של סבב 2 יצאה ב-0 (passed: true); הריצה המלאה של סבב 3 — בקוד 1, על המעבד בלבד (ראו "ממצא פתוח" בסעיף סבב סקירה 3). המספרים שהיו כאן לפני סבב 2 (92.2% ו-92.6%) נמדדו בשיטה הישנה, שהסתירה עד כחצי MB.
  • אף קובץ אמיתי אינו נחסם: הארוך ביותר קובץ שמור של 6,151 שורות (מרווח פי 1.30), והצפוף ביותר 38,153 טוקנים בספירה מדויקת (פי 1.18). ב-docs/ עד 1,287 טוקנים, בריפו עד 8,453, ב-amir-bug-patterns עד 5,142.
  • המעבד: הגרוע בריצה המלאה של סבב 2 — 0.583 שניות (5 חזרות לכל קלט), מתחת ל-0.66 — כלומר 45 בדקה שומר על הטענה (לפני הדחיפה נקבע: מעל 0.667 — לעצור ולדווח). בסבב 3 נמצא שזמן המעבד על המכונה הזו משתנה במהלך היום, עד מעל 0.66 — ראו "ממצא פתוח" בסעיף סבב סקירה 3.

בדיקת ההכפלה (--doubling) — על 4.2.0 כל 30 הצורות ליניאריות. על 3.0.0 היא מסמנת את הגדרות הקישור (הזמן לבית קלט גדל פי 4.13 בין 16KB ל-64KB) ומשאירה את התבליטים ליניאריים — כלומר היא הייתה תופסת את #367 מראש. בסבב 2, עם האיפוס: אף צורה לא סומנה; זיכרון 0.90–1.29, מעבד 0.72–1.56.

אפס-דיף — הסקריפט של main על שני המצבים (main מול ה-PR), אותו קורפוס (הריפו + amir-bug-patterns 456e90b), בלי --expected-change: 524 מפות Markdown ו-208 RST, צורות עוינות, טקסט כותרות, ספירות טוקנים ו-20,666 צורות אורקל ב-12 משפחות — אפס שינויים, חוץ ממקרה סירוב אחד שהוכרע מראש: md_too_many_sections (50,001 כותרות) היה מפורסר עד הסוף ומסורב בשורה 100,003, ועכשיו נדחה ב-too_many_lines לפני הפרסור.

מוטציות — 31, כל אחת ב-worktree נפרד, כולן נתפסות על ידי טסט שבאמת נכשל. בהן: המוטציה שביקשת — הרשימה עוברת בבנאי של StateCore במקום בכלל core (עם המעקה ובלעדיו); הבדיקה אחרי הפרסור במקום ב-append; חיתוך שקט; try/except שמחזיר את מה שנבנה; תקרת השורות אחרי הפרסור; גבולות; המצב על המופע המשותף (ריצת הבקרה של טסט המקביליות, עם setswitchinterval(1e-6)); רישום הכלל אחרי החימום; שינויי 45/0.66/10; _line_of; max; האורקל עם התקרות.

T2 — כל טסט חדש נכשל על main, חוץ משני שומרים שמטבעם עוברים שם (token_count בלי תקרות; באץ' מלא בתוך המגבלה) — אותם מפילות מוטציות.

סוויטה מלאה — 6,311 עברו. 6 נכשלו: 5 הם כלים שחסרים בסביבה (isort/autopep8, כמו בסבבים הקודמים), ו-test_sticky_reminders_polling_browser.py::test_letting_every_click_render_breaks_the_test שעובר לבד גם על main וגם על ה-PR (55/55) — עומס של ההרצה המקבילית, לא קשור לשינוי. את השאר משאירים ל-CI.

mypy — שער ה-CI נקי בקומיט הראשון (3ba65cc) השער לא היה נקי, ואני כתבתי שהוא נקי: הרצתי אותו, ואחריו תיקון E501 פיצל שורה ב-_table_past ל-header = [...] בזמן ש-header כבר החזיק מספר — [return-value], בדיוק מה שהשער מחפש. claude[bot] תיקן ב-4884812, ונבדק (סבב הסקירה למטה). על העץ הנוכחי השער נקי, ובקבצים שנגעתי רק שגיאות שקיימות כבר ב-main. ruff (לא חוסם ב-CI) — פחות שורות ארוכות מ-main; נוספו רק Optional/List ב-md_parser וב-doc_sections, בכוונה, כי זה הסגנון של שני המודולים.

🧪 בדיקות נדרשות ב‑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)
  • בדיקות רצות ועוברות (הרלוונטיות; את הסוויטה המלאה — CI)
  • תיעוד עודכן (README/Docs)
  • אם נוספו ג'ובים חדשים — לא רלוונטי
  • אם נוספו/שונו משתני סביבה – עודכן docs/environment-variables.rst וגם services/config_inspector_service.py (ברירת המחדל של MCP_RATE_LIMIT_PER_MINUTE: 45)
  • אם נוספו/השתנו טוקנים — לא רלוונטי
  • אין סודות/מפתחות בקוד
  • אין מחיקות מסוכנות/פעולות על root
  • הודעת הקומיט תואמת Conventional Commits
  • CHANGELOG עודכן (docs/whats-new.rst)
  • כל ה‑Required Checks לעיל ירוקים — ממתינים ל-CI
  • צילום/וידאו UI — לא רלוונטי
  • עיינתי במסמכי אתר התיעוד — נתיב: AI-MAP.md, docs/doc-authoring.rst, docs/versioning-stable-anchors.rst, docs/mcp-server.rst (מודל הריצה, mcp-limits, גבולות הבקשה, mcp-docs-path-policy, mcp-docs-section, mcp-read-batch, mcp-md-known-divergences), docs/development/scripts.rst, docs/environment-variables.rst, docs/testing.rst, docs/style-glossary.rst | המשפט: "ספירות מופעים בפרוזה אסורות; ערכים שנאכפים מותרים" (doc-authoring) — ולכן בפרוזה נקובים שמות הקבועים, והערכים יושבים בטבלת mcp-limits עם טסט שמשווה אותם לקוד. בסבב הסקירה נקראו שוב doc-authoring ו-versioning-stable-anchors לפני העריכה של scripts.rst, ובסבב 2 — doc-authoring, versioning-stable-anchors ו-style-glossary לפני העריכה של mcp-server.rst ו-whats-new.rst, ובסבב 3 — doc-authoring, versioning-stable-anchors ו-style-glossary לפני העריכה של scripts.rst; בסבב 4 הם לא נקראו מחדש לפני העריכה של scripts.rst, ונקראו שוב אחרי הדחיפה. המשפט שהנחה: "מספר שחייב להופיע נושא תאריך ואת שם הסקריפט שמייצר אותו" (prose-restates-code-fact).

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

  • קובץ Markdown ארוך מ-8,000 שורות או צפוף מ-45,000 טוקנים יקבל סירוב (too_many_lines / too_many_tokens) במקום מפה. נמדד שאין כזה היום בשום מקור שהכלי קורא, והקובץ הארוך ביותר שנמצא בכלל (שמור, 6,151 שורות) רחוק מזה ב-30%. קובץ שסורב עדיין קריא בטווחים (codekeeper_get_repo_file עם lines).
  • מגבלת הקצב יורדת ל-45 בדקה לזהות (MCP_RATE_LIMIT_PER_MINUTE לא מוגדר ב-Render, כלומר ברירת המחדל היא מה שרץ). באץ' מלא (20) עדיין נכנס.
  • #3466 נשאר פתוח, אבל החומרה שלו יורדת: עלות כל פרסור חסומה עכשיו בתוך הפרסר עצמו, מכל מקור שהתוכן מגיע ממנו — ולא רק כי הכלי קורא ריפואים מרשימת היתר.
  • המרווח של 72 דק: 4.1%. מדידה עתידית שעוברת אותו אינה רעש, כי החסם כבר כולל את פיזור הסידורים ואת פיגור המונים. אם זה יקרה, גוזרים מחדש את הקבוע או מהדקים תקרה. מה שיזוז אז הוא רוחב המאגר (היום 10 חוטים).
  • ביצועים: append בפייתון במקום ב-C לכל טוקן — על הקובץ השמור הצפוף ביותר לא נמדד הבדל (0.16 שניות עם התקרה ובלעדיה). עלות בניית המופע עם הכלל החדש נמדדה מחדש (0.03–0.04 מילישניות).

🔗 קישורים

  • Issues קשורים: Closes #3391 · #3466 (נשאר פתוח, חומרה יורדת) · #3431 (המדידה הישנה של 2.3) · #3429 (המאגר, לא משתנה כאן — 10 חוטים, ועכשיו מוצדק גם לקלט העוין; ובאג המדידה שנולד בו תוקן כאן, בסבב 2)
  • מסמכים: תוכנית-PR-ב-תקרות-md_parser.md ב-Ck (התוכנית המאושרת)
  • Branch Protection & PR Rules

📌 סטיות מהתוכנית

  1. WORST_CASE_CPU_SECONDS הוא קבוע חדש שלא היה בתוכנית. הסיבה: תוספת 2 (שישה עותקים של אותו מספר) והחלטה 3 ("טענה שאפשר לבדוק"). עכשיו יש למדידה בעלים אחד, limits.py ו-read_batch.py נוקבים בשמו, וטסט גוזר ממנו את שתי הטענות ואת החשבון בתיעוד.
  2. docs/environment-variables.rst נערך לפני שקראתי את doc-authoring — סטייה בסדר העבודה. נבדק אחר כך מול הכללים, ולא נמצא מה לתקן.
  3. הרשומה של #3431 ב-whats-new (עם 2.3 ו-60) לא שוכתבה — היא רשומה היסטורית מתוארכת; הרשומה החדשה אומרת מה השתנה.
  4. סקריפט המדידה חותך כל קלט לגדול ביותר שהכלי עוד מפרסר (fit_to_the_tool: בתים ושורות). בהרצה הראשונה על הקוד הסופי הקורפוס, המסמך הצפוף וכל הצורות נדחו בתקרת השורות ונמדדו באפס — כלומר נמדד סירוב במקום פרסור. בהרצה השנייה החיתוך השאיר \r בודד בקלט עם \r\n, והסקריפט נפל בקול על זה — תוקן (_without_a_torn_crlf) ומקובע בטסט.
  5. טסט המקביליות עבר על main בגרסה הראשונה שלו — חוט שמת בשקט (ה-except עצמו זרק, כי TooManyTokens אינו קיים שם). עבר ל-Future.result(), ועכשיו הוא נכשל על main ותופס את ריצת הבקרה. באותה משפחה: מריץ המוטציות שלי ספר בהתחלה "לא נאספו טסטים" כתפיסה (בורר -k שלא התאים) — תוקן לפני הריצה הסופית.
  6. ה-docstring הראשי של tests/test_md_parser.py עודכן — הוא אמר ש"אין צרכן" מאז #3428.

🔁 סבב סקירה 1 (4884812, 3b1a740)

  • הקומיט של claude[bot] (4884812) — נבדק, תקין. שינוי שם בלבד: header ← header_tokens במונה הטוקנים של _table_past. על 3ba65cc mypy מחזיר שלוש שגיאות בסקריפט, ובהן [return-value] שהשער של ה-CI מחפש; על 4884812 — נקי. כל קלט שהסקריפט בונה (הקלט הגרוע, _table_past בכמה תקרות, חמשת קלטי המעבד) זהה בבית לפני ואחרי, ו-31 הטסטים של הסקריפט ושל הטענות עוברים. הפספוס היה שלי: השער רץ לפני תיקון ה-E501 שהכניס את השגיאה, ולא אחריו.
  • Greptile — MemoryError שנראה כהצלחה: נכון, תוקן ב-3b1a740. שוחזר בילד האמיתי (RLIMIT_AS של 12MB מעל מה שהייבוא תפס): memory_error עם שיא של 10.5MB, מתחת לתקציב, ופסק הדין אמר true בשתי השאלות. עכשיו MEASURED_OUTCOMES — רשימת היתר של תוצאות שהן מדידה (parsed, too_many_tokens, too_many_sections); כל תוצאה אחרת נרשמת ב-md_unmeasured ומפילה את every_input_measured, ו-passed הוא קוד היציאה. רשימת היתר תופסת גם את too_many_lines — הבאג שכבר קרה בהרצה הראשונה על הקוד הזה (32 מתוך 38 המדידות היו סירובים, ופסק הדין עבר). זה K11, ודפוס 1 ב-BY-STACK/observability.md (המדד כולל מה שלא הוגש).
  • CodeRabbit — רצפת רעש לזיכרון ב-growth: נכון, תוקן ב-3b1a740 כמו שהוצע. על הקוד הקודם, שיא אפס בגודל הקטן נתן יחס של 78,125,000,000 וסימן צורה ליניארית. עכשיו MEMORY_NOISE_FLOOR_BYTES (1MiB) על השיא בגודל הקטן, ומתחתיה None שאינו מסמן. הטסט קיבל שם חדש, test_growth_checks_each_resource_only_above_its_noise_floor, עם הערכים הקיימים מעל הרצפה, שיא אפס, והגבול עצמו. מוטציה שהעבירה את רצפת המעבד לגודל הקטן שרדה — נוסף לה טסט.
  • תוספת קטנה: ה-docstring של SUPERLINEAR_GROWTH אמר "נמדד עד 1.45" (מהאב-טיפוס); בשתי ההרצות הסופיות של --doubling הטווח הוא 0.56–1.73. (בסבב 2 עודכן שוב, לפי ההרצה עם האיפוס.)
  • אימות: הטסטים החדשים נופלים על 4884812 (T2; המרכזיים — על assert 0 == 1 ועל יחס של 78 מיליארד); 16 מוטציות, כולן נתפסו; הרצה אמיתית של הסקריפט על 3b1a740 — קוד 0, passed: true, הקלט הגרוע עדיין נעצר ב-too_many_tokens (91.6%), מעבד 0.563; הרצה אמיתית של --doubling — קוד 0, אף צורה לא סומנה, ואף צורה לא איבדה את בדיקת הזיכרון (השיא הקטן ביותר בגודל הקטן 1.18MB); שער ה-mypy של ה-CI נקי על העץ הסופי; ruff — אין ממצא חדש מול HEAD.
  • ממצא חדש — נסגר בסבב 2 (ראו למטה): השיא נספר מעל שיא-העבר של התהליך (max(VmRSS, VmHWM) לפני הפרסור, מאז #3429), ולכן מה שהפרסור מקצה מתחת לשיא-העבר אינו נספר לו — עד כחצי MB בקלטים של 512KB (בקטנים 0). בקלט הגרוע: נמדדו 33,771,520 בתים, ועם ה-headroom שהוחסר (452KB) — 34,234,368, כלומר 92.9% במקום 91.6%. עדיין בתוך התקציב, אבל פסק הדין מוטה לטובת "נכנס" בעד חצי MB. התיקון השורשי: לאפס את VmHWM ממש לפני הפרסור — כתיבת 5 ל-/proc/self/clear_refs (מאז לינוקס 4.0, proc_pid_clear_refs(5); נבדק כאן שעובד) — ואז השיא הוא בדיוק מה שהפרסור הוסיף. לא מומש, כי הוא משנה את שיטת המדידה שמתועדת ב-server.py ואת המספרים בתיעוד.

🔁 סבב סקירה 2 — שיטת המדידה (0955b3d, 6f4557c)

הבאג, והוא קיים מאז #3429: הסקריפט חישב את השיא כ"השיא אחרי הפרסור, פחות הגבוה מבין ה-RSS שלפני הפרסור לבין השיא שהתהליך כבר הגיע אליו". זיכרון שהתהליך תפס ושחרר לפני הפרסור — בעיקר החוצץ של קריאת הקובץ — הסתיר את מה שהפרסור הקצה מתחתיו: עד כחצי MB על קלט של 512KB, וכ-10MB על outline של 10MB. הטעות תמיד לכיוון "נכנס". היא נכנסה יחד עם הסקריפט עצמו, ב-5bbf0d5 של #3429.

מה השתנה בשיטה (0955b3d):

  • איפוס: הילד המודד כותב 5 ל-/proc/self/clear_refs רגע לפני הפרסור, וסופר את השיא מה-RSS שלפני הפרסור. לפני האיפוס הוא מרים את השיא ב-probe של 8MiB (דרך mmap, לא דרך malloc), וכך בכל ריצה מוכח שהאיפוס באמת קרה.
  • כישלון בקול, בלי נפילה לשיטה הישנה: קובץ שלא קיים או כתיבה שנכשלת — SystemExit עם הודעה. כתיבה שהתקבלה בלי איפוס — SystemExit. probe קטן מכדי להבחין — עצירה.
  • 40 סידורי זיכרון: אותו פרסור על אותו קלט משתנה בעד כ-0.9MB לפי סידור הזיכרון שלפניו. לכן כל מדידת זיכרון רצה על LAYOUT_SEEDS = range(40) (PYTHONHASHSEED מ-0 עד 39), והמספר הוא המקסימום. למה 40 ולא 10: מצב שמופיע בעשירית מהסידורים חומק מעשר ריצות ביותר משליש מהפעמים (0.9 בחזקת 10), ומארבעים ריצות בפחות מ-2% (0.9 בחזקת 40). שני המספרים הם קבועים עם שם, והנימוק כתוב לידם.
  • חסם עליון: מה שנשפט הוא המקסימום ועוד פיגור המונים של הקרנל (counter_lag_bound_bytes: שלושה מונים, וכל אחד מפגר עד batch−1 עמודים; כאן 380,928 בתים).
  • הצמדה למעבד אחד: הילד המודד מוצמד למעבד אחד, כדי שהפיגור יהיה של מעבד אחד בלבד. ההצמדה היא של המדידה, לא של השרת, והיא נכשלת בקול כשאי אפשר (אין sched_setaffinity, או OSError).

ריצת בקרה (תנאי 1): אותו קלט (הגרוע, כמו שהכלי מקבל אותו), אותם 40 זרעים, מוצמד, ישן וחדש לסירוגין.

השיטה הישנההשיטה החדשה
מקסימום34,373,632 (93.24%)34,750,464 (94.27%)
מינימום33,841,15234,209,792

ההפרש במקסימום הוא +376,832 בתים — כמעט בדיוק ה-headroom שהשיטה הישנה דיווחה והפחיתה (חציון 376KB). "הישנה ועוד ה-headroom שלה" נותנת 34,758,656, בתוך 8KB מהחדשה. אחרי האיפוס נשארו 0KB בכל 40 הריצות.

טבלת ההחלטה — ארבעת הזוגות, ישן מול חדש (40 זרעים לכל זוג, באחוזים מ-36,864,000):

8,000/45,000 ישן8,000/45,000 חדש7,000/50,000 ישן7,000/50,000 חדש9,000/45,000 ישן9,000/45,000 חדש8,000/50,000 ישן8,000/50,000 חדש
מקסימום93.3293.1893.4394.3896.8398.8097.9497.88
חסם עליון—94.21—95.41—99.83—98.91

הזוג הנבחר (8,000/45,000) נשאר הזול ביותר, והזוג 9,000/45,000 כמעט נוגע בתקרה. תנאי העצירה (96%) לא נדלק: כמו שהכלי מקבל אותו, הדגימה הגבוהה ביותר בכל הריצות עם האיפוס היא 34,959,360 (94.84%), והחסם העליון שלה 35,340,288 — 95.87%.

72 — ההחלטה שלך: להשאיר.

  • המספר שמחזיק אותו: 69.02 בתים לכל בית של תקרת הקריאה — 95.9% מ-72, מרווח של 4.1% בלבד. זה חסם עליון ולא ממוצע: הדגימה הגבוהה ועוד פיגור המונים. הוא כתוב ליד הקבוע עם התאריך, ולידו כתוב שמדידה עתידית שעוברת את 72 אינה רעש — החסם כבר כולל את הפיזור ואת הפיגור.
  • הצורה שממנה 72 נגזר ב-#3429 — CLOUD.md משוכפל עד 512KB, בלי תקרות — עולה 72.4 על 4.2.0 ו-72.8 על 3.0.0 (חסם 73.2 ו-73.6), מעל 72. ה-71.7 של #3429 היה ריצה אחת מתוך טווח של 71.2–72.8. לכן הקבוע מחזיק רק בזכות התקרות של ה-PR הזה, וגם זה כתוב ליד הקבוע.

הפיזור (דרישה 2): בכל הריצות עם האיפוס, השיא של הקלט הגרוע נע בין 34,004,992 ל-34,959,360 (92.2%–94.8%). בגרסה מוקדמת של הילד 36 מ-40 הזרעים נפלו ב-92.5%–92.9% וארבעה ב-94.5%; בגרסה הסופית 39 נפלו ב-93.6%–94.3% ואחד ב-92.7%. איזה מצב שכיח תלוי בתוכנית הילד עצמה, ולכן בתיעוד כתוב הטווח ולא "ארבעה מתוך 40". מי שמריץ פעם אחת ומקבל מספר נמוך מהמתועד פגש סידור, לא שינוי.

הטסט שמוכיח שהאיפוס קרה (תנאי 5): מסמך של 4,000 פריטים, אחרי חימום של 7,000 פריטים שהשאיר שיא-עבר גבוה. השיא שנמדד קטן מהשיא של החימום, וגדול מ-1MB. בלי האיפוס (הקוד הישן), הילד מדווח 0 על פרסור שעולה בפועל 4.85–5.90MB. מוטציה שמסירה את כתיבת האיפוס מפילה את הטסט.

  • מוטציות: 24, כולן נתפסות. בהן: הסרת האיפוס, הסרת הבדיקה שאחריו, איפוס לפני ה-probe, הנוסחה הישנה יחד עם האיפוס, מסכת מעבד שלא חוזרת, והסרת שמירת הרצפה ב---doubling.
  • T2: על 3b1a740 (לפני הסבב) 30 מהטסטים של הסקריפט נופלים, ו-20 עוברים — אלה שלא השתנו.

כל מספר שנמדד בשיטה הישנה ומופיע בתיעוד — עודכן כאן (6f4557c: server.py, md_parser.py, mcp-server.rst, whats-new.rst, git_mirror_service.py, הסקריפט והטסטים שלו):

מההיהעכשיו
הקלט הגרוע תחת התקרות92.6%חסם עליון 95.9% (השיא 92.2%–94.8%)
CLOUD.md משוכפל, בלי תקרות (4.2.0 / 3.0.0)70.1 / 71.272.4 / 72.8 (חסם 73.2 / 73.6)
קורפוס ה-Markdown משוכפל (4.2.0 / 3.0.0)20.5 / 20.824.6 / 24.7
Markdown עוין, בלי תקרותכ-290 בתים לבית, כ-142MiB291.9, 142.5MiB
קורפוס ה-RST משוכפל"לא עולה מעל השיא שלפני הפרסור"2.0 בתים לבית
עמוד ה-RST הצפוף, משוכפל6.47.0
RST עוין, בלי תקרה / עם 50,00044.0 / 20.1MiB44.3 / 20.2MiB
outline של 10MB — הפרסור לבדו51MiB61.2MiB
outline של 10MB — המסלול כולו61–77MiB80.8–91.6MiB (חסם 81.2–92.0)
lines= של הבאנדל (7MB)כ-37MiB29.6MiB (חסם 29.9)
12MB, לפני #3433 / אחרי20MiB / 022MiB / 0
7MB, מתחת לתקרה14MiB14MiB
תמחור לפי ה-outline היה נותן4 חוטים (או 9 לפי הבאנדל)3 חוטים
--doubling0.56–1.73זיכרון 0.90–1.29, מעבד 0.72–1.56

מה לא שונה, בכוונה:

  • הרשומות ההיסטוריות של #3429 ושל #3433 ב-whats-new מתעדות מה נכתב אז. הרשומה החדשה אומרת שהמספרים שם נמדדו בשיטה הישנה, ומפנה לנכונים.
  • ערכים סינתטיים בטסט של המועמד (90.2, 6.4, 66.6) הם קלט לטסט, ולא טענה על מדידה.
  • ב-docstring של SUPERLINEAR_GROWTH נשאר ה-1.73: זה זמן מעבד מריצה מוקדמת של --doubling, והאיפוס לא משפיע על זמן מעבד. זה היחס הליניארי הגבוה ביותר שנצפה.

סטיות מהדרישות, ודברים שנמצאו בדרך:

  1. פיגור המונים הוא עד 372KB, ולא 128KB (דרישה 1). החסם מוכח מקוד הקרנל: ה-RSS הוא סכום של שלושה מונים, וכל אחד יכול לפגר עד 31 עמודים. ה-128KB היה רק הפיגור הגבוה שנצפה. חסם עליון חייב להיות החסם המוכח, ולכן יוצא 95.9% ולא 94.9%.
  2. הפיזור הפוך ממה שנכתב בדרישה 2 (ראו למעלה). בתיעוד כתוב הטווח שנצפה, ושהשכיחות תלויה בתוכנית הילד.
  3. אזעקת שווא שתוקנה בשורש. הריצה המלאה הראשונה עצרה בקול על hostile.md: נשארו 100KB אחרי האיפוס, והסף היה 64KB, שניחשתי מכיול. הקרנל כותב באיפוס ערך משוער, ולכן שארית כזו היא ספירת-יתר, כלומר הכיוון הבטוח. הסף נגזר עכשיו מאותו חסם של הקרנל, ויש בדיקה בקול שה-probe גדול מספיק כדי להבחין בין איפוס שקרה לאיפוס שלא קרה.
  4. הטווח בתיעוד תוקן אחרי 0955b3d. באימות מול הפלטים הגולמיים נמצא שהמינימום הוא 34,004,992 (מריצת הזוגות), ולא 34,037,760.
  5. הבאנדל ירד מכ-37 ל-29.6MiB, ואין לי הסבר. השיטה הישנה רק מורידה מהשיא, כך שהמדידה הקודמת כללה משהו אחר. הרתמה המקורית לא נשמרה.
  6. מספרי המסלול המלא נמדדו ברתמה שאינה בריפו (הסקריפט מודד רק את הפרסור), וכך כתוב לידם. התיקון השורשי שאני מציע: להוסיף את הרתמה לריפו כסקריפט, כדי שגם המספרים האלה יהיו ניתנים לבדיקה חוזרת. לא מומש בלי אישור.
  7. שלוש טענות מיושנות תוקנו באותן פסקאות: "הכלי מריץ רק rst_parser" (לא נכון מאז #3428), "הכלי מעביר לפרסר 50,000" (מאז #3420 זו ברירת המחדל של שני הפרסרים), ו"הקובץ הגדול ביותר 169KB" (היום docs/mcp-server.rst, מתחת ל-250KB).
  8. שלושה עותקים של מספר הוחלפו בהפניה לבעלים: ב-docs_handlers, ב-rst_parser ובהערות של שני טסטים, לפי prose-restates-code-fact. כך מדידה חוזרת תשנה מקום אחד.

שערים, על העץ הסופי (6f4557c):

  • טסטים: 1,192 טסטים קשורים עוברים. שני מודולים שדילגו כי חסר docutils הורצו אחר כך עם docutils, ו-10 הטסטים שלהם עוברים. 8 הדילוגים שנשארו דורשים מונגו אמיתי.
  • flake8 החוסם: 0.
  • ruff: אותם 179 ממצאים כמו ב-HEAD.
  • mypy: הצעד הקפדני נקי, הפלט הרחב זהה ל-HEAD, ו-0 ממצאי attr-defined/return-value.
  • בקומיט של השיטה (0955b3d): 51 טסטים של הסקריפט, 24/24 מוטציות, ו-189 טסטים קשורים.
  • Sphinx: בנייה לא הורצה, לפי CLAUDE.md: אין עמוד חדש, ולא נגעתי ב-toctree. הקטעים ששונו נבדקו ב-docutils ונמצאו נקיים.

דפוסים שנקראו במלואם בסבב הזה: silent-fallback-to-worse-path, state-record-without-state-change, import-time-side-effects, line-number-coupling, widened-exception-scope, prose-restates-code-fact, host-metric-in-container, external-input-isinstance, CRITICAL-PATTERNS K11, CORE-PATTERNS U3, TESTING-PATTERNS.md ו-claude-md-snippets/testing.md. ומהאתר (לפני עריכת docs/): doc-authoring, versioning-stable-anchors, style-glossary.

🔁 סבב סקירה 3 — כיסוי הזיכרון של קלטי המעבד (a154817)

Greptile — "כיסוי חסר למדידת זיכרון": נכון, תוקן ב-a154817. קלטי המעבד רצו על חמישה סידורים בלבד (range(CPU_REPEATS)), אבל הזיכרון שלהם נכנס לפסק הדין ולמועמד לקבוע — והבדיקה שמדידה רצה על כל LAYOUT_SEEDS חלה רק על המדידה המכריעה. שוחזר על 6f4557c: קלט מעבד שהזיכרון שלו עובר את התקציב רק בזרע 39 — פסק הדין עבר, ויציאה ב-0.

השורש — למה הבדיקה הקודמת לא יכלה לתפוס את זה: מדידה על פחות סידורים נמוכה בדיוק כשהסידור היקר לא הוגרל, ואז היא גם לא נבחרת כמכריעה. בדיקה שחלה רק על המכריעה לא הייתה נדלקת אף פעם על המקרה שבשבילו נכתבה.

מה השתנה:

  • קלטי המעבד רצים על כל LAYOUT_SEEDS, כמו כל מדידה שרצה כמו הכלי.
  • זמן המעבד שלהם עדיין נשפט על CPU_REPEATS הריצות הראשונות — אותם זרעים 0 עד 4 כמו קודם, כמו שהריוויוור ביקש — כי WORST_CASE_CPU_SECONDS נגזר ממקסימום על חמש ריצות לכל קלט (3ba65cc). שאר הריצות נשמרות בשורה (cpu_seconds_all), ו-cpu_timed_runs אומר כמה מהן נספרו. הנימוק כתוב ליד CPU_REPEATS.
  • verdict בודק כל מדידה כמו הכלי: md_undersampled (אילו מדידות רצו על פחות סידורים, וכמה) ו-every_input_on_every_layout, במקום decided_on_every_layout ו-md_worst_layout_runs, שהוסרו מהשורה האחרונה.
  • אף החלטת זיכרון לא זזה: על 40 סידורים, החסם העליון הגבוה מבין קלטי המעבד הוא 50.42% מהתקציב בריצה האמיתית ו-52.96% במדידה נפרדת של 21:14 (בשתיהן lazy_quote_then_table), רחוק מהקלט הגרוע. אבל הכיסוי אינו תיאורטי: בריצה האמיתית, בארבעה מחמשת הקלטים השיא הגבוה נפל בזרע שחמשת הזרעים הראשונים לא כיסו — עד 270,336 בתים יותר (ordered_to_cap, זרע 11).

אימות:

  • T2: שני הטסטים החדשים נופלים על 6f4557c — assert 0 == 1 (פסק הדין עבר על שיא שלא נמדד) ו-assert True is False (מדידה חסרה שאינה המכריעה עברה). השלישי מקבע שהמעבד נשפט על CPU_REPEATS הראשונות; על 6f4557c הוא נופל על assert 5 == 40, כי שם לא היו ריצות מאוחרות בכלל.
  • מוטציות: 9, כולן נתפסות — בהן החזרת range(CPU_REPEATS) (שבעה טסטים נופלים), בדיקה של המכריעה בלבד, מעבד על כל הריצות, מעבד על האחרונות במקום הראשונות, <= במקום <, ו-passed שמתעלם מהמדידות החסרות.
  • ריצה אמיתית מלאה של הסקריפט על a154817 (21:31–21:46 UTC): כל 38 המדידות של Markdown שרצו כמו הכלי רצו על 40 סידורים (md_undersampled ריק), וכל אחת מדדה פרסור. הזיכרון עובר: המכריע הוא עדיין הקלט הגרוע — חסם עליון 35,303,424 בתים, כלומר 95.77% (מועמד 68.95 מול 72) — וקלטי המעבד מגיעים עד 50.42%. אבל הריצה יצאה בקוד 1, בגלל המעבד — ראו את הממצא הפתוח למטה.
  • שערים: 53 טסטים של הסקריפט עוברים (73 יחד עם טסטי הטענות, התקצירים והעוגנים); flake8 החוסם — 0; ruff — בשני קובצי הפייתון שנגעתי, אותם ממצאים כמו ב-HEAD; mypy — הצעד הקפדני נקי, 0 ממצאי attr-defined/return-value, ואין שגיאות בקבצים שנגעתי. בנייה של Sphinx לא הורצה (פרוזה בעמוד קיים).
  • main מוזג לענף (557b898, רק CLAUDE.md). שתי שורות הטריגר החדשות נקראו: write-from-cached-read לא חל (**worst הוא מילון בזיכרון מתוצאת המדידה, בלי קאש ובלי כתיבה למסד), ודפוס 9 של mongodb לא נוגע כאן.

סטיות מהבקשה:

  1. הריוויוור ביקש למדוד את הזיכרון של קלטי המעבד על 40 זרעים. עשיתי את זה, ובנוסף הרחבתי את הבדיקה בפסק הדין מהמכריעה לכל מדידה — זה השורש, ובלעדיו מדידה חסרה עתידית תעבור בדיוק באותה דרך.
  2. שני שדות הוסרו מהשורה האחרונה (decided_on_every_layout, md_worst_layout_runs). אין להם צרכן בקוד — רק הטסטים, שעודכנו.

⚠️ ממצא פתוח — החלטה שלך, לא שונה כאן: 0.66 אינו חסם יציב על המכונה הזו. כדי לבדוק אם אפשר לשפוט את המעבד על כל 40 הריצות, מדדתי את חמשת קלטי המעבד על 40 זרעים (21:14–21:17 UTC, מוצמד, בלי שום דבר אחר במקביל). אותם זרעים שנמדדו ב-17:4x רצו עכשיו לאט יותר ב-15–25%: ב-lazy_quote_then_table חמש הריצות הראשונות — 0.601, 0.602, 0.676, 0.582, 0.580 (ב-17:4x: 0.448 עד 0.553); על 40 ריצות המקסימום 0.871, ו-10 מתוך 40 מעל 0.66. כלומר גם הבדיקה של היום, על חמש ריצות, הייתה נופלת בשעה הזו, בלי קשר לתיקון. והריצה האמיתית על a154817 נפלה בדיוק על זה: md_worst_cpu_seconds הוא 0.824, מ-capped:deep_quote_19_lazy בזרע 4 — ריצה בודדת (החציון שלה 0.548, והבאה אחריה 0.661); ושני קלטי מעבד עוברים את 0.66 כבר בחמש הריצות הראשונות שלהם (0.669 ו-0.738). פסק הדין שופט את המעבד על בדיוק אותן ריצות כמו לפני התיקון, כך שהנפילה אינה ממנו. (בזמן הריצה הזו הרצתי במקביל פקודות קלות — git וקריאות API — שאולי הוסיפו רעש; המדידה של 21:14 רצה לבד, ומראה את אותה תמונה.) ויש כאן גם חוסר עקביות שקיים מסבב 2: שורות הזיכרון נשפטות במעבד על כל 40 הריצות, וקלטי המעבד על חמש. מגבלת הקצב (45) והדדליין של read_batch נגזרים מ-0.66, ולכן זו החלטה ולא תיקון:

  • (א) — ההמלצה שלי: לשפוט את המעבד על כל 40 הריצות בכל השורות (עקבי עם הזיכרון), ולגזור מחדש את WORST_CASE_CPU_SECONDS כחסם עליון על הפיזור, ממדידות בכמה שעות שונות — כמו שעשינו לזיכרון. עם הגבוה שנצפה היום (0.871), מגבלת הקצב שעוד שומרת על "דקת מעבד בדקה" יורדת ל-34 (30 ÷ 0.871), והדדליין נשאר בתוך 60 (10 + 17.4 = 27.4).
  • (ב) להשאיר חמש ריצות ו-0.66, ולכתוב ליד הקבוע שזמן המעבד על המכונה המודדת משתנה בעשרות אחוזים במהלך היום, ושפסק דין שנופל על המעבד בשעה איטית אינו בהכרח שינוי בקוד.

🔁 סבב סקירה 4 — קרנל מוכח, וטענת הקצב של תור הכתיבה (5baaeed)

CodeRabbit — "יש להוסיף בדיקת תאימות ליכולות הקרנל": נכון, תוקן. הנוסחה של counter_lag_bound_bytes נגזרה מקוד הקרנל, ובדקתי עכשיו במקור לפי גרסה על אילו גרסאות היא באמת חסם:

  • מ-v6.2 mm->rss_stat הוא percpu_counter, והשיא מתעדכן מ-get_mm_rss, שקורא אותו משוער. ב-v6.1 אלה מונים אטומיים, ולצידם (כשמוגדר SPLIT_RSS_COUNTING) מטמון לכל חוט שמסונכרן כל 64 אירועים — מודל אחר, שהנוסחה אינה חלה עליו.
  • מ-v6.16 task_mem קורא את VmRSS ב-get_mm_counter_sum, כלומר מדויק. ב-v6.2, v6.6, v6.11, v6.12, v6.14 ו-v6.15 הוא קורא ב-get_mm_counter (ו-get_mm_counter_sum אינו קיים ב-mm.h של v6.2), משוער גם הוא — ואז גם ה-RSS שלפני הפרסור סוטה, והשיא יכול לחסור עד פי שניים מהחסם.
  • נבדק ב-v6.16, ב-v6.18 וב-master: compute_batch_value (max(32, 2 × המעבדים)), percpu_counter_add_batch, והאיפוס של clear_refs — כמו שהנוסחה מניחה.

מה השתנה: counter_lag_bound_bytes מתחיל ב-_require_a_kernel_the_lag_bound_is_proven_on — לינוקס בלבד (גם ל-macOS יש release, ו-23.1.0 גדול מ-6.16), גרסה שנקראת מ-os.uname().release, ורצפה של LAG_BOUND_PROVEN_FROM_KERNEL = (6, 16). אחרת SystemExit, לפני הילד הראשון. כל שימוש בחסם עובר בפונקציה הזו, כך שגם פסק הדין וגם --doubling מוגנים, והעצירות הקיימות נשארו כמו שהן.

למה גרסה ולא בדיקה של היכולת עצמה (הסטייה היחידה מההצעה): המונים הם מבנה פנימי של הקרנל, ואין ממשק שחושף אותם. בדיקה אמפירית — השוואה של VmRSS לספירה אחרת — עוברת במקרה בכל רגע שבו מה שמחכה אצל המעבד הוא אפס, כי אז הקריאה המשוערת שווה בדיוק לסכום. רצפת גרסה לעולם אינה מקבלת קרנל ששני הדברים חסרים בו, כי הם ב-upstream מאז; לקרנל של הפצה עם מספר נמוך שקיבל אותם ב-backport היא מסרבת — וזה הכיוון הבטוח. הנימוק כתוב ליד הקבוע.

איפה הבדיקה עוברת היום: כאן (6.18.44 — החסם שנמדד לא השתנה, 380,928 בתים), ובתמונה של runner ה-ubuntu-24.04 של GitHub (6.17.0-1022-azure לפי ה-README של actions/runner-images) — שם ה-CI מריץ את הטסטים עם ילד אמיתי, ולכן הם ממשיכים לרוץ.

CodeRabbit — "צמצמו את הטענה על קצב התרוקנות התור": נכון, תוקן. "התור מתרוקן הרבה מעל המגבלה" לא נבע מהמדגם: 30 השורות מראות שבעומס של אותם ימים התור לא הצטבר, אבל לא באיזה קצב הגיעו הכתיבות. עובד אחד עומד בקצב של המגבלה רק כשגוף כתיבה נמשך בממוצע פחות מ-60 חלקי DEFAULT_RATE_LIMIT_PER_MINUTE שניות — 1.33 ב-45 — והגוף האיטי במדגם הוא 1.993. עכשיו ההערה ב-limits.py אומרת את התנאי, מה נמדד, ומה המדגם אינו מראה. הטענה הייתה ב-main עם "60 בדקה" (#3443), וה-PR הזה רק החליף את המספר ב"המגבלה".

אימות:

  • T2: עשרה טסטים חדשים — תשע גרסאות קרנל (ארבע מוכחות, חמש לא), ועצירה לפני כל ילד על קרנל לא מוכח. על a154817 שישה מהם נופלים (DID NOT RAISE <class 'SystemExit'>, ו-"a measuring child was started").
  • מוטציות: שש, כולן נתפסות — בלי הבדיקה, <= במקום <, בלי בדיקת לינוקס, רצפה ב-6.2, השוואה של המספר הראשון בלבד, וגרסה שאי אפשר לקרוא שעוברת.
  • ילד אמיתי על הקרנל הזה: עשרת הטסטים שמריצים ילד אמיתי עוברים.
  • שערים: 127 טסטים עוברים (הסקריפט, limits, הטענות, התקצירים והעוגנים); flake8 החוסם — 0; ruff — בשלושת קובצי הפייתון שנגעתי, אותם ממצאים כמו ב-HEAD; mypy — הצעד הקפדני נקי, הפלט הרחב 1,598 כמו קודם, 0 ממצאי attr-defined/return-value, ואין שגיאות בקבצים שנגעתי.
  • לא אומת: קרנל ישן אמיתי — אין כאן כזה; מסלול הסירוב נבדק עם os.uname מזויף. וגם קרנל חדש מה-master שנקרא (2026-09-27): הרצפה מקבלת אותו בלי שהקוד שלו נקרא, ואין בדיקה שתיפול בוודאות אם קרנל עתידי ישנה את ספירת המונים — כך כתוב גם ליד הקבוע.

דפוסים שנקראו במלואם — תיקון: בגרסה הקודמת של הסעיף הזה כתבתי "בסבב הזה" על כולם, וזה לא מדויק. בסבב הזה, לפני התיקון, נקראו CORE-PATTERNS U3, external-input-isinstance, host-metric-in-container ו-silent-fallback-to-worse-path. את prose-restates-code-fact, line-number-coupling, widened-exception-scope, TESTING-PATTERNS.md ו-claude-md-snippets/testing.md קראתי במלואם בסבב 3, והם היו לפניי בזמן העבודה, אבל לא קראתי אותם מחדש בסבב הזה. אחרי הדחיפה קראתי את כולם שוב, מהגרסה העדכנית של amir-bug-patterns (75c2793), יחד עם RECURRING-PATTERNS R6 (הפונקציה החדשה מפרסרת גרסת קרנל, ואין לזה עותק אחר בריפו), CRITICAL-PATTERNS K13 ו-secret-in-derived-text (בהודעות העצירה יש רק release ו-sysname, לא סוד), ו-linter-fix-changes-runtime-behavior — ובדקתי מולם את הדיף של 5baaeed: אין ממצא. ומהאתר: doc-authoring, versioning-stable-anchors ו-style-glossary נקראו בסבב 3, לפני העריכה של scripts.rst שם, ולא מחדש לפני העריכה בסבב הזה; אחרי הדחיפה קראתי אותם שוב — אין ממצא.

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

  • Revert של הקומיט מחזיר את הפרסור לתקרת הכותרות בלבד ואת מגבלת הקצב ל-60. אין מיגרציה ואין שינוי במסד. להקלה בלי revert: MCP_RATE_LIMIT_PER_MINUTE ב-Render מחזיר מגבלה אחרת; את התקרות עצמן אפשר להזיז רק בקוד (והסקריפט בודק אותן מחדש).

🤖 Generated with Claude Code

https://claude.ai/code/session_01GQfdgfwKvZ9XNtLPs88DJf


Generated by Claude Code

Review in cubic