Skip to content

sefariasqlite: מיזוג קטגוריות רקורסיבי, וסתירת תיאורים נפתרת כש־ForDB מכריע - #137

Closed
YOSEFTT wants to merge 606 commits into
kdroidFilter:masterfrom
YOSEFTT:otzaria
Closed

YOSEFTT wants to merge 606 commits into
kdroidFilter:masterfrom
YOSEFTT:otzaria

Conversation

@YOSEFTT

@YOSEFTT YOSEFTT commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

הבעיה

הקומיט be013e77 ב־otzaria-library הוסיף כלל ל־category_renames.csv: מילונים וספרי יעץ,ספרות עזר. מטרתו לאחד את קטגוריית העזר של ספריא עם זו של אוצריא.

בבנייה האמיתית זו הצמדת שם פשוטה. renameCategories רץ לפני ששלב אוצריא מצטרף, ושם עדיין אין «ספרות עזר» בשורש. אבל שער update-fordb מריץ את אותם כללים על ה־DB המשוחרר, ושם שני העצים כבר קיימים, ולכן הכלל הופך למיזוג. השער נכשל (run 40520029742):

[category_renames.csv] 'מילונים וספרי יעץ' -> 'ספרות עזר': Cannot merge category id=1158 into id=1185: conflicting heShortDesc values

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

מאחורי הכשל הזה מסתתר כשל שני. המיזוג העביר את תת־הקטגוריות של המקור («דקדוק», «מילונים», «אנציקלופדיות») ליעד, ושם יש כבר קטגוריות באותם שמות. getAllCategoryDescriptionRows נכשל על נתיב קנוני כפול, כך שפתרון סתירת התיאורים לבדו לא היה מעביר את השער.

השינוי

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

סתירת תיאורים (resolveMergedDescriptions): עד היום כל סתירה הכשילה את המיזוג. עכשיו סתירה בשדה נפתרת לטובת היעד רק כש־sefaria_category_changes.csv מחליף או מוחק את השדה הזה בנתיב הקנוני של היעד. במקרה כזה seedAllMetadata דורס את הערך ממילא, כך ששום תיאור לא הולך לאיבוד. בכל מקרה אחר, כולל שורה עם תא ריק (Keep) או שורה לנתיב אחר, המיזוג נכשל כמו קודם. ברירת המחדל של הפרמטר היא מפה ריקה, כלומר ההתנהגות הקפדנית.

חיווט: RenameCategoriesPostProcess (ה־preflight והמעבר האמיתי) ו־ValidateForDbInputs (שני המעברים) מעבירים את שורות sefaria_category_changes.csv. הקובץ כבר נטען ונבדק בשניהם, ועכשיו נעשה בו גם שימוש.

אין שינוי סכמה ואין צורך ביצירה מחדש של DB.

מה זה פותר במקרה הנוכחי

ב־DB המשוחרר יש סתירות בשתי רמות: הרמה העליונה ו«מילונים» (heDesc). לשתיהן יש שורת החלפה ב־CSV של otzaria-library. «דקדוק» ו«אנציקלופדיות» זהים בשני העצים. לכן, אחרי המיזוג הזה, הכלל אמור לעבור את השער בלי שינוי נוסף ב־otzaria-library.

בדיקות

נוספו חמש בדיקות ל־RenameCategoryDescriptionsTest:

  • סתירה ש־ForDB מכריע (Replace/Clear) שומרת את ערך היעד.
  • סתירה לא נפתרת על ידי Keep או על ידי שורה לנתיב אחר.
  • מיזוג רקורסיבי: תת־קטגוריות מתמזגות, ילדים ייחודיים עוברים, ספרים בכל רמה עוברים, ואין נתיב כפול.
  • סתירה ברמה פנימית נכשלת לפני שמשהו משתנה.
  • תרחיש השער עצמו דרך collectForDbRuleFailures: נכשל כשרק הרמה העליונה מוכרעת, ועובר כששתי הרמות מוכרעות.

הבדיקות הקיימות שמכשילות סתירה לא מוכרעת נשארו בלי שינוי, ועוברות.

./gradlew :sefariasqlite:jvmTest --tests '*RenameCategoryDescriptionsTest' --tests '*ValidateForDbInputsTest' --tests '*ForDbTitleGateTest'
./gradlew :sefariasqlite:jvmTest

לא הרצתי מקומית את validateForDbInputs מול ה־DB המשוחרר, כי זו הורדה של כמה GB. אחרי המיזוג, ריצה חוזרת של update-fordb ב־otzaria-library (workflow_dispatch או הריצה היומית) תאמת את זה.

🤖 Generated with Claude Code

המשך דברי קלוד מהשיחה שלי איתו:
בדיקות:

הבדיקות הרלוונטיות: כל 22 עוברות, כולל 5 החדשות.
כל המודול: 748 בדיקות, 20 נכשלות. הרצתי את חמש המחלקות הנכשלות גם על קוד הבסיס (88c2067), ושם נכשלות אותן 20 בדיוק. לכן הן לא קשורות לשינוי. לפי הודעות השגיאה הן נראות כבעיות סביבת Windows: קידוד של לוג תהליך־בן, נתיבים עם , וקובץ WAL נעול.
הבדיקה שעדיין חסרה: השער ב־otzaria-library מושך את Otzaria/SeforimLibrary מענף otzaria, ולא את ה־fork. לכן update-fordb ימשיך להיכשל עד שה־PR ימוזג שם. אחרי המיזוג, הפעלה ידנית של update-fordb (או הריצה היומית) תאמת שהכלל עובר. את validateForDbInputs מול ה־DB המשוחרר לא הרצתי מקומית, בגלל הורדה של כמה GB.

Y-PLONI and others added 30 commits September 30, 2026 17:45
…ria-fork

כינויים: הורדה מהפורק Otzaria/SeforimAcronymizer
שלב חדש, compressLineContent, רץ אחרי Phase-2 (הקורא האחרון של טקסט רגיל) ולפני compactSeforimDb. הוא דוחס כל line_content.content וכל version_line.content שאינו NULL למסגרת zstd אחת (רמה 19, בלי checksum, עם גודל תוכן ו-dictID) עם המילון שב-resources/zstd/line_content.zdict, ושומר אותו בטבלה zstd_dict. כל מסגרת מפוענחת ומושווית לפני הכתיבה; שורה שכבר BLOB מדולגת, כך שריצה שנקטעה ממשיכה. בסוף ה-DB מוחתם כסכמה 7.

המילון (2MB, fastcover k=2000 d=8) אומן על 200K שורות אקראיות של v30. על 50K שורות שלא באימון: בלי מילון 2.54x, עם המילון 3.80x. על v30 המלא: line_content מ-5,426MB ל-1,391MB, ה-DB כולו מ-7.56GiB ל-3.49GiB אחרי compact; ההורדה (zst של הקובץ) גדלה בכ-320MB. המילון, גרסת zstd-jni והרמה קפואים: טסט golden נועל את בתי המסגרות, ו-producer מסרב לייצר patch כשהמילון השתנה (UnpatchableAnchorException), כי לקוח לא יוכל לפענח אותו.

חוזה: CURRENT_DB_SCHEMA_VERSION = 7, ‏zstd_dict בטבלאות ה-patch וב-hash מיד אחרי line_content, ‏FULL_REBASE_SCHEMA_VERSIONS = [6, 7] גם ב-patch_fan_lib.sh וב-patch_anchor_schema.py (עוגן בסכמה 6 מקבל מחסום). stampSchemaVersion מחתים כברירת מחדל 6 (הצורה שלפני הדחיסה), סכמה 7 דורשת שכל השורות מסגרות, וסכמה נמוכה יותר מסרבת למסד דחוס. סכמה 6 נשמרת קפואה בחוזים.

קוראים של טקסט רגיל: DumpLines ו-generateLinkerLinks נכשלים במפורש על מסד דחוס; ההכרזה על עדכון ספרייה מפענחת דרך zstandard; build-library-index נכשל כשהאפליקציה שבנויה לא קוראת את סכמת ה-DB. צעד הדחיסה ב-workflow מדלג כש-source_commit קודם לו (ה-DB נשאר בסכמה 6). scripts/zstd/train_line_content_dict.py משחזר את אימון המילון.
סכמה 6 עוד לא פורסמה (v30 היה pre-release לבדיקה בלבד, ואין גרסת אפליקציה רשמית שקוראת אותה), ולכן אין צורך בסכמה חדשה. zstd_dict מצטרף לחוזה של סכמה 6: בטבלאות ה-patch וב-hash מיד אחרי line_content. CURRENT_DB_SCHEMA_VERSION נשאר 6, המחסום של 7 בוטל, וסקריפטי ה-fan חזרו לצורתם.

הדחיסה היא תכונה של ה-DB בתוך סכמה 6: מסד עם zstd_dict דחוס, ומסד בלעדיה רגיל. compressLineContent לא מחתים מחדש; stampSchemaVersion דורש שבמסד דחוס כל השורות יהיו מסגרות. הקוראים כבר מכריעים לפי zstd_dict, ובניית האינדקס בודקת עכשיו שלאפליקציה יש LineContentCodec כשהמסד דחוס. לקוח על מסד רגיל (v30) מקבל הורדה מלאה, כי ה-producer מסרב ל-patch בין מסד רגיל לדחוס.

logical_hash_contract.json נוצר מחדש ב-updater וזהה בבתים.
שורה ארוכה מ-16MB (התקרה של LineContentCodec באפליקציה) מכשילה עכשיו את הדחיסה בבנייה, במקום לצאת למסד שהאפליקציה תסרב לפענח. ההערה על zstd_dict ב-PatchDbProducer תוקנה (הטבלה ברשימת ה-patch, אבל לעולם לא משתנה דרך patch), וההערה על סדר ה-hash של סכמה 6 מסבירה שהטבלה נוספה לפני שהסכמה שוחררה.
validateCompressed בדק רק שה-BLOB מתחיל ב-28 B5 2F FD. לכן מסגרת קטועה, מסגרת של מילון אחר, מסגרת שמתפענחת ליותר מ-16MiB או ל-UTF-8 לא תקין עברו את הדחיסה, את ה-resume ואת ה-stamp, ונכשלו רק באפליקציה.

עכשיו האימות מוודא שבמסד רק המילון המאושר, ומפענח כל שורה ב-line_content וכל שורה שאינה NULL ב-version_line: dictID של המסגרת הוא של המילון, גודל התוכן ידוע ועד תקרת הקורא, המסגרת היא בדיוק מסגרת אחת, והפענוח הוא UTF-8 תקין (בלי החלפה). שורה פגומה מכשילה עם שם הטבלה והמזהה שלה. הקריאה במנות של 20,000 שורות, והפענוח מקבילי עם DDict אחד משותף.

טסטים: מסגרת של ארבעת בתי ה-magic בלבד, dictID אחר ב-version_line, פענוח מעל התקרה, UTF-8 לא תקין, ו-resume על BLOB פגום שכבר היה במסד. כל החמישה נכשלו לפני התיקון.
כל worker יצר ZstdDictCompress משלו. עם המילון הזה CDict עולה כ-50MiB, ולכן הזיכרון הנייטיב גדל עם מספר ה-threads. CDict ו-DDict אינם משתנים ובטוחים לשימוש מכמה threads, ולכן עכשיו נוצר אחד מכל סוג לריצה, ולכל thread נשאר הקונטקסט שלו.

הפורמט לא השתנה: טסט חדש משווה את המסגרות שהריצה המקבילית כותבת למסד ל-hash הקפוא. על דגימה של 1,045,997 שורות אמיתיות מ-v30 כל המסגרות זהות בבתים לאלה של הקוד הקודם. ב-32 threads הזיכרון הפרטי בשיא ירד מ-3,257MiB ל-1,495MiB.
הסקריפט אינו משחזר את המילון הקפוא. ליד הסקריפט מתועד מה שנרשם בקומיט ובתיאור ה-PR (2MB, fastcover k=2000 d=8, ‏200K שורות של v30, רמה 19), מה שלא נרשם (קובץ ה-v30, גרסאות, f, accel, seed), ותוצאת הרצה של הסקריפט על v30 מקומי, שנתנה מילון אחר. המילון עצמו לא שונה.
רק בתים 4–7 בכותרת המילון (המזהה, little-endian) נכתבו מחדש; שאר 2,097,148 הבתים זהים, וכך גם גוף כל מסגרת. מזהה בטווח 32768..65535 תופס שדה Dictionary_ID של 2 בתים בכותרת כל מסגרת במקום 4 – כ-14.6MB על 7.3M מסגרות. מזהים עד 32767 ומ-2^31 שמורים (zdict.h).

DICT_SHA256 ו-FROZEN_FRAMES_SHA256 עודכנו, בדיקה חדשה מוודאת שהמזהה בטווח ושהמסגרת מסמנת שדה של 2 בתים, ותיעוד המקור בסקריפט האימון מתאר את ההחלפה. הסקריפט מאמן מעתה עם dict_id=32768.
…td-dict

סכמת DB 6: דחיסת טקסט השורות ב-zstd עם מילון קבוע
השלב Stage release assets מפצל את ה-DB המלא רק כשהוא גדול מ-1.9 GiB, לחלקים של 1900 MiB ולצדם מניפסט בצורת split_release_asset.sh (גודל ו-sha256 לכל חלק ולשלם). מתחת לסף הנכס נשאר קובץ אחד כמו היום.

db_asset_names.sh: FULL_DB_ASSET_JQ מזהה גם מניפסט פיצול ומחזיר את שם הארכיון, ונוספו stage_full_db, download_full_db_by_name, split_full_db_meta ו-stream_full_db. ה-prefetch וה-fan של העוגנים, בניית אינדקס הספרייה וההכרזה עוברים דרכם, ו-validate_build_provenance מקבל את ה-DB כקובץ יחיד או כמניפסט וחלקים, אבל לא את שניהם.

assemble_split_asset.sh הועתק מאוצריא כדי לחבר ולאמת כאן. נוספה test_split_full_db לשתי רשימות הסוויטות.
השלב Build both catalogs מזרים עכשיו את ה-DB דרך stream_full_db מתוך FULL_DB_ASSET_JQ, והבדיקות עוד חיפשו את gh release download -p ... -O - הישן. הן בודקות כעת את אותן ערבויות בצורה החדשה: הנכס המלא נבחר לפי סכמת כל גרסה, ה-DB מוזרם בלי עותק zst על הדיסק, ו-zstd מקבל --long=31 --memory=2048MB.

ה-gh המדומה דוחה הורדה שאינה ל-stdout או בלי -R, ה-zstd המדומה דוחה הרצה בלי הדגלים, ותיקיית העבודה נשארת עם שני קובצי ה-JSON בלבד. נוספה בדיקה לנכס מפוצל: המניפסט והחלקים מוזרמים לפי הסדר.
DB מלא מעל מגבלת GitHub מתפרסם בחלקים, וכל הצרכנים מחברים אותו
…ים דחוסים

טסט ה-golden של CompressLineContentCliTest כותב עכשיו גם את המסגרות עצמן: zstd/line_content_golden_frames.json מחזיק את הטקסט, המסגרת (base64), ה-dictID, ה-SHA-256 של המילון והרמה, והטסט נכשל אם הקובץ אינו זהה בבתים למה שהדוחס מייצר. האפליקציה מפענחת את אותו וקטור עם המילון האמיתי, ולכן הוא חוזה בין שני המימושים ולא רק hash.

delta-real-diff-test.yml (וממנו גם delta-pipeline-dryrun ו-delta-real-diff-arm) מריץ compressLineContent על v1 ועל v2 לפני producePatchAndVerify, כך שה-patch ובדיקת ה-hash שלו הם דחוס→דחוס. צעד נוסף מוודא שבשני המסדים יש מילון אחד וזהה ושכל שורה ב-line_content וב-version_line היא מסגרת zstd. סיכום ה-patch מונה גם line_content ו-zstd_dict.

test_ci_workflows מריץ את גוף צעד הדחיסה מול gradle מדומה, ואת צעד הבדיקה מול מסדי SQLite אמיתיים: שני מסדים דחוסים עוברים, ושורת טקסט, BLOB שאינו מסגרת, מילון שונה, שני מילונים או מסד בלי zstd_dict נכשלים.
…std-vectors

בדיקות דחיסת השורות: וקטור golden לאפליקציה, ודלתא דחוס→דחוס ב-workflows
renameCategories יצר את תיקיית היעד של book_moves.csv עם rowid משתמע, MAX(id)+1. זה המזהה הבא אחרי קטגוריות ספריא, וה-build state כבר שומר אותו לתיקייה של אוצריא. ב-v30 התיקייה החדשה 'תלמוד בבלי/כללי הש״ס' קיבלה 1478, שהוא המזהה של 'אור הישר/סדר נשים'. הבדיקה שאחרי ההכנסה ב-upsertCategory תפסה את זה, ושלב אוצריא העביר את סדר נשים ל-1526. ensureCounterAtLeast לא עוזר כאן, כי הוא מגן רק על מזהים חדשים ולא על מזהה שכבר שמור. אותו פגם גרם ב-db_version=19 לשיבוץ השגוי של 26 ספרים.

זה יחזור בכל תיקיית יעד חדשה: המזהה הבא, 1479, שמור ל'פסקי רבינו מענדל קלויזנער'. גם מזהי התיקיות הקיימות לא היו יציבים. הם זזו כשספריא הוסיפה קטגוריה, וכששורות ב-book_moves.csv נוספו, נמחקו או שינו סדר.

עכשיו renameCategories מצרף (ATTACH) את ה-build state, כמו synthesizeSeifimAltToc, ותיקיית יעד מקבלת מזהה לפי המפתח 'book_moves:<נתיב>' ב-id_lookup. מרחב המפתחות נפרד מזה של ספריא ושל אוצריא. מפתח של ספריא הוא הנתיב לפני שינוי השם, ולאוצריא כבר יש מפתח לנתיב 'מחשבת ישראל/אחרונים/רמחל' עם מזהה אחר (1322), כך ששיתוף מרחב המפתחות היה ממספר מחדש קטגוריה חיה. בבנייה הראשונה, שבע התיקיות שכבר פורסמו מקבלות את המזהים שהן פורסמו איתם (1472-1478), בתנאי שאף מפתח ואף שורה ב-DB לא תופסים אותם. תיקייה חדשה מקבלת מזהה חדש מעל המונה ומעל ה-DB. שורות המזהים נכנסות באותה טרנזקציה עם ה-DB, ולכן גם בהן יש rollback, וה-BuildStateVerifier רץ בסוף.

הבדיקה במנגנון ה-dry-run של ValidateForDbInputs לא השתנתה: הוא עובד בלי build state ועושה rollback.

בדיקות: BookMoveLeafIdsTest מריץ את שלושת השלבים (ספריא, renameCategories, אוצריא) על קבצים אמיתיים. הוא משחזר את ההתנגשות של v30 ומראה שבלי התיקון היא מתרחשת. הוא מוודא שהמזהים נשמרים בשלוש בניות רצופות גם כשעץ ספריא גדל וסדר השורות משתנה, ושהמזהים האמיתיים של v30 נשמרים.
ב-v30 התיקייה 'תלמוד בבלי/כללי הש״ס' תפסה את 1478, ושלב אוצריא העביר את סדר נשים ל-1526. ב-v28, הגרסה היציבה, וגם ב-v29, סדר נשים נמצא ב-1478. האפליקציה שומרת לפי (שם הספר, categoryId) הגדרות לכל ספר, ספרים מוסתרים, טיוטות הערות וקישורי משתמש. גם כרטיסיות, סימניות והיסטוריה שמורות עם categoryId, ובספר שנפתח מהן לוח המפרשים והקישורים יוצא ריק כשהמזהה השתנה. אם 1526 יישאר, כל מי שיעבור מ-v28 יאבד את כל אלה בשבעת הספרים.

כללי הש״ס הופיעה לראשונה רק ב-v30, שהוא pre-release, ולכן המזהה שלה לא משנה דבר למשתמשי v28. הספרים שבה עברו אליה מתיקיות אחרות בכל מקרה. לכן היא יוצאת מ-PUBLISHED_BOOK_MOVE_LEAF_IDS ומקבלת מזהה חדש (1536). רק מי שהתקין את v30 יראה שינוי נוסף ב-14 ספרים.

CATEGORY_ID_RESTORES מחזיר את המפתח של סדר נשים ב-build state מ-1526 ל-1478. זה קורה לפני שנוצרת תיקיית יעד כלשהי, ורק כל עוד המפתח עדיין על 1526 ואף מפתח או שורה ב-DB לא תופסים את 1478. לכן זה רץ פעם אחת, ובבניות הבאות לא עושה כלום. המזהה 1526 נשאר פנוי. מזהים חדשים רק עולים, ולכן הוא לא יחולק שוב.

בדיקות: בבנייה הראשונה ממצב של v30, שש התיקיות נשארות ב-1472-1477, סדר נשים ב-1478 וכללי הש״ס ב-1536, בלי שום הקצאה מחדש בשלב אוצריא. בבנייה השנייה כל המזהים נשמרים. כשהמזהה 1478 תפוס, השחזור לא רץ.
תיקיית יעד של book_moves.csv יכולה להחזיק גם ספרים של אוצריא. היום זה המצב ב-4 מתוך 7 התיקיות: 1472, 1473, 1476 ו-1536. לתיקייה כזו יש שני מפתחות ב-build state. renameCategories משתמש במפתח 'book_moves:<נתיב>', ושלב אוצריא משתמש במפתח של הנתיב עצמו. אף שלב לא הסתכל על המפתח של השלב השני, ולכן המזהה של התיקייה השתנה כששורה נוספה או נמחקה:
- הוספה: כששורה ראשונה מכוונת לתיקייה שהייתה של אוצריא בלבד, התיקייה נוצרה במזהה חדש. למשל יד דוד הייתה עוברת מ-1499 למזהה חדש.
- מחיקה: כשנמחקה השורה האחרונה לתיקייה משותפת, שלב אוצריא יצר אותה מחדש לפי המפתח שלו. למשל רמחל הייתה חוזרת מ-1476 ל-1322, שעדיין שמור ב-build state.
האפליקציה שומרת לפי (שם הספר, categoryId) הגדרות לכל ספר, ספרים מוסתרים, טיוטות הערות, קישורי משתמש וכרטיסיות. כל שינוי כזה היה מוחק אותם לספרי אוצריא שבתיקייה.

התיקון מחזיק את שני המפתחות על אותו מזהה:
- renameCategories: מפתח חדש לתיקיית יעד מקבל קודם את המזהה שפורסם, כמו קודם. אם אין כזה, הוא מקבל את המזהה שהמפתח של אוצריא שומר לאותה תיקייה. זה קורה רק כשאף שורה ב-DB לא תופסת את המזהה, וכל מפתח שמחזיק אותו שייך לאותה תיקייה. אחרת התיקייה מקבלת מזהה חדש, כמו קודם.
- שלב אוצריא: כשהוא מוצא תיקיית book_moves קיימת, הוא מצמיד אליה את המפתח שלו (alignWithBookMoveLeaf). כשהוא יוצר תיקייה שהייתה תיקיית book_moves, הוא מחזיר לה את המזהה הזה (upsertOtzariaCategory). מפתח שהמזהה שלו תפוס בידי קטגוריה חיה אחרת לא זז, כי מפתחות של ספריא חולקים את אותו מרחב.
"אותה תיקייה" נקבע באותה השוואה ש-findExistingCategory עושה, בלי גרשיים וסיומות "על התורה" וכדומה. לשם כך הפונקציות normalizeHebrewLabel ו-comparableLabel עברו כמו שהן מ-Generator ל-CategoryLabels ב-generator-common. גם BOOK_MOVE_LEAF_KEY_PREFIX עבר לשם.

בדיקות: ב-BookMoveLeafIdsTest יש עכשיו את שני השחזורים של ה-QA (הוספה ומחיקה). יש גם בדיקה של רמחל מול המפתח הישן 1322, בדיקה של כתיב גרשיים שונה, שתי בדיקות שמוודאות שהמזהה לא נלקח כשהוא תפוס, וסימולציה של 7 בניות שבהן שורות נוספות, נמחקות ומשנות סדר. בלי התיקון חמש מהבדיקות נכשלות (6 -> 8, 1476 -> 1322, 6 -> 11), ועם התיקון כולן עוברות.
הרצה אמיתית: ארבע בניות רצופות, כל אחת עם renameCategories ו-appendOtzariaLines, על seed של v30 עם ה-build state של v30. בבנייה הראשונה כל הקטגוריות זהות ל-v30, חוץ מסדר נשים (1478) וכללי הש״ס (1536). בבנייה השנייה נמחקו שורות רמחל, נוספה שורה ליד דוד והסדר הפוך. בשלישית נמחקו שורות שות מהרשם. ברביעית חזרו כל השורות. בכל הבניות אף מזהה לא זז. בלי התיקון, באותן בניות, רמחל עברה ל-1322, יד דוד ל-1537 ושות מהרשם ל-1538.
… שלו

במבני 'פרקים' שעוברים בירושה למפרשים, כותרות דף נתלו בפרק הלא נכון או לפני עוגן הפרק. בחידושי אנשי שם על שבת, דף ד. עד ז: הופיעו תחת 'במה מדליקין', ופרק 'יציאות השבת' חסר. בקרן לדוד על שבת, דף קלה. ודף קנג: הופיעו תחת 'במה מדליקין'. בהגהות הב"ח על רי"ף מועד קטן, לחיצה על הפרק הראשון דילגה על 33 שורות. במסד v30 זה קרה ב-178 מתוך 544 מבנים.

היו לזה ארבע סיבות:
- כל כותרת שמעל העוגן הראשון נתלתה בפרק הראשון שהוצב (coerceAtLeast(0)), גם כשזה לא פרק 1 של הבסיס. כך זה קרה גם לכותרת שהושמטה מהעיגון כי אינה לפי הסדר.
- פרק שאין אליו קישור לא הוצב כלל, והדפים שלו עברו לפרק הקודם או לפרק הבא.
- כותרת דף שצמודה מעל השורה המקושרת הראשונה של פרק נרשמה בפרק הקודם. כך היה ב-643 גבולות.
- בלי תורם פגינציה מפורש לא נקראו כותרות הדף לפי העמוד שלהן, גם במפרשי רי"ף שכותרותיהם הן דפי הרי"ף.

התיקון:
- הפגינציה של בסיס הקישורים עצמו משמשת כשהקישורים מוכיחים אותה. צריך לפחות שני קישורים, ותשעה מכל עשרה נופלים על העמוד של כותרת הדף שמעליהם. קישור שסותר את טווחי הפרקים פוסל אותה, כמו את התורם. רק תורם מפורש מציב פרקים מכותרות דף בלבד, ולכן הבחירה בין קישורים לכותרות לא השתנתה.
- עם פגינציה, פרק שאין אליו קישור נפתח בכותרת הדף הראשונה שבטווח העמודים שלו, בפער שבין הפרקים. כותרת שמעבר לטווח הפרק היא הפניה, והפרק לא נפתח בה.
- כותרת דף שצמודה מעל השורה המקושרת הראשונה פותחת את הפרק, כי אין תחתיה שורה של הפרק הקודם. עם פגינציה זה קורה רק כשהיא לא שייכת לפרק הקודם.
- layoutChapters ממקם את הכותרות שמעל העוגן הראשון. עם פגינציה כל כותרת הולכת לפרק של העמוד שלה. כותרת שאינה לפי הסדר פותחת את הפרק שלה במקומה, או מצטרפת אליו. בלי פגינציה הכותרות פותחות את הפרק הראשון, אם הוא פרק 1 של הבסיס. אחרת אין דרך לדעת לאיזה פרק הן שייכות, והספר נשאר בלי מבנה (unplaced-leading-dafs). עוגן של פרק לעולם לא נמצא מתחת לדף הראשון שלו.

אימות: הרצתי את השלב על עותק של הטבלאות מ-v30, בלי המבנים שעברו בירושה. לפני התיקון הפלט זהה ל-v30, כולל line_alt_toc, וגם הלוג זהה (LINKS=332, TITLE=212). הבודק שהשתמשתי בו פוסל שלושה מצבים: כותרת דף מחוץ לטווח העמודים של הפרק שלה, לפי הפגינציה שמתאימה הכי טוב מבין בסיסי הקבוצה; ילד מעל העוגן של הפרק שלו; וכותרת דף שפותחת את הפרק הבא אבל רשומה בפרק הקודם. לפני התיקון היו 178 ספרים פגומים עם 1,422 מופעים. אחרי התיקון נשארו 3 ספרים עם 8 מופעים, ובכולם הקישורים עצמם סותרים את הפגינציה. 366 הספרים שהיו תקינים לא השתנו בכלל, לא במבנה ולא ב-line_alt_toc. כל כותרת דף עדיין מופיעה פעם אחת. ב-24 ספרים נוספו 42 פרקים. שני ספרים זעירים, הגהות הב"ח על רי"ף מכות והגהות מאלפס ישן על ביצה, נשארים בלי מבנה. 149 כותרות שאינן לפי הסדר, בתוך הטווח של פרק (הפניות בקובץ שיטות קמאי, וסדרה שנייה בחידושי חתם סופר), נשארות לפי השורה, כמו קודם.
…יו סותרים אותו אינו בסיס

תיקון לשלושה ליקויים שנמצאו בבדיקה של 88a8103, ולשניים שהיו קודם לכן.

1. רגרסיה מ-88a81038: במפרשי הרי"ף הפגינציה שהקישורים מוכיחים הפעילה את ההצמדה לכותרת הדף, ולכן פרק שמתחיל באמצע עמוד נפתח בכותרת העמוד ובלע את סוף הפרק הקודם יחד עם שורת "הדרן עלך" שלו. כך היה בר"ן על חולין, על עבודה זרה, על גיטין, על מגילה ועל שבת, בנימוקי יוסף על יבמות, על בבא קמא ועל בבא בתרא, וברבינו יונה על ברכות.
   עכשיו השלב קורא בטקסט של המפרש את שורות הסיום ("הדרן עלך...", "סליקו להו...", "סליק פירקא") ואת שורות הפתיחה ("פרק..."). פרק לא נפתח מעל שורה שמסיימת פרק קודם, בין כותרת העמוד לבין החומר שלו. הוא נפתח בשורה שאחריה, וכותרת העמוד נשארת בפרק הקודם עם השורות שתחתיה. הכלל חל על ההצמדה לפי קישורים, על השיבוץ לפי כותרות דף ועל השלמת פרקים בלי קישור. כך תוקנו גם שורות "הדרן" שהיו בפרק הבא כבר ב-v30, במפרשים שמשובצים לפי כותרות דף (צאן קדשים, חק נתן, ראש יוסף, בית אהרן, מים קדושים, פורת יוסף ועוד). שורת סיום שממשיכה בדיבור הראשון של הפרק הבא נשארת בפרק שהיא מסיימת, כמו ב-v30.
   לא נחשבים כשורת סיום: "סליק להו" בתוך דיון ארמי, "לא סליק להו", וסימון באמצע שורה ארוכה. כשהפרק נפתח לפני שורת הסיום, כמו בספר שעובר על כל עמוד פעמיים (ברכת הזבח: "חדושים" ואחריהם "הגהות"), אין שורה אחת שמחלקת את העמוד, וכותרת העמוד פותחת את הפרק כמו קודם.

2. הגהות הב"ח על רי"ף ביצה היו כולן תחת פרק אחד. הבסיס שנבחר היה מבנה ה-Chapters של מלחמת השם והמאור הקטן, שבו יש רק שני פרקים, והשמות שלו מוזזים.
   מבנה Chapters של מפרש (שאינו ספר בסיס) נבדק מול כל בסיס אחר שמקשר אליו ושיש לו אותה פגינציה: לפחות שני קישורים, ותשעה מכל עשרה על אותו עמוד. אם יש בסיס כזה שמקשר לפחות חמש שורות בתוך פרקים בשני הספרים, ואף בסיס כזה לא שם מחצית מהן בפרק בעל אותו שם, המבנה אינו בסיס. כך נפסלו 27 מבנים, כולם של המאור הקטן, המאור הגדול ומלחמת השם. ספריא עצמה מסתירה אותם (exclude_structs).
   הפרקים של ספרים כאלה מחושבים מהקישורים שלהם כמו של כל מפרש. הם לא נכתבים, והמבנה המקורי שלהם נשאר כפי שיובא. במעבר שלישי הם נזקפים לזכות ספרים שאין להם בסיס אחר. כך כתוב שם על ברכות, ביצה, סוכה, פסחים ומועד קטן, שהקישורים שלהם הם רק מהמאור הקטן, מקבלים את פרקי המסכת במקום התוויות המוזזות. הב"ח על ביצה מקבל את חמשת פרקי הרי"ף.

3. "(לעיל) דף ט." נקרא עכשיו ככותרת דף. כשיש פגינציה, קטע כזה שייך לפרק של הדף שלו בכל מקום שהוא נמצא. בקרן לדוד על סוכה הקטע על דף ט. עובר לפרק "סוכה", במקום "לולב הגזול".

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

אימות: הרצתי את השלב על עותק של הטבלאות מהמסד המועמד, בלי המבנים שעברו בירושה. לפני התיקון הפלט זהה למועמד, וגם הקוד שלפני 88a8103 נותן פלט זהה ל-v30. 542 מבנים נכתבו. השתנו 45 ספרים, וכל השינויים נבדקו.
הבודק כולל עכשיו את הבדיקות האלה:
- שורת סיום בפרק אחר מזה שהיא מסיימת;
- שורה שהקישורים שלה מהקבוצה או מבבלי ומהרי"ף אינם כוללים את הפרק שלה, גם דרך מפרש ביניים;
- רציפות של כל פרק בסדר השורות;
- כל כותרת דף מופיעה פעם אחת;
- שני פרקים שנפתחים באותה שורה;
- פרק בלי שורות.
התוצאות, מול המועמד ומול v30, שורה מול שורה:
- שורות סיום בפרק הלא נכון: תוקנו 65 מול המועמד ו-54 מול v30.
- שורות שהקישורים מבבלי ומהרי"ף סותרים: תוקנו 21.
- שורות שהקישורים דרך מפרש ביניים סותרים: תוקנו 65.
- פרקים לא רציפים: מ-1 ל-0.
- נוספו כותרות הדף שהיו חסרות: 7.
- רגרסיות: אפס בכל הבדיקות.
37 כותרות דף בכתוב שם נראות מחוץ לטווח, אבל זה רק בבודק, שמודד עמודי רי"ף לפי טווחי הבבלי. לפי טווחי הרי"ף מספר הדפים שאינם במקומם ירד מ-73 ל-13.
נשארו ליקויים שהיו כבר ב-v30 ולא השתנו:
- שורות סיום במפרשים שהקישורים אליהם דלילים, על הרא"ש ועל הבבלי (קרבן נתנאל, דברי חמודות, פלפולא חריפתא), ובספרים שעוברים על כל עמוד פעמיים;
- הסדרה השנייה בחידושי חתם סופר;
- ההפניות בקובץ שיטות קמאי;
- בסיסים שאי אפשר לשפוט, כמו הלכות קטנות לרי"ף (מנחות) ורי"ף מגילה, ששמות הפרקים השלישי והרביעי בו מוחלפים במקור.
פרק שהמפרש עוזב וחוזר אליו נכתב פעם לכל רצף, והמזהים נשמרים לפי מספר
הפרק בבסיס: כל פרקי הבסיס קודם, והרצפים המאוחרים אחריהם. הקורא ממיין
אחים לפי מזהה, ולכן קרן לדוד על סוכה הוצג "סוכה, לולב הגזול, לולב
הגזול" במקום "לולב הגזול, סוכה, לולב הגזול", ודגל האח האחרון ניתן
לפרק שאחריו בא עוד אח. כך גם פרק שנפתח מעל הראשון שלא כסדר (קרן לדוד
על שבת).

המפתחות והמזהים שנשלפים מה-build state לא השתנו. כל קבוצת אחים מחלקת
מחדש את המזהים ששלפה, בסדר עולה לפי שורת הפתיחה, והאחרון בשורות מסומן
אחרון. הדפים שתחת הפרק שומרים על המזהים שלהם. כשסדר הפרקים הוא סדר
השורות, החלוקה זהה לקודמת והשורות שנכתבות זהות לגמרי; בדיקה משווה מול
הכותב הקודם, עם build state ובלעדיו, ובבנייה חוזרת.
המשמר sourceNumbered זיהה רק "(א) " ו-"{א} ", ולכן 0525264 הוסיף את
הסימון גם לשורות שכבר נפתחות בסימון משלהן: "(נח) [נח]" ב-7,428 שורות של
אליה רבה, "(ב) ב) תפלה" בביאור על ספר המצוות לרס"ג, "(ב) <b>ב</b>" בפרקי
אבות שבסידור ספרד.

- SOURCE_MARKER_REGEX מזהה עכשיו גם [א], א) (גם צמוד, "נה)עינוי"), א.
  ו-א:, וגם <b>א</b>, אחרי תגים פותחים. סוגריים יכולים להיצמד לתג או
  לנקודתיים ("[ב]:").
- במערך שהמספור שלו שבור, פריט שנפתח בסימון של המספר שלו עצמו נשאר בלי
  התוספת. פריט בלי סימון, או עם סימון של מספר אחר, מקבל אותה כמו קודם.

נבדק מול ייצוא ספריא 2026-09-30_18-17-36735409591-1 (6,216 ספרים,
4,224,933 שורות). מספר זהה מיד אחרי "(X) " יורד מ-11,822 שורות ב-29 ספרים
לשורה אחת (מהרש"ל, "<b>ב'</b>" עם גרש ASCII, נשאר בכוונה). 11,854 שורות
מאבדות את התוספת ושום טקסט אחר לא משתנה. מפתחות השורות לא משתנים, ולכן
גם מזהי השורות. מתוך 61,887 השורות של 0525264 חוזרות לנוסח של v29
8,148 (אליה רבה 7,429, רס"ג 604, סידור ספרד 114, סדר מעמדות 1), ו-53,739
שומרות את הסימון.
ב-relink של v30 אבדו 906 קישורי LINKER ב-16 ספרים שהשורות שלהן קיבלו
"(א) " ב-05252649 (שו"ע הרב 1073→563, ארבעת חלקי באר היטב, אליה רבה, בית
שמואל, חלקת מחוקק). הטקסט עצמו לא השתנה. ה-NER של ספריא מתייג את הסימון
כציטוט, ופתרון שלו משנה את הקשר ה-ibid לפני הציטוט הראשון בשורה, כמו
"בסי' ע"ט ס"ט". ההפסד כמעט כולו בציטוט הראשון בשורה (באר היטב חו"מ: 90
מול 2), ובשורות שלא השתנו לא אבד דבר. המסנן של LinkerToOtzaria מוחק רק
את רשומת הסימון אחרי הפתרון, ושינוי בו משנה את טביעת המנוע ומחייב
relink מלא.

LinkerInputView מגדיר את הטקסט שה-linker מקבל: בשורת ספריא שנפתחת
בספרה עברית בסוגריים, בדיוק לפי leading_hebrew_numeral_marker_end של
ה-linker, הסימון ורווח אחד אחריו לא נשלחים. DumpLines כותב את התצוגה הזו
ל-lines_snapshot ומסמן linker_input_policy ב-meta. GenerateLinkerLinks
בודק את source_hash מול אותה תצוגה ומזיז את ה-offsets בחזרה לשורה
השמורה. רשומה עם hash של השורה המלאה (מלפני השינוי) עדיין מתקבלת כמו
שהיא. ספרים שאינם מספריא לא משתנים.

נבדק על השורות שאחרי התיקון הקודם (DumpLines אמיתי על 109 הספרים): ב-14
מתוך 16 הספרים שנפגעו הקלט ל-linker זהה בבתים לקלט של v29 (בבית שמואל
שורה אחת שונה, באור זרוע 16). בכל 7,647 רשומות v29 של 16 הספרים ה-hash
תואם לשורה החדשה והעוגן נופל על אותו טקסט. 4,279 מהן מוזזות. ההגדרה ב-Kotlin
זהה לפונקציה ב-Python על 499,239 תחיליות שורה אמיתיות.

המחיר: ב-relink הבא משתנה הקלט של כ-817 ספרי ספריא שיש בהם שורות
כאלה (בערך 1/6 מהטקסט). זה relink חלקי, לא מלא, כי טביעת המנוע לא
משתנה. בכ-700 מהם הסימון קיים עוד לפני v30, ולכן הפלט שלהם ישתנה גם
ביחס ל-v29.
1487ea2 הרחיב את SOURCE_MARKER_REGEX גם לבדיקת המערך כולו. כך מערך
שהמספור שלו במקור מוזז באחד מהמספור של ספריא נחשב ממוספר במקור, ואיבד
את כל התוספות. בכף אחת כ"ד ובמלמד להועיל ב' קל"ג סעיף ב' מציג "א."
והפריט הראשון נשאר בלי מספר. גם פתיח וזנב של מערך כזה איבדו את המספר
(באר הגולה חו"מ, איסור והיתר הארוך).

- בדיקת המערך חוזרת בדיוק לגרסה שלפני 1487ea2: רק "(אות) " ו-"{אות} ".
- הצורות החדשות ([א], א), א., <b>א</b>) מבטלות רק את התוספת של הפריט
  שלהן, ורק כשהן אותו מספר. סימון של מספר אחר משאיר את "(X) " כמו קודם.
- סימון שבא מיד אחרי כותרת פותחת (טקסט עטוף בתגים לבדו לפני ה-<br>
  הראשון, כמו "<big><strong>סימן א. …</strong></big><br>א." בקשר גודל)
  נחשב רק כשבאותו מערך יש פריט אחר שנפתח בסימון שלו. "<br>א." בודד הוא
  בדרך כלל רשימת עניינים (תלמוד עשר הספירות). דיבור מודגש עם פירוש לפני
  ה-<br> אינו כותרת (ביאור הגר"א).

נבדק מול ייצוא ספריא 2026-09-30_18-17-36735409591-1 (6,216 ספרים,
4,224,933 שורות):
- כל שורה שמאבדת את "(X) " נושאת סימון של אותו מספר: 11,822 בתחילת
  השורה, 36 אחרי כותרת פותחת (קשר גודל 25, רס"ג 6, סידור ספרד 4, אליה
  רבה 1). אין אף שורה שמאבדת את המספר בלי סימון משלה.
- מספר כפול בתחילת השורה נשאר רק בשורה אחת (מהרש"ל "<b>ב'</b>", גרש
  ASCII, נשאר בכוונה).
- 11,858 שורות מאבדות את התוספת. 166 מהן בספרים שברשימה השחורה (קול
  התור וחלקי במראה הבזק), ולכן ב-DB אלה 11,692 שורות בטבלת line. המספר
  11,854 בהודעה של 1487ea2 ספר את הפלט של הקורא כולל הספרים האלה. 67
  השורות שב-version_line הן טקסט הגרסאות, שהבדיקה הזו לא קוראת. שום טקסט
  אחר לא משתנה. מפתחות השורות לא משתנים.
- מתוך 61,887 השורות של 0525264 חוזרות לנוסח של v29 8,156 (אליה רבה
  7,429, רס"ג 608, סידור ספרד 118, סדר מעמדות 1), ו-53,731 שומרות את
  הסימון.
…עדיהם

עד עכשיו LinkerInputView הסיר את התווית "(א) " רק בשורות של ספריא. אותו
נזק לפתרון ה-ibid קיים גם בספרי אוצריא שהשורות שלהם נפתחות בסימון כזה.

- הבחירה לפי ספר (LinkerInputView.modeFor). בספריא כל ספר, תווית פשוטה
  בלבד, בדיוק כמו קודם. בכל מקור אחר רק ספר שיש בו לפחות 20 שורות
  שנפתחות בתווית, וזה מחושב מהשורות של הספר עצמו בלבד. הבחירה יכולה
  להשתנות רק כשנוספות או נמחקות שורות עם תווית. עריכה כזו משנה ממילא את
  ה-snapshot של הספר, וה-linker קורא אותו מחדש באותו מחזור, כך שהיפוך לא
  עולה relink נוסף ולא יכול להתנגש ברשומות ישנות.
- בספרים האלה יורדת גם תווית שעטופה לבדה בתגים שלה, "<b>(א)</b> " או
  "<sup …>(א)</sup> ". התגים חייבים להיסגר בסדר הפוך מיד אחרי התווית,
  כלומר יורד רק קטע מאוזן שאין בו דבר מלבדה. כותרות לא נוגעים בהן.
  תווית שממשיכה בתוך התג ("<b>(א) נידון</b>") נשארת, כי הסרתה אינה
  קידומת. מה שיורד הוא תמיד קידומת, ולכן offset בתצוגה ועוד אורך
  הקידומת הוא אותו מקום בשורה השמורה.
- DumpLines מחזיק את השורות של ספר אחד עד שהתצוגה שלו ידועה, ורושם את
  הספרים שנבחרו בטבלה lines_snapshot_stripped_books. ב-meta
  linker_input_policy עובר ל-v2.
- GenerateLinkerLinks מזהה את התצוגה לפי source_hash: השורה המלאה, התצוגה
  הרחבה או התצוגה הפשוטה. רשומה עם hash של השורה המלאה מתקבלת כמו שהיא.

נבדק ב-DumpLines אמיתי על DB מוקטן מ-fb/out2: כל 277 ספרי אוצריא שיש
בהם תווית, 40 ספרי אוצריא בלי תווית ו-25 ספרי ספריא, 1,141,928 שורות.
- כל שורה ב-snapshot זהה לחישוב עצמאי ב-Python, שמשתמש בפונקציה של
  ה-linker. 0 אי-התאמות.
- נבחרו 111 ספרים (Dicta 92, MoreBooks 7, Orayta 6 ועוד). יורדות 57,692
  שורות: 51,838 מתוך 52,385 תוויות פשוטות (99.0%) ו-5,854 מתוך 6,019
  עטופות. הספרים האלה הם 9.0% מטקסט אוצריא (4.5% מכלל הטקסט). בכולם יש
  "שם", כך שכל ספר נקרא מחדש במלואו.
- 206 ספרי האוצריא שלא נבחרו (140 מהם עם 674 תוויות ספורדיות) זהים
  בבתים לשורות השמורות. 25 ספרי הספריא זהים לתצוגה של 4b599b3.
- רשומות ישנות: 81,959 רשומות v30 ו-39,909 רשומות v29 מתקבלות בהזזה 0.
  20,404 רשומות שה-linker ייצר על התצוגה החדשה חוזרות בדיוק לאותו עוגן.
  92 רשומות v30 יושבות בתוך תווית עטופה ("<b>(ב)</b>" → Bava Kamma 107b),
  ולא ייווצרו יותר.
… נחשב

QA מצא שלוש בעיות בכלל של e991876:
- בביאור הגר"א חו"מ ז:יא השורה "<b>י"א. </b> שבת" איבדה את "(יא)". שם
  "י"א" הוא "יש אומרים", ולא הסימון של הס"ק.
- אליה רבה קסב:ה נשארה "(ה) [אות ה]".
- כ-630 שורות נשארו עם המספר פעמיים: "<b>א </b>" בתרומת הדשן, "<b>א.</b>"
  באורחות חיים להרא"ש, "( א )" בנהר מצרים.

מה השתנה:
- צורות חדשות שמזוהות: "( אות )" עם רווחים, "[אות X]", "אות.</b>",
  ו-"<b>אות</b>" עם רווח או נקודה לפני ה-</b>. המילים "סעיף" ו-"ס"ק" אינן
  סימון, כי הן מפנות לסעיף של ספר אחר.
- ספרה שהיא כל התוכן של קטע מודגש ("<b>י"א. </b>", "<b>  לא  </b> פלוג")
  היא גם הצורה של דיבור קצר. היא נחשבת סימון רק כשיש באותו מערך פריט
  נוסף שנושא סימון משלו, כמו בתרומת הדשן ובסידור ספרד. זה גם הכלל שכבר חל
  על סימון אחרי כותרת פותחת. כל שאר הצורות נחשבות גם לבד.

נבדק מול ייצוא ספריא 2026-09-30_18-17-36735409591-1 (6,216 ספרים,
4,224,933 שורות). origin/otzaria היא נקודת ההשוואה:
- 12,479 שורות מאבדות את "(X) ". 166 מהן בספרים שברשימה השחורה (קול
  התור וחלקי במראה הבזק), ולכן ב-DB אלה 12,313 שורות בטבלת line. טקסט
  הגרסאות ב-version_line לא נבדק כאן.
- בכל אחת מהן יש סימון של אותו מספר: בתחילת השורה, או ב-36 שורות מיד
  אחרי כותרת פותחת. בבדיקה עצמאית עם זיהוי מקל יותר, אין אף שורה שמאבדת
  את המספר בלי סימון משלה. אין שינוי טקסט אחר, ומפתחות השורות לא משתנים.
- לעומת e991876: 591 שורות של תרומת הדשן, 29 של אורחות חיים להרא"ש, ושורה
  אחת בנהר מצרים ואחת באליה רבה מאבדות את הכפילות. השורה בביאור הגר"א
  מקבלת שוב את "(יא)".
- שורות שעדיין מציגות את אותו מספר פעמיים, בבדיקה המקלה: 9 בתחילת השורה
  (קודם 630).
  - חמש מהן דיבור ולא סימון: ביאור הגר"א, אורים ותומים "<b>י"א </b>",
    מעדני יום טוב "<b>  לא  </b>", באר היטב יו"ד "<b>ט'. </b>" ושערי תשובה
    "<b>ב' </b>".
  - שתיים בשומר ישראל הן אות ראשונה של מילה, "<strong>ד</strong>רשתי".
  - שתיים במהרש"ל, "<b>ב'</b>" עם גרש ASCII, נשארות בכוונה.
  עוד 15 אחרי כותרת: 14 רשימות עניינים בתלמוד עשר הספירות, ואחת בשולחן
  ערוך אבן העזר קמד:א, כותרת יחידה במערך שלה.
…סמן את עצמו

QA מצא 41 שורות שעדיין מציגות את המספר פעמיים, כי הסימון שלהן אינו הכתיב
הקנוני של המספר: בקשר גודל "(יז) טוב.", "(יח) חי.", "(י) יוד.", "(כא) אך.",
"(כז) זך." (35 שורות), ובמפתחות של תרומת הדשן "(רנ) <b>רן </b>", "(שמד) <b>דשמ </b>",
"(שנ) <b>שן </b>", "(שנד) <b>דשן </b>", "(קנ) <b>קן </b>" (6 שורות).

- צורה אחרת של מספר: אותן אותיות בכל סדר, אותיות סופיות בערך שלהן, עד ארבע
  אותיות. וגם "יוד" ל-10, שאינו סכום של אותיותיו. היא נחשבת רק כשהיא אותו
  מספר, ורק באותן צורות סימון שהכלל כבר מכיר.
- היא נחשבת רק בחלק של הספר (צומת בסכמה) שבו לפחות 20 פריטים, ולפחות 60%
  מהפריטים שמקבלים "(X)", נושאים סימון קנוני משלהם. מעבר ראשון על הספר סופר,
  ורק ספר שיש בו חלק כזה עובר מעבר שני. בייצוא הנוכחי יש 8 חלקים כאלה בטקסט
  הראשי ו-3 בגרסאות.
- השער הוא לפי חלק ולא לפי ספר: בתרומת הדשן חלק א' ממוספר בלי סימונים (0 מתוך
  708), ולכן הספר כולו רק 45.3%, אבל המפתחות 98.8% ו-99.2%.

למה 60% ו-20: בייצוא ספריא 2026-09-30_18-17-36735409591-1, בכל חלק עם 20 סימונים
קנוניים או יותר, השיעור הוא עד 21.1% (באר הגולה חו"מ, אורחות חיים להרא"ש) או
לפחות 68.4% (קול התור, ובשאר 92% ומעלה). בלי השער, הצורה האחרת הייתה מורידה גם
7 שורות בטקסט הראשי (ועוד 2 בגרסה), כולן בחלקים של 0%-0.1% ואף אחת מהן אינה
סימון: "<b>בידי. </b>" בסעיף כ"ו ו-"<b>בלבו. </b>" בסעיף מ' (באר היטב אבה"ע),
"<b>אבל </b>" בסעיף ל"ג (אורים ותומים, תומים), "<b>  אינן  </b>" בסעיף קי"א (מעדני
יום טוב על נדה), "האחד:" בסעיף י"ח (במראה הבזק חלק י'), ועוד "<b>י"א </b>" ו-"<b>  לא  </b>"
באותם מערכים, שהיו נחשבים רק משום ש"אבל" ו"אינן" נתנו להם פריט מסומן נוסף.

נבדק מול אותו ייצוא (6,216 ספרים, 5,485,998 שורות כולל גרסאות עבריות), לעומת
227dd54:
- משתנות 43 שורות בדיוק: קשר גודל 35, תרומת הדשן 6, ביאור על ספר המצוות לרס"ג
  1 ("(י) יו"ד)") ואותה שורה בגרסת ורשה שלו. בכל אחת יורד רק ה-"(X) " שנוסף.
- ה-ref, אינדקס השורה ותוכן מפתח השורה לא משתנים. גם הטקסט שה-linker מקבל
  (LinkerInputView) זהה בבתים, כי הוא כבר הוריד את ה-"(X) " הזה.
- שום שורה אחרת לא משתנה.
Y-PLONI and others added 21 commits October 22, 2026 07:01
סכמה 7 לפני v33: עוגנים בגופי הערות, דחיסת line_note, hasMarker, span לשמות קודש, מילון באנרים וסדר hash לפי PK
The first schema-7 build (DB ~8.5 GiB) filled the 20G tmpfs at the Phase-2
pair commit (DB + VACUUM INTO candidate + buildstate twice) and failed with
SQLITE_FULL (run 39748793289).
book_banners.csv required a distinct id on every row, so one wording used by
91 books became 91 banner rows, and a wording change touched all of them.
Rows may now repeat an id when they carry the identical text; the generator
already stores each (id, text) once, so banner keeps one row per wording.
One id with two texts still fails the build.

Needed by the otzaria-library change that moves the in-book copyright
lines into banners (317 rows, 87 wordings).

Verify: ./gradlew :sefariasqlite:jvmTest --tests '*SeedBookNoticesTest*'

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
בספר העיטור כמה דפים מתחילים בתוך אותה פסקה. processAltTocStructures השמיט כל ערך שהצביע על שורה של אח קודם, ולכן מבנה הדפים היה מאבד כ-130 מתוך 304 דפים.

עכשיו כל האחים נשמרים. ב-line_alt_toc השורה המשותפת משויכת לאחרון שבהם, והעמוק עדיין גובר על ההורה. האפליקציה גוללת לסמן [כותרת…] בתוך השורה.

בדיקה: siblingsSharingALineAreAllKept נכשלת בלי התיקון.
…line-3181

alt_toc: אחים על אותה שורה נשמרים כולם (otzaria#3181)
sefaria_author_changes.csv reached only the `author` table; line 2 of the
book body kept Sefaria's spelling. The reader now applies the same mapping
before writing the author line, so both agree (kdroidFilter#96).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-line-canonical

תיקון: שורת המחבר בספרי ספריא אחרי המיפוי הקנוני (kdroidFilter#96)
build-library-index.yml פיצל תמיד, ולכן גם אינדקס של 1.6 GB התפרסם כ-otzaria-library-index.tar.zst.part-000 עם מניפסט. מעכשיו, כמו ה-DB המלא, הארכיון מתפרסם שלם כשהוא עד PART_SIZE, ומפוצל לחלקים עם מניפסט רק מעליו. ריצה חוזרת שעוברת בין שתי הצורות מוחקת מה-release את נכסי הצורה האחרת.

build_library_vectors.sh מוריד את הארכיון השלם כשהוא קיים ב-release ומאמת אותו מול ה-sha256 שב-provenance; אחרת ממשיך דרך המניפסט והחלקים כמקודם. הצרכן בצד האפליקציה (fetch_prebuilt_library_index.sh) כבר תומך בשתי הצורות (Otzaria/otzaria bb9942bbf).
בנייה של v33 נכשלה ב-build hook של otzaria_search_engine: rustup could not choose a version of rustup to run ... no default is configured. מנוע שמוצמד לקומיט git מקומפל מקוד המקור, ו-Dart לא מעביר ל-build hooks את RUSTUP_HOME ו-CARGO_HOME. לכן המנוע מסיק אותם מהמבנה ‎.cargo לצד ‎.rustup, ובתמונה הם היו ב-/opt/cargo וב-/opt/rustup. rustup חיפש ב-~/.rustup הריק.

ההתקנה עוברת ל-/opt/rust/.rustup ול-/opt/rust/.cargo, ו-PATH מעודכן בהתאם. התמונה נבנית מחדש בכל ריצה.
ריצת האינדקס של v34 נכשלה בבדיקת ההתאמה לצעדי build_linux: אוצריא עברה ל-Flutter 3.47.7 (0ad7c0367) והוסיפה --dart-define=OTZARIA_BUILD_COMMIT ל-flutter build linux.

FLUTTER_VERSION, FLUTTER_COMMIT ו-FLUTTER_SHA256_X86_64 עודכנו לפי releases_linux.json ולפי התג 3.47.7. טביעת צעד ה-aarch64 עודכנה (ההבדל היחיד הוא מספר הגרסה), וכך גם העותק המשוקף של פקודת הבנייה ותבניות הבדיקה. הבנייה של האינדקס עצמו נשארת בלי defines.
מנוע 6.0.0 (5ea4900c) מביא סכמת אינדקס 10 ו-LINE_TEXT_VERSION 4. אינדקס הספרייה נבנה מעכשיו בסכמה 10, ותוסף בסכמה 7 לא היה קורא אותו. סטים בגרסת טקסט 3 לא תואמים לאפליקציה החדשה, ולכן גם התכנון והאימות עוברים ל-6.0.0.

ה-sidecar לא השתנה (86f45489); bootstrap_runner.sh בונה מחדש את export_semantic_plan ואת validate_semantic_vectors כשה-pin משתנה.
rot192-i8i4 מגיע ל-r@10 0.977 ו-r@50 0.979 על 10 מיליון מפתחות החלונות של v34, מתחת לרף 0.98 של המאמת. הניסויים הראו שה-alias והסיבוב אינם הגורם, ו-i8-sym-vec עובר אך גדול ב-64%.

G6_FLOORS ב-pins.env קובע רף לפי משפחה, ו-validated() מעביר אותו כ---min-recall-10/50. שאר המשפחות נשארות עם רף המאמת.
… מכריע

שער update-fordb מריץ את category_renames על ה־DB המשוחרר, שבו עץ אוצריא כבר
קיים. הכלל «מילונים וספרי יעץ» -> «ספרות עזר» נכשל שם על סתירת heShortDesc
(run 40520029742), ואחריה היה נכשל שוב על נתיבים כפולים, כי המיזוג העביר את
«דקדוק», «מילונים» ו«אנציקלופדיות» לצד הקטגוריות באותו שם.

- renameOrMergeCategory ממזג תת־קטגוריות באותו שם לעומק, כמו ששלב אוצריא מכניס
  תיקייה לקטגוריה קיימת. כל התיאורים נפתרים לפני השינוי הראשון.
- סתירת תיאור נפתרת לטובת היעד רק כש־sefaria_category_changes.csv מחליף או מוחק
  את השדה בנתיב היעד, כך ש־seedAllMetadata דורס אותו. אחרת המיזוג עדיין נכשל.
- הבנייה (preflight והמעבר האמיתי) וה־validator מעבירים את הכללים האלה.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


0 out of 2 committers have signed the CLA.
❌ @Y-PLONI
❌ @palmoni5
You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@palmoni5

Copy link
Copy Markdown
Contributor

I have read the CLA Document and I hereby sign the CLA

@palmoni5

Copy link
Copy Markdown
Contributor

@YOSEFTT
אתה בטוח שהתכוונת להכניס פה 606 קומיטים??

@Y-PLONI

Y-PLONI commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

@YOSEFTT @palmoni5
הוא התבלבל.... נראה שהוא התכוון להגיע לfork שלנו...

@YOSEFTT

YOSEFTT commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

@Y-PLONI אתה צודק!

@YOSEFTT YOSEFTT closed this Oct 28, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 28, 2026
@YOSEFTT YOSEFTT reopened this Oct 28, 2026
@YOSEFTT YOSEFTT closed this Oct 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants